[volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현#37
[volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현#37MINJOOOONG wants to merge 5 commits intoLoopers-dev-lab:MINJOOOONGfrom
Conversation
Walkthrough이 PR은 사용자 관리 기능을 구현합니다. 회원가입, 인증, 비밀번호 변경을 위한 REST API 엔드포인트, 비즈니스 로직, 데이터 모델, 검증자들을 추가하며, 관련 테스트와 인프라 설정도 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as UserV1Controller
participant Service as UserService
participant Validators as Validators<br/>(Email, LoginId, etc.)
participant PasswordEncoder
participant Repository as UserRepository
participant Database as User Entity<br/>(DB)
rect rgba(100, 150, 200, 0.5)
Note over Client,Database: 회원가입 흐름 (POST /register)
Client->>Controller: POST /register<br/>(loginId, password, name, email, birthDate)
Controller->>Service: register(...)
Service->>Validators: validate all fields
Validators-->>Service: validation result
alt validation failed
Service-->>Controller: CoreException (BAD_REQUEST)
Controller-->>Client: 400 Bad Request
else validation passed
Service->>Repository: existsByLoginId(loginId)
Repository->>Database: query
Database-->>Repository: exists?
alt loginId already exists
Service-->>Controller: CoreException (CONFLICT)
Controller-->>Client: 409 Conflict
else loginId available
Service->>PasswordEncoder: encode(rawPassword)
PasswordEncoder-->>Service: hashedPassword
Service->>Repository: save(new User(...))
Repository->>Database: insert
Database-->>Repository: User (persisted)
Repository-->>Service: User
Service-->>Controller: User
Controller-->>Client: 201 Created (UserResponse)
end
end
end
rect rgba(150, 200, 100, 0.5)
Note over Client,Database: 사용자 정보 조회 흐름 (GET /me)
Client->>Controller: GET /me<br/>Headers: X-Loopers-LoginId, X-Loopers-LoginPw
Controller->>Service: authenticate(loginId, rawPassword)
Service->>Repository: findByLoginId(loginId)
Repository->>Database: query by loginId
Database-->>Repository: User
Repository-->>Service: Optional<User>
alt user not found
Service-->>Controller: CoreException (UNAUTHORIZED)
Controller-->>Client: 401 Unauthorized
else user found
Service->>PasswordEncoder: matches(rawPassword, hashedPassword)
PasswordEncoder-->>Service: match result
alt password mismatch
Service-->>Controller: CoreException (UNAUTHORIZED)
Controller-->>Client: 401 Unauthorized
else password matches
Service-->>Controller: User
Controller->>Validators: NameMasker.mask(name)
Validators-->>Controller: maskedName
Controller-->>Client: 200 OK (MeResponse with masked name)
end
end
end
rect rgba(200, 150, 100, 0.5)
Note over Client,Database: 비밀번호 변경 흐름 (PATCH /me/password)
Client->>Controller: PATCH /me/password<br/>Headers: X-Loopers-LoginId, X-Loopers-LoginPw<br/>Body: currentPassword, newPassword
Controller->>Service: authenticate(loginId, currentPassword)
Service->>Repository: findByLoginId(loginId)
Repository->>Database: query
Database-->>Repository: User
Repository-->>Service: Optional<User>
alt auth failed
Service-->>Controller: CoreException (UNAUTHORIZED)
Controller-->>Client: 401 Unauthorized
else auth succeeded
Service->>Validators: validatePasswordChange(currentPassword, newPassword)
Validators-->>Service: validation result
alt validation failed
Service-->>Controller: CoreException (BAD_REQUEST)
Controller-->>Client: 400 Bad Request
else validation passed
Service->>PasswordEncoder: encode(newPassword)
PasswordEncoder-->>Service: hashedNewPassword
Service->>Repository: update user password
Repository->>Database: update
Database-->>Repository: success
Repository-->>Service: ok
Service-->>Controller: void
Controller-->>Client: 200 OK (ApiResponse<Void>)
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
modules/jpa/src/main/resources/jpa.yml (1)
49-105:⚠️ Potential issue | 🟡 Minor
test프로필 블록이 중복 정의되어 있습니다.Line 50과 Line 89에서 동일한
spring.config.activate.on-profile: test블록이 두 번 선언되어 있습니다. Spring Boot YAML 다중 문서에서 동일 프로필의 후순위 블록이 선순위 블록의 값을 덮어쓰므로, 첫 번째test블록(Line 50-62)의 설정은 사실상 무의미합니다.AI 요약에 따르면 기존
prd프로필이test로 변경된 것으로 보이는데, 이로 인해 중복이 발생한 것 같습니다. 두 블록을 하나로 통합해야 합니다.중복 블록 통합 제안
Line 49-63의 첫 번째
test블록을 제거하고, Line 88-105의 블록에 모든test프로필 설정을 통합하세요:--- -spring.config.activate.on-profile: test - -spring: - jpa: - show-sql: true - hibernate: - ddl-auto: create - -datasource: - mysql-jpa: - main: - maximum-pool-size: 10 - minimum-idle: 5 - ---- spring.config.activate.on-profile: test spring: jpa: show-sql: true hibernate: ddl-auto: create datasource: mysql-jpa: main: jdbc-url: jdbc:mysql://localhost:3306/loopers username: application password: application maximum-pool-size: 10 minimum-idle: 5
🤖 Fix all issues with AI agents
In @.claude/README.md:
- Around line 25-27: README endpoints use /api/v1/members/... but the
implementation and PR use /api/v1/users/..., causing mismatch; update the listed
endpoints (회원가입, 내 정보 조회, 비밀번호 수정) to use /api/v1/users/register,
/api/v1/users/me, and /api/v1/users/me/password (or the exact route names used
by the UserController /users handlers) so the documentation matches the actual
routes referenced in the codebase.
In
`@apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java`:
- Line 12: PasswordPolicyValidator's ALLOWED_PATTERN only restricts allowed
characters but doesn't enforce the required composition or length; update
PasswordPolicyValidator to enforce 8-16 length and that the password contains at
least one lowercase, one uppercase, one digit and one special character (in
addition to the allowed-character check). Implement this by replacing or
augmenting ALLOWED_PATTERN with a single regex that enforces all rules (or keep
ALLOWED_PATTERN for characters and add separate checks using patterns like
(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~])^.{8,16}$)
inside the PasswordPolicyValidator validation method so passwords like
"abcdefgh" will fail. Ensure the change is applied wherever
PasswordPolicyValidator performs validation (the ALLOWED_PATTERN and the class's
validation method).
- Around line 39-43: The validatePasswordChange method in
PasswordPolicyValidator is dead/duplicated; replace the inline duplicate in
UserService.changePassword (the currentPassword.equals(newPassword) check) with
a call to PasswordPolicyValidator.validatePasswordChange(oldPassword,
newPassword) so production code uses the validator, and update
PasswordPolicyValidatorTest to reflect the validator being exercised;
alternatively, if you prefer removal, delete validatePasswordChange from
PasswordPolicyValidator and remove its use in tests and any references so only
the single check in UserService.changePassword remains—ensure tests are updated
accordingly.
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java`:
- Around line 40-47: Wrap the save call in UserService#create (or the method
that currently calls userRepository.existsByLoginId(...) and
userRepository.save(...)) with a try/catch that catches
DataIntegrityViolationException and rethrows a CoreException(ErrorType.CONFLICT,
"이미 존재하는 로그인 ID입니다."); keep the initial existsByLoginId check for fast-fail but
handle the TOCTOU race by converting DataIntegrityViolationException from
userRepository.save(user) into the same conflict CoreException so
ApiControllerAdvice returns 409 instead of 500.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`:
- Around line 51-57: Remove the MethodArgumentNotValidException handler from
ApiControllerAdvice: delete the
handleValidationException(MethodArgumentNotValidException e) method (and any
now-unused imports for MethodArgumentNotValidException or Collectors if only
used there), ensuring all validation errors are instead routed through the
existing CoreException-based flow; after removal, run compilation/tests to
confirm no references remain to MethodArgumentNotValidException or the deleted
method.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java`:
- Line 7: Remove the Bean Validation usage that causes
MethodArgumentNotValidException: delete the import jakarta.validation.Valid and
remove any `@Valid` annotations found in UserV1Controller (and any methods at the
noted locations), and also remove corresponding `@NotBlank` usages mentioned in
UserV1Dto; update the controller method signatures (e.g., methods handling user
create/update in UserV1Controller) to accept DTOs without `@Valid` so the existing
CoreException-based error handling remains intact.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java`:
- Line 4: Remove the Bean Validation annotations and imports from the DTO and
controller: delete the jakarta.validation.constraints.NotBlank import and any
`@NotBlank` usages in UserV1Dto (and rely on the domain
User/PasswordPolicyValidator for validation), and remove
jakarta.validation.Valid import and any `@Valid` annotations from
UserV1Controller; ensure no new MethodArgumentNotValidException handling is
added and domain-layer validators continue to enforce null/blank rules.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/user/PasswordPolicyValidatorTest.java`:
- Around line 66-113: Add negative tests under the existing CharacterValidation
nested class in PasswordPolicyValidatorTest to cover missing-required-character
combinations: create tests that call validator.validate(password, birthDate) and
assert a CoreException (ErrorType.BAD_REQUEST) for (1) password with only
lowercase letters and special char (e.g., "abcdefgh!"), (2) only uppercase
letters and special char, (3) missing digits (letters+specials), and (4) missing
special characters (letters+digits). Use the same test structure and assertions
as the existing space/Korean tests so they exercise
PasswordPolicyValidator.validate and fail when the required mix
(upper+lower+digit+special) is not met.
In `@apps/commerce-batch/src/main/resources/application.yml`:
- Line 7: The application.yml in commerce-batch currently hardcodes the active
profile to "test" via the active property; change the default active profile to
"local" in apps/commerce-batch/src/main/resources/application.yml and ensure
test runs explicitly set `@ActiveProfiles`("test") or use environment variables to
override the profile; also verify properties that should not be enabled by
default (batch.job.enabled and initialize-schema: always) are not forced by the
default profile so production deployments pick up correct values.
In `@CLAUDE.md`:
- Around line 206-209: Update the DTO guidance to match project practice: remove
the instruction "Jakarta Validation 어노테이션 사용" and instead state that DTOs
(records with a static factory method like from()) do not use Bean Validation
annotations; all input validation is performed in the domain layer (e.g., User
constructor, PasswordPolicyValidator and other domain validators). Keep the DTO
shape/record and the from() factory note, but explicitly document that
validation happens in domain classes and validators rather than with
Jakarta/Bean Validation on DTOs.
In `@loopers-docker/build.gradle`:
- Around line 27-28: The two QueryDSL lines are wrong: leave
com.querydsl:querydsl-jpa::jakarta as an implementation dependency but change
com.querydsl:querydsl-apt::jakarta to be declared as an annotation processor so
the Q-classes are generated at compile time; replace the implementation
declaration for querydsl-apt with an annotationProcessor configuration (use the
exact artifact id com.querydsl:querydsl-apt::jakarta) and keep querydsl-jpa as
implementation.
In `@loopers-docker/docker/infra-compose.yaml`:
- Around line 12-13: The docker-compose service currently sets unsupported env
vars MYSQL_CHARACTER_SET and MYSQL_COLLATE which the official mysql:8.0 image
ignores; remove those environment variables from the service and instead add a
command entry on the MySQL service (referencing the service name and the env var
names MYSQL_CHARACTER_SET / MYSQL_COLLATE to locate the lines) to pass MySQL
server startup options such as --character-set-server=utf8mb4 and
--collation-server=utf8mb4_general_ci so the desired charset/collation are
applied at runtime.
In `@loopers-docker/src/main/java/com/loopers/docker/ConnectionHealthLogger.java`:
- Around line 42-49: The logRedisConnection method leaks RedisConnection because
redisConnectionFactory.getConnection() returns a Closeable RedisConnection that
isn't closed; change logRedisConnection to obtain the connection in a
try-with-resources (or explicitly close in finally) around the ping() call so
the RedisConnection is always closed, preserving existing success and error
logging in the same try/catch structure and referencing
redisConnectionFactory.getConnection() and logRedisConnection.
In `@loopers-docker/src/main/java/com/loopers/docker/RedisConnectionLogger.java`:
- Around line 23-39: The RedisConnectionLogger code can leak connections and may
NPE when getConnectionFactory() is null; modify the block using redisTemplate so
you first null-check redisTemplate.getConnectionFactory() and bail with a log if
null, and when calling getConnection() (or ping()) obtain the Connection and
ensure it is closed (use try-with-resources or explicit close) instead of
relying on implicit closure; keep the existing test write/read using
redisTemplate.opsForValue().set/get and delete, but ensure the connection used
for ping is closed to prevent resource leaks (refer to RedisConnectionLogger,
redisTemplate, getConnectionFactory(), getConnection(), ping()).
In `@loopers-docker/src/main/resources/jpa.yaml`:
- Around line 18-28: Move the HikariCP-specific properties so they are nested
under spring.datasource.hikari (currently they are directly under
spring.datasource), e.g. relocate pool-name, maximum-pool-size, minimum-idle,
connection-timeout, validation-timeout, keepalive-time, max-lifetime,
leak-detection-threshold, initialization-fail-timeout and the
data-source-properties.rewriteBatchedStatements into spring.datasource.hikari.*
so Hikari maps them correctly; keep the same keys (pool-name, maximum-pool-size,
etc.) but place them under the hikari block (and use
spring.datasource.hikari.data-source-properties.rewriteBatchedStatements for the
rewriteBatchedStatements setting).
In `@loopers-docker/src/main/resources/redis.yaml`:
- Around line 6-11: The YAML uses the wrong property path: change the root key
from spring.datasource.redis to spring.data.redis and move the connection
properties out of the master nested object so Spring Boot reads them; i.e.,
ensure properties named database, host and port exist directly under
spring.data.redis (not spring.datasource.redis.master.host/port) so Spring Data
Redis picks up database, host and port values.
In `@modules/jpa/src/main/resources/jpa.yml`:
- Line 18: The default JDBC URL in jpa.yml (property name "jdbc-url") is missing
the database name; update the jdbc-url default value from
jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306} to include the database
path (for example
jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/loopers) so profiles
that rely on the default can connect successfully.
🧹 Nitpick comments (20)
.gitignore (1)
40-41: 불필요한 ignore 항목일 수 있음 (nul)
Windows 예약 장치명이라 파일로 생성되지 않아, 실제로 생성되는 산출물이 없다면 제거를 검토해도 됩니다.apps/commerce-batch/src/test/java/com/loopers/job/demo/DemoJobE2ETest.java (1)
58-61:run.id추가로 반복 실행 안정성 확보 — 좋습니다.
System.currentTimeMillis()기반의run.id를 추가하여 동일 파라미터로 인한JobInstanceAlreadyCompleteException방지가 잘 되어 있습니다.한 가지 참고 사항:
shouldFail_whenRequestDateMissing테스트에는run.id가 없습니다. 실패한 Job은 Spring Batch에서 동일 파라미터로 재실행을 허용하므로 현재는 문제 없지만, 일관성을 위해 해당 테스트에도run.id를 추가하는 것을 고려해볼 수 있습니다.♻️ 선택적 개선안
void shouldFail_whenRequestDateMissing() throws Exception { // arrange jobLauncherTestUtils.setJob(job); // act - var jobExecution = jobLauncherTestUtils.launchJob(); + var jobParameters = new JobParametersBuilder() + .addLong("run.id", System.currentTimeMillis()) + .toJobParameters(); + var jobExecution = jobLauncherTestUtils.launchJob(jobParameters);loopers-docker/build.gradle (1)
21-35: 테스트 의존성이 누락되어 있습니다.
spring-boot-starter-test를 비롯한 테스트 의존성이 전혀 선언되지 않았습니다. 테스트가 필요 없는 모듈이라면 무시해도 되지만, 프로젝트에 테스트를 추가할 계획이라면 추가가 필요합니다.추가 제안
// redis implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // test + testImplementation("org.springframework.boot:spring-boot-starter-test") }apps/commerce-api/src/test/java/com/loopers/domain/user/NameMaskerTest.java (1)
9-68: 테스트 구조가 깔끔하고, 주요 시나리오를 잘 커버하고 있습니다.AAA 패턴과
@Nested/@DisplayName을 활용한 구조가 가독성이 좋습니다.선택적으로,
null과 빈 문자열 입력에 대한 엣지 케이스 테스트를 추가하면NameMasker의 방어적 처리(return name)까지 검증할 수 있습니다.loopers-docker/docker/infra-compose.yaml (1)
1-1:version키는 최신 Docker Compose에서 더 이상 사용되지 않습니다.Docker Compose V2에서는
version필드가 무시됩니다. 제거해도 무방합니다.apps/commerce-api/src/main/java/com/loopers/domain/user/EmailValidator.java (1)
1-21: 전반적으로 깔끔한 구현입니다.도메인 레이어에서 검증 로직을 수행하고
CoreException으로 에러를 전달하는 프로젝트 패턴을 잘 따르고 있습니다. 기본적인 이메일 형식 검증 용도로는 충분합니다.사소한 개선 사항: Line 13에서
email.isEmpty()대신email.isBlank()를 사용하면 공백 문자로만 이루어진 입력도 함께 잡을 수 있습니다.♻️ isEmpty → isBlank 변경 제안
public void validate(String email) { - if (email == null || email.isEmpty()) { + if (email == null || email.isBlank()) { throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 필수입니다."); }apps/commerce-api/src/main/java/com/loopers/domain/user/User.java (2)
31-37: 생성자의password파라미터가 BCrypt 해시임을 문서화하는 것을 권장합니다.Learning에 따르면,
User생성자의password파라미터는 반드시 BCrypt 해시여야 하며, 평문 비밀번호가 전달되어서는 안 됩니다. 이 규약을 Javadoc으로 명시하면 향후 유지보수 시 실수를 방지할 수 있습니다.Based on learnings: "In User domain classes, the password parameter to the constructor should be a BCrypt hash. Do not pass or store plain-text passwords; the constructor should only accept a valid BCrypt hash. If applicable, document this expectation in the User class Javadoc."
📝 Javadoc 추가 제안
+ /** + * `@param` password BCrypt로 인코딩된 해시값. 평문 비밀번호를 전달하지 마세요. + * 반드시 UserService에서 PasswordPolicy 검증 후 인코딩된 값을 전달해야 합니다. + */ public User(String loginId, String password, String name, String email, String birthDate) {
39-41:changePassword도 동일하게 BCrypt 해시만 받아야 합니다.
changePassword메서드에도 평문이 아닌 인코딩된 해시만 전달되어야 한다는 가드 또는 문서를 추가하면 좋겠습니다. 현재 메서드는 어떤 문자열이든 무조건 할당하므로, 호출자가 잘못 사용할 여지가 있습니다.📝 문서화 제안
+ /** + * `@param` newPassword BCrypt로 인코딩된 새 비밀번호 해시 + */ public void changePassword(String newPassword) { this.password = newPassword; }apps/commerce-api/src/test/java/com/loopers/domain/user/EmailValidatorTest.java (1)
17-91: 테스트 커버리지가 적절합니다.프로젝트의 다른 검증기 테스트 패턴(LoginIdValidatorTest, BirthDateValidatorTest)과 일관된 스타일을 유지하고 있습니다.
한 가지 보완 사항:
EmailValidator.validate()가null입력도 명시적으로 처리하고 있으므로,null케이스에 대한 테스트도 추가하면 좋겠습니다.🧪 null 테스트 케이스 추가 제안
`@DisplayName`("이메일이 null이면 예외가 발생한다") `@Test` void throwsException_whenEmailIsNull() { // act CoreException result = assertThrows(CoreException.class, () -> { validator.validate(null); }); // assert assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
10-12:@Component대신@Repository사용을 고려해 보세요.
@Repository는 Spring의 persistence exception translation을 활성화하여 JPA 예외를 Spring의DataAccessException으로 변환해 줍니다. 인프라 레이어의 저장소 구현체에 더 적합한 스테레오타입입니다.♻️ 제안
-import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; `@RequiredArgsConstructor` -@Component +@Repository public class UserRepositoryImpl implements UserRepository {CLAUDE.md (1)
51-51: 코드 블록에 언어가 지정되지 않았습니다.Line 51과 Line 83의 fenced code block에 언어를 지정하면 가독성과 마크다운 린트 호환성이 향상됩니다.
♻️ 제안
Line 51:
-``` +```text RootLine 83:
-``` +```text com.loopersAlso applies to: 83-83
apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java (1)
14-17:password와birthDate에 대한 null 방어가 없습니다.
validate()메서드에서password가 null이면password.length()에서 NPE가 발생하고,birthDate가 null이면birthDate.replace()에서 NPE가 발생합니다. 상위 레이어에서 null이 들어올 가능성이 낮더라도, 도메인 검증기로서 방어적 검증을 두는 것이 안전합니다.apps/commerce-api/src/test/java/com/loopers/domain/user/LoginIdValidatorTest.java (1)
17-91:null입력에 대한 테스트 케이스가 누락되어 있습니다.
LoginIdValidator.validate()가null체크를 수행하고 있으므로(Line 13), 해당 경로를 커버하는 테스트도 추가하면 좋겠습니다.💡 null 테스트 케이스 추가 제안
`@DisplayName`("loginId가 null이면 예외가 발생한다") `@Test` void throwsException_whenLoginIdIsNull() { // act CoreException result = assertThrows(CoreException.class, () -> { validator.validate(null); }); // assert assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }loopers-docker/src/main/java/com/loopers/docker/RedisConnectionLogger.java (1)
28-36: 시작 시 Redis에 테스트 데이터를 쓰는 것은 공유 환경에서 부작용을 유발할 수 있습니다.헬스 체크 목적이라면
ping()만으로 충분합니다. 쓰기/읽기 테스트는 공유 Redis 인스턴스에서 키 충돌이나 예기치 않은 데이터 변경을 초래할 수 있으며, 삭제 전 예외 발생 시 테스트 키가 남게 됩니다.apps/commerce-api/src/main/java/com/loopers/domain/user/LoginIdValidator.java (1)
7-20: 구현이 기존 Validator 패턴(EmailValidator, BirthDateValidator)과 일관되게 잘 작성되었습니다.도메인 레이어에서 검증을 수행하고
CoreException을 던지는 프로젝트 패턴을 잘 따르고 있습니다.한 가지 고려 사항: loginId에 대한 길이 제약(최소/최대)이 없어,
"a"와 같은 극단적으로 짧은 ID나 매우 긴 ID가 허용됩니다. 향후 필요 시 길이 검증 추가를 고려해 보세요.loopers-docker/src/main/java/com/loopers/docker/ConnectionHealthLogger.java (1)
13-49:MySqlConnectionLogger,RedisConnectionLogger와 기능이 중복됩니다.같은 패키지에 이미 MySQL과 Redis 각각의 헬스 체크를 수행하는
MySqlConnectionLogger와RedisConnectionLogger가 존재합니다. 이 클래스가 추가되면 애플리케이션 시작 시 동일한 헬스 체크가 두 번 실행됩니다. 하나로 통합하거나, 기존 클래스들을 제거하는 것을 권장합니다.loopers-docker/src/main/java/com/loopers/docker/MySqlConnectionLogger.java (1)
10-37: ConnectionHealthLogger와 MySQL 연결 확인이 중복됩니다.
ConnectionHealthLogger.logMySQLConnection()이 이미ApplicationReadyEvent시점에 MySQL 연결을 확인하고 로그를 남기고 있습니다. 이 클래스는 추가로VERSION()정보를 제공하지만, 시작 시 두 번 MySQL 연결을 확인하게 됩니다. 하나로 통합하거나, 중복이 의도적인지 확인해 주세요.apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (2)
328-332:registerUser헬퍼 메서드가 두 개의 Nested 클래스에 중복되어 있습니다.
GetMe와ChangePassword내부에 동일한registerUser메서드가 있습니다. 외부 클래스(UserV1ApiE2ETest)로 추출하면 중복을 제거하고 유지보수성을 높일 수 있습니다.또한, 이 헬퍼가 등록 결과를 검증하지 않으므로 등록 실패 시 후속 테스트가 잘못된 이유로 실패할 수 있습니다. 등록 성공 여부를 assert하는 것을 권장합니다.
♻️ 리팩터링 제안
외부 클래스 레벨로 헬퍼를 추출하고 결과를 검증합니다:
+ private void registerUser(String loginId, String password, String name, String email, String birthDate) { + var request = new UserV1Dto.RegisterRequest(loginId, password, name, email, birthDate); + ResponseEntity<ApiResponse<UserV1Dto.UserResponse>> response = + testRestTemplate.exchange(ENDPOINT_REGISTER, HttpMethod.POST, new HttpEntity<>(request), + new ParameterizedTypeReference<ApiResponse<UserV1Dto.UserResponse>>() {}); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + `@DisplayName`("GET /api/v1/users/me - 내 정보 조회") `@Nested` class GetMe { // ... - private void registerUser(String loginId, String password, String name, String email, String birthDate) { - var request = new UserV1Dto.RegisterRequest(loginId, password, name, email, birthDate); - testRestTemplate.exchange(ENDPOINT_REGISTER, HttpMethod.POST, new HttpEntity<>(request), - new ParameterizedTypeReference<ApiResponse<UserV1Dto.UserResponse>>() {}); - } }
ChangePassword내부의registerUser도 동일하게 제거합니다.Also applies to: 454-458
62-65:ParameterizedTypeReference반복 선언을 간소화하는 것을 고려해 보세요.거의 모든 테스트 메서드에서
ParameterizedTypeReference<ApiResponse<...>>를 인라인으로 반복 선언하고 있습니다. 자주 사용되는 타입 참조를 클래스 레벨 상수로 추출하면 가독성이 개선됩니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
53-62:changePassword에서authenticate결과를 활용하면 DB 조회를 줄일 수 있습니다.현재
authenticate(loginId, loginPw)로 인증 후 결과(User)를 버리고,changePassword(loginId, ...)에서 같은 사용자를 다시 조회합니다. 인증된User객체를changePassword에 전달하면 불필요한 DB 조회를 줄일 수 있습니다.다만,
UserService.changePassword의 독립적 사용성을 유지하려는 설계 의도가 있다면 현재 구조도 수용 가능합니다.
| - 회원가입 (`POST /api/v1/members/register`) | ||
| - 내 정보 조회 (`GET /api/v1/members/me`) | ||
| - 비밀번호 수정 (`PATCH /api/v1/members/me/password`) |
There was a problem hiding this comment.
문서의 엔드포인트 경로가 실제 구현과 불일치합니다.
README에서는 /api/v1/members/... 경로를 사용하지만, PR 목표와 실제 구현에서는 /api/v1/users/... 경로를 사용합니다. Claude AI가 이 문서를 참고할 때 혼동이 발생할 수 있습니다.
📝 수정 제안
-- 회원가입 (`POST /api/v1/members/register`)
-- 내 정보 조회 (`GET /api/v1/members/me`)
-- 비밀번호 수정 (`PATCH /api/v1/members/me/password`)
+- 회원가입 (`POST /api/v1/users/register`)
+- 내 정보 조회 (`GET /api/v1/users/me`)
+- 비밀번호 수정 (`PATCH /api/v1/users/me/password`)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - 회원가입 (`POST /api/v1/members/register`) | |
| - 내 정보 조회 (`GET /api/v1/members/me`) | |
| - 비밀번호 수정 (`PATCH /api/v1/members/me/password`) | |
| - 회원가입 (`POST /api/v1/users/register`) | |
| - 내 정보 조회 (`GET /api/v1/users/me`) | |
| - 비밀번호 수정 (`PATCH /api/v1/users/me/password`) |
🤖 Prompt for AI Agents
In @.claude/README.md around lines 25 - 27, README endpoints use
/api/v1/members/... but the implementation and PR use /api/v1/users/..., causing
mismatch; update the listed endpoints (회원가입, 내 정보 조회, 비밀번호 수정) to use
/api/v1/users/register, /api/v1/users/me, and /api/v1/users/me/password (or the
exact route names used by the UserController /users handlers) so the
documentation matches the actual routes referenced in the codebase.
|
|
||
| private static final int MIN_LENGTH = 8; | ||
| private static final int MAX_LENGTH = 16; | ||
| private static final String ALLOWED_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~]+$"; |
There was a problem hiding this comment.
비밀번호 문자 조합 필수 요건이 누락되었습니다.
ALLOWED_PATTERN은 허용 문자 범위만 검증하고, 영문 대소문자 + 숫자 + 특수문자가 모두 포함되어야 하는 조합 규칙을 검증하지 않습니다. CLAUDE.md 도메인 분석에 명시된 "비밀번호: 8~16자, 영문 대소문자+숫자+특수문자 모두 포함" 규칙에 따르면, "abcdefgh"와 같은 비밀번호가 현재 검증을 통과합니다.
🐛 제안: 문자 조합 검증 추가
+ private static final String UPPERCASE_PATTERN = ".*[A-Z].*";
+ private static final String LOWERCASE_PATTERN = ".*[a-z].*";
+ private static final String DIGIT_PATTERN = ".*[0-9].*";
+ private static final String SPECIAL_CHAR_PATTERN = ".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~].*";
+
private void validateAllowedCharacters(String password) {
if (!password.matches(ALLOWED_PATTERN)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 영문 대소문자, 숫자, 특수문자만 허용됩니다.");
}
+ if (!password.matches(UPPERCASE_PATTERN) || !password.matches(LOWERCASE_PATTERN)
+ || !password.matches(DIGIT_PATTERN) || !password.matches(SPECIAL_CHAR_PATTERN)) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 영문 대소문자, 숫자, 특수문자를 모두 포함해야 합니다.");
+ }
}Also applies to: 26-30
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java`
at line 12, PasswordPolicyValidator's ALLOWED_PATTERN only restricts allowed
characters but doesn't enforce the required composition or length; update
PasswordPolicyValidator to enforce 8-16 length and that the password contains at
least one lowercase, one uppercase, one digit and one special character (in
addition to the allowed-character check). Implement this by replacing or
augmenting ALLOWED_PATTERN with a single regex that enforces all rules (or keep
ALLOWED_PATTERN for characters and add separate checks using patterns like
(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~])^.{8,16}$)
inside the PasswordPolicyValidator validation method so passwords like
"abcdefgh" will fail. Ensure the change is applied wherever
PasswordPolicyValidator performs validation (the ALLOWED_PATTERN and the class's
validation method).
| public void validatePasswordChange(String oldPassword, String newPassword) { | ||
| if (oldPassword.equals(newPassword)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "새 비밀번호는 기존 비밀번호와 달라야 합니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'validatePasswordChange' --type=javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 608
🏁 Script executed:
# Find UserService and examine changePassword method
fd -t f "UserService.java" --type=java | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 256
🏁 Script executed:
# Find UserService.java file
fd "UserService.java"
# Then search for changePassword method
rg -n "changePassword" --type=java -A 10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3554
🏁 Script executed:
# Get complete UserService.changePassword() method
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java | sed -n '51,67p'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 971
validatePasswordChange 메서드는 사용되지 않는 데드 코드입니다.
검색 결과, validatePasswordChange()는 테스트 코드(PasswordPolicyValidatorTest)에서만 호출되며 프로덕션 코드에서는 사용되지 않습니다. 더불어 UserService.changePassword()의 59번 줄에서 동일한 검증 로직(currentPassword.equals(newPassword))이 직접 구현되어 있어 중복됩니다. 이 메서드는 제거하거나, UserService에서 이를 호출하도록 통합해야 합니다.
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java`
around lines 39 - 43, The validatePasswordChange method in
PasswordPolicyValidator is dead/duplicated; replace the inline duplicate in
UserService.changePassword (the currentPassword.equals(newPassword) check) with
a call to PasswordPolicyValidator.validatePasswordChange(oldPassword,
newPassword) so production code uses the validator, and update
PasswordPolicyValidatorTest to reflect the validator being exercised;
alternatively, if you prefer removal, delete validatePasswordChange from
PasswordPolicyValidator and remove its use in tests and any references so only
the single check in UserService.changePassword remains—ensure tests are updated
accordingly.
| if (userRepository.existsByLoginId(loginId)) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다."); | ||
| } | ||
|
|
||
| String encodedPassword = passwordEncoder.encode(rawPassword); | ||
| User user = new User(loginId, encodedPassword, name, email, birthDate); | ||
|
|
||
| return userRepository.save(user); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find User entity file and examine unique constraint setup
fd -t f "User.java" --path "*/domain/user/*" -x cat {} \;Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 318
🏁 Script executed:
#!/bin/bash
# Search for unique constraint annotations on loginId in entity files
rg -n "@UniqueConstraint|@Column.*unique|loginId" --type=java -g "*/domain/user/User.java" -B 2 -A 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 233
🏁 Script executed:
#!/bin/bash
# Search for SQL migration/schema files
fd -t f "\.(sql|ddl)" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
#!/bin/bash
# Check if DataIntegrityViolationException is caught anywhere in the codebase
rg -n "DataIntegrityViolationException" --type=javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
#!/bin/bash
# Find User entity file
find . -name "User.java" -type f | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 152
🏁 Script executed:
#!/bin/bash
# Search for loginId field definition and unique constraints
rg -n "loginId|login_id" -A 3 -B 1 | grep -A 3 -B 1 "unique\|@Column\|@Entity" | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 743
🏁 Script executed:
#!/bin/bash
# Search for existing exception handling patterns in service classes
rg -n "catch.*Exception|DataIntegrityViolationException" --type=java -B 2 -A 2 | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3155
🏁 Script executed:
#!/bin/bash
# Check UserRepository interface for any custom exception handling
fd "UserRepository" --type f -x cat {} \;Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1072
🏁 Script executed:
#!/bin/bash
# Get the full UserService.java file
cat ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2748
🏁 Script executed:
#!/bin/bash
# Search for ApiControllerAdvice or global exception handlers
find . -name "*ControllerAdvice*" -o -name "*ExceptionHandler*" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 170
🏁 Script executed:
#!/bin/bash
# Check if DataIntegrityViolationException is handled anywhere as a global handler
rg -n "DataIntegrityViolationException\|ExceptionHandler\|ControllerAdvice" --type=javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
#!/bin/bash
# Examine the ApiControllerAdvice exception handler
cat ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 6762
🏁 Script executed:
#!/bin/bash
# Search for any existing DataIntegrityViolationException handling in the entire codebase
rg -n "DataIntegrityViolationException" --type=java -A 3 -B 3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
#!/bin/bash
# Check if there's any try-catch pattern in service classes for save operations
rg -n "userRepository\.save|\.save\(" --type=java -B 2 -A 2 | head -60Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2506
🏁 Script executed:
#!/bin/bash
# Check if there are any tests for concurrent user registration
find . -name "*UserService*Test*" -type f -exec cat {} \;Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3229
existsByLoginId 확인과 save 사이의 TOCTOU 경합 조건에 대한 방어 코드 추가 권장
동시 요청 시 두 스레드가 모두 existsByLoginId 확인을 통과한 후 동시에 save를 실행하면, DB의 unique 제약 조건으로 인해 DataIntegrityViolationException이 발생합니다. 현재 ApiControllerAdvice의 일반 예외 핸들러는 이를 INTERNAL_ERROR(500)로 처리하므로, 적절한 CONFLICT(409) 상태코드를 반환하지 못합니다.
아래와 같이 DataIntegrityViolationException을 catch하여 CoreException(ErrorType.CONFLICT)로 변환하는 방어 코드 추가를 권장합니다.
🛡️ 방어 코드 제안
`@Transactional`
public User register(String loginId, String rawPassword, String name, String email, String birthDate) {
loginIdValidator.validate(loginId);
emailValidator.validate(email);
birthDateValidator.validate(birthDate);
passwordPolicyValidator.validate(rawPassword, birthDate);
if (userRepository.existsByLoginId(loginId)) {
throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.");
}
String encodedPassword = passwordEncoder.encode(rawPassword);
User user = new User(loginId, encodedPassword, name, email, birthDate);
- return userRepository.save(user);
+ try {
+ return userRepository.save(user);
+ } catch (org.springframework.dao.DataIntegrityViolationException e) {
+ throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.");
+ }
}🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java`
around lines 40 - 47, Wrap the save call in UserService#create (or the method
that currently calls userRepository.existsByLoginId(...) and
userRepository.save(...)) with a try/catch that catches
DataIntegrityViolationException and rethrows a CoreException(ErrorType.CONFLICT,
"이미 존재하는 로그인 ID입니다."); keep the initial existsByLoginId check for fast-fail but
handle the TOCTOU race by converting DataIntegrityViolationException from
userRepository.save(user) into the same conflict CoreException so
ApiControllerAdvice returns 409 instead of 500.
| @ExceptionHandler | ||
| public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) { | ||
| String message = e.getBindingResult().getFieldErrors().stream() | ||
| .map(error -> String.format("'%s' %s", error.getField(), error.getDefaultMessage())) | ||
| .collect(Collectors.joining(", ")); | ||
| return failureResponse(ErrorType.BAD_REQUEST, message); | ||
| } |
There was a problem hiding this comment.
MethodArgumentNotValidException 핸들러가 프로젝트 원칙과 충돌합니다.
이 프로젝트는 DTO에 Bean Validation 어노테이션(@Valid, @NotBlank, @Email 등)을 사용하지 않고, 모든 검증을 도메인 레이어(CoreException)를 통해 수행합니다. 따라서 MethodArgumentNotValidException이 발생할 상황이 없으며, 이 핸들러는 데드 코드가 됩니다.
또한 이 핸들러의 존재는 향후 개발자가 Bean Validation 어노테이션을 도입하도록 유도할 수 있어, 통합된 에러 처리 패턴을 약화시킬 수 있습니다.
Based on learnings: "In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format. Do not introduce MethodArgumentNotValidException handlers or Bean Validation handling, as that would create inconsistent error handling patterns."
🗑️ 핸들러 제거 제안
- `@ExceptionHandler`
- public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) {
- String message = e.getBindingResult().getFieldErrors().stream()
- .map(error -> String.format("'%s' %s", error.getField(), error.getDefaultMessage()))
- .collect(Collectors.joining(", "));
- return failureResponse(ErrorType.BAD_REQUEST, message);
- }
-📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @ExceptionHandler | |
| public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) { | |
| String message = e.getBindingResult().getFieldErrors().stream() | |
| .map(error -> String.format("'%s' %s", error.getField(), error.getDefaultMessage())) | |
| .collect(Collectors.joining(", ")); | |
| return failureResponse(ErrorType.BAD_REQUEST, message); | |
| } |
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`
around lines 51 - 57, Remove the MethodArgumentNotValidException handler from
ApiControllerAdvice: delete the
handleValidationException(MethodArgumentNotValidException e) method (and any
now-unused imports for MethodArgumentNotValidException or Collectors if only
used there), ensuring all validation errors are instead routed through the
existing CoreException-based flow; after removal, run compilation/tests to
confirm no references remain to MethodArgumentNotValidException or the deleted
method.
| private void logRedisConnection() { | ||
| try { | ||
| redisConnectionFactory.getConnection().ping(); | ||
| log.info("✅ Redis Connection SUCCESS"); | ||
| } catch (Exception e) { | ||
| log.error("❌ Redis Connection FAILED", e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Redis 연결이 닫히지 않아 리소스 누수가 발생합니다.
redisConnectionFactory.getConnection()이 반환하는 RedisConnection은 Closeable을 구현하고 있어 사용 후 반드시 닫아야 합니다. 현재 코드에서는 ping() 호출 후 연결이 닫히지 않습니다.
🐛 리소스 누수 수정 제안
private void logRedisConnection() {
- try {
- redisConnectionFactory.getConnection().ping();
- log.info("✅ Redis Connection SUCCESS");
+ try (var connection = redisConnectionFactory.getConnection()) {
+ connection.ping();
+ log.info("✅ Redis Connection SUCCESS");
} catch (Exception e) {
log.error("❌ Redis Connection FAILED", e);
}
}🤖 Prompt for AI Agents
In `@loopers-docker/src/main/java/com/loopers/docker/ConnectionHealthLogger.java`
around lines 42 - 49, The logRedisConnection method leaks RedisConnection
because redisConnectionFactory.getConnection() returns a Closeable
RedisConnection that isn't closed; change logRedisConnection to obtain the
connection in a try-with-resources (or explicitly close in finally) around the
ping() call so the RedisConnection is always closed, preserving existing success
and error logging in the same try/catch structure and referencing
redisConnectionFactory.getConnection() and logRedisConnection.
| try { | ||
| // Redis에 명령을 날려서 container 로그에 기록 남김 | ||
| String ping = redisTemplate.getConnectionFactory().getConnection().ping(); | ||
| log.info("✅ Redis Connection SUCCESS: {}", ping); | ||
|
|
||
| // 테스트 데이터 쓰기/읽기 | ||
| String key = "spring-boot:connection-test"; | ||
| String value = "Connected at " + System.currentTimeMillis(); | ||
| redisTemplate.opsForValue().set(key, value); | ||
| String result = redisTemplate.opsForValue().get(key); | ||
| log.info(" └─ Redis Test Write/Read: {}", result); | ||
|
|
||
| // 정리 | ||
| redisTemplate.delete(key); | ||
| } catch (Exception e) { | ||
| log.error("❌ Redis Connection FAILED", e); | ||
| } |
There was a problem hiding this comment.
Redis 연결 리소스 누수 및 getConnectionFactory() null 가능성이 있습니다.
getConnection()반환값이 닫히지 않아 리소스가 누수됩니다 (ConnectionHealthLogger와 동일한 문제).getConnectionFactory()는@Nullable이며null을 반환할 수 있어 NPE 위험이 있습니다.
🐛 수정 제안
public void logConnectionStatus() {
try {
- // Redis에 명령을 날려서 container 로그에 기록 남김
- String ping = redisTemplate.getConnectionFactory().getConnection().ping();
- log.info("✅ Redis Connection SUCCESS: {}", ping);
+ var connectionFactory = redisTemplate.getConnectionFactory();
+ if (connectionFactory == null) {
+ log.error("❌ Redis Connection FAILED: ConnectionFactory is null");
+ return;
+ }
+ try (var connection = connectionFactory.getConnection()) {
+ String ping = connection.ping();
+ log.info("✅ Redis Connection SUCCESS: {}", ping);
+ }
// 테스트 데이터 쓰기/읽기🤖 Prompt for AI Agents
In `@loopers-docker/src/main/java/com/loopers/docker/RedisConnectionLogger.java`
around lines 23 - 39, The RedisConnectionLogger code can leak connections and
may NPE when getConnectionFactory() is null; modify the block using
redisTemplate so you first null-check redisTemplate.getConnectionFactory() and
bail with a log if null, and when calling getConnection() (or ping()) obtain the
Connection and ensure it is closed (use try-with-resources or explicit close)
instead of relying on implicit closure; keep the existing test write/read using
redisTemplate.opsForValue().set/get and delete, but ensure the connection used
for ping is closed to prevent resource leaks (refer to RedisConnectionLogger,
redisTemplate, getConnectionFactory(), getConnection(), ping()).
| pool-name: mysql-main-pool | ||
| maximum-pool-size: 40 | ||
| minimum-idle: 30 | ||
| connection-timeout: 3000 # 커넥션 획득 대기시간(ms) ( default: 3000 = 3sec ) | ||
| validation-timeout: 5000 # 커넥션 유효성 검사시간(ms) ( default: 5000 = 5sec ) | ||
| keepalive-time: 0 # 커넥션 최대 생존시간(ms) ( default: 0 ) | ||
| max-lifetime: 1800000 # 커넥션 최대 생존시간(ms) ( default: 1800000 = 30min ) | ||
| leak-detection-threshold: 0 # 커넥션 누수 감지 (주어진 ms 내에 반환 안 하면 로그 경고) ( default: 0 = 비활성화 ) | ||
| initialization-fail-timeout: 1 # DB 연결 실패 시 즉시 예외 발생 ( default: -1 = 무한대기 ) | ||
| data-source-properties: | ||
| rewriteBatchedStatements: true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the jpa.yaml file and examine its content
find . -name "jpa.yaml" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 129
🏁 Script executed:
# Search for HikariCP properties in YAML and properties files to check for misconfiguration
rg -n 'maximum-pool-size|minimum-idle|connection-timeout|pool-name' --type yaml --type propertiesRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 119
🏁 Script executed:
# Read the jpa.yaml file to examine the actual configuration
cat ./loopers-docker/src/main/resources/jpa.yamlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1101
🏁 Script executed:
# Search for HikariCP properties in YAML files only
rg -n 'maximum-pool-size|minimum-idle|connection-timeout|pool-name|hikari' --type yamlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1263
🏁 Script executed:
# Check the modules/jpa/src/main/resources/jpa.yml file to see if it has hikari properties properly configured
cat modules/jpa/src/main/resources/jpa.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2371
🏁 Script executed:
# Also check the apps files to see their structure
head -20 apps/commerce-streamer/src/main/resources/application.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 600
🏁 Script executed:
# Verify Spring Boot's HikariCP property mapping by checking if there are any Spring Boot configuration files
# that might show how properties are being mapped
find . -name "*Config.java" -o -name "*Configuration.java" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 449
🏁 Script executed:
# Search for any application.yml or application.properties in the loopers-docker module
# to see if there's a pattern of how datasource is configured in that module
find ./loopers-docker -name "application*.yml" -o -name "application*.properties"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Check if loopers-docker has a pom.xml or build.gradle to understand its nature
ls -la ./loopers-docker/ | grep -E "pom.xml|build.gradle|src"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 201
🏁 Script executed:
# Check the DataSourceConfig.java to understand how datasource is configured in modules/jpa
cat ./modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 897
🏁 Script executed:
# Check the loopers-docker build.gradle to see what dependencies it has
cat ./loopers-docker/build.gradleRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 977
🏁 Script executed:
# Check the src structure of loopers-docker
find ./loopers-docker/src -type f | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 541
🏁 Script executed:
# Check the application.yaml to see how jpa.yaml is imported/included
cat ./loopers-docker/src/main/resources/application.yamlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 230
🏁 Script executed:
# Also check if there's a main application class
cat ./loopers-docker/src/main/java/com/loopers/docker/LoopersDockerApplication.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 409
HikariCP 속성이 잘못된 경로에 위치하여 적용되지 않습니다.
pool-name, maximum-pool-size, minimum-idle, connection-timeout 등의 HikariCP 전용 속성들이 spring.datasource 하위에 직접 배치되어 있습니다. Spring Boot에서 이 속성들은 spring.datasource.hikari.* 경로에 있어야 HikariCP에 매핑됩니다. 현재 설정에서는 이 속성들이 무시되어 HikariCP 기본값(최대 풀 크기: 10, 최소 유휴: 10 등)이 사용됩니다.
🔧 수정 제안
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/loopers
username: application
password: application
- pool-name: mysql-main-pool
- maximum-pool-size: 40
- minimum-idle: 30
- connection-timeout: 3000
- validation-timeout: 5000
- keepalive-time: 0
- max-lifetime: 1800000
- leak-detection-threshold: 0
- initialization-fail-timeout: 1
- data-source-properties:
- rewriteBatchedStatements: true
+ hikari:
+ pool-name: mysql-main-pool
+ maximum-pool-size: 40
+ minimum-idle: 30
+ connection-timeout: 3000
+ validation-timeout: 5000
+ keepalive-time: 0
+ max-lifetime: 1800000
+ leak-detection-threshold: 0
+ initialization-fail-timeout: 1
+ data-source-properties:
+ rewriteBatchedStatements: true🤖 Prompt for AI Agents
In `@loopers-docker/src/main/resources/jpa.yaml` around lines 18 - 28, Move the
HikariCP-specific properties so they are nested under spring.datasource.hikari
(currently they are directly under spring.datasource), e.g. relocate pool-name,
maximum-pool-size, minimum-idle, connection-timeout, validation-timeout,
keepalive-time, max-lifetime, leak-detection-threshold,
initialization-fail-timeout and the
data-source-properties.rewriteBatchedStatements into spring.datasource.hikari.*
so Hikari maps them correctly; keep the same keys (pool-name, maximum-pool-size,
etc.) but place them under the hikari block (and use
spring.datasource.hikari.data-source-properties.rewriteBatchedStatements for the
rewriteBatchedStatements setting).
| datasource: | ||
| redis: | ||
| database: 0 | ||
| master: | ||
| host: localhost | ||
| port: 6379 |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Spring Boot 3 spring.data.redis configuration properties documentation
💡 Result:
For Spring Boot 3, Redis connection properties are documented under the spring.data.redis.* prefix:
- Reference docs (Redis section): explains how Boot auto-configures Lettuce/Jedis and shows basic
spring.data.redis.*examples (host/port/database/username/password, etc.). [1] - “Common Application Properties” (full property list): searchable table of all
spring.data.redis.*properties (cluster, lettuce/jedis pool, timeouts, etc.). [2] - The reference docs also cover using
spring.data.redis.urland Redis SSL properties likespring.data.redis.ssl.enabled/spring.data.redis.ssl.bundle. [3]
Links (official docs):
https://docs.spring.io/spring-boot/reference/data/nosql.html
https://docs.spring.vmware.com/spring-boot/docs/3.0.15.2/reference/html/application-properties.html
https://docs.spring.io/spring-boot/docs/3.2.9/reference/html/data.html
spring.datasource.redis는 Spring Boot Redis 자동 구성에서 인식되지 않는 경로입니다.
Spring Boot의 Redis 자동 구성은 spring.data.redis.* 경로를 사용합니다. 현재 spring.datasource.redis.* 하위에 정의된 database, master.host, master.port 속성은 무시되며, Spring Boot 기본값(localhost:6379)으로만 동작하게 됩니다. 이는 환경별 설정 변경 시 의도한 설정이 적용되지 않는 문제를 유발합니다.
올바른 Spring Data Redis 설정 경로로 수정
spring:
data:
redis:
repositories:
enabled: false
- datasource:
- redis:
- database: 0
- master:
- host: localhost
- port: 6379
+ database: 0
+ host: localhost
+ port: 6379🤖 Prompt for AI Agents
In `@loopers-docker/src/main/resources/redis.yaml` around lines 6 - 11, The YAML
uses the wrong property path: change the root key from spring.datasource.redis
to spring.data.redis and move the connection properties out of the master nested
object so Spring Boot reads them; i.e., ensure properties named database, host
and port exist directly under spring.data.redis (not
spring.datasource.redis.master.host/port) so Spring Data Redis picks up
database, host and port values.
| main: | ||
| driver-class-name: com.mysql.cj.jdbc.Driver | ||
| jdbc-url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT} | ||
| jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306} |
There was a problem hiding this comment.
기본 JDBC URL에 데이터베이스 이름이 누락되었습니다.
jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}에는 데이터베이스 이름(예: /loopers)이 포함되어 있지 않습니다. 이 기본값을 사용하는 프로필에서는 연결 실패가 발생합니다.
제안
- jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}
+ jdbc-url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:loopers}🤖 Prompt for AI Agents
In `@modules/jpa/src/main/resources/jpa.yml` at line 18, The default JDBC URL in
jpa.yml (property name "jdbc-url") is missing the database name; update the
jdbc-url default value from
jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306} to include the database
path (for example
jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/loopers) so profiles
that rely on the default can connect successfully.
📌 Summary
배경: TDD 기반으로 회원 도메인의 핵심 기능(회원가입, 내 정보 조회, 비밀번호 변경)을 단계적으로 구현할 필요가 있었으며, 인증·예외 처리·상태 변경 흐름을 실제 API 관점에서 검증해야 했음
목표: 회원가입, 인증 기반 내 정보 조회, 비밀번호 변경 API를 구현하고 주요 시나리오를 테스트로 검증
결과: E2E 테스트 흐름을 기준으로 회원 도메인 핵심 기능을 구현하고, 성공/실패 시나리오를 모두 검증함
🧭 Context & Decision
문제 정의
선택지와 결정
🏗️ Design Overview
변경 범위
주요 컴포넌트 책임
UserV1ControllerUserServiceUserPasswordPolicyValidator🔁 Flow Diagram
sequenceDiagram autonumber participant Client participant API as UserV1Controller participant Service as UserService participant Policy as PasswordPolicyValidator participant DB as UserRepository Client->>API: POST /api/v1/users/register API->>Service: register(request) Service->>Policy: validate(password) Policy-->>Service: ok Service->>DB: check duplicate DB-->>Service: result alt duplicate Service-->>API: CONFLICT API-->>Client: 409 else success Service->>Service: encode password Service->>DB: save user DB-->>Service: saved Service-->>API: success API-->>Client: 200 endsequenceDiagram autonumber participant Client participant API as UserV1Controller participant Service as UserService participant DB as UserRepository Client->>API: GET /api/v1/users/me API->>Service: authenticate(loginId, loginPw) Service->>DB: find user DB-->>Service: user alt unauthorized Service-->>API: UNAUTHORIZED API-->>Client: 401 else authorized Service-->>API: user info API->>API: mask name API-->>Client: 200 endsequenceDiagram autonumber participant Client participant API as UserV1Controller participant Service as UserService participant Policy as PasswordPolicyValidator participant DB as UserRepository Client->>API: PATCH /api/v1/users/me/password API->>Service: authenticate(loginId, loginPw) alt unauthorized Service-->>API: UNAUTHORIZED API-->>Client: 401 else authorized API->>Service: changePassword(currentPw, newPw) Service->>DB: find user DB-->>Service: user alt currentPw mismatch Service-->>API: UNAUTHORIZED API-->>Client: 401 else valid Service->>Policy: validate(newPw) Policy-->>Service: ok Service->>Service: encode & update Service-->>API: success API-->>Client: 200 end end✅ Checklist
Summary by CodeRabbit
릴리스 노트
새 기능
문서