[volume-1] 회원가입/내 정보 조회/비밀번호 수정 기능 작성#27
[volume-1] 회원가입/내 정보 조회/비밀번호 수정 기능 작성#27SukheeChoi wants to merge 9 commits intoLoopers-dev-lab:SukheeChoifrom
Conversation
- MemberModel 엔티티 및 MemberRepository 인터페이스 추가 - MemberService: 로그인 ID 중복 검증, 비밀번호 규칙 검증, 암호화 저장 - MemberV1Controller: POST /api/v1/members API - PasswordEncoderConfig: BCrypt 설정 - ApiControllerAdvice: @Valid 검증 예외 핸들러 추가 - 단위 테스트 6개 추가 (Service 4개, Controller 2개) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- AuthMember 어노테이션 및 AuthMemberResolver 추가 - 헤더 기반 인증 (X-Loopers-LoginId, X-Loopers-LoginPw) - GET /api/v1/members/me API 추가 - 이름 마스킹 로직 (홍길동 → 홍길*) - ErrorType.UNAUTHORIZED 추가 - 단위 테스트 5개 추가 (Controller 3개, DTO 2개) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberModel에 changePassword() 메서드 추가 - MemberService에 비밀번호 변경 로직 구현 - 현재 비밀번호 검증 - 동일 비밀번호 사용 불가 - 비밀번호 규칙 검증 (8~16자, 영문/숫자/특수문자) - 생년월일 포함 불가 - MemberV1Controller에 PATCH /me/password 엔드포인트 추가 - CLAUDE.md를 .gitignore에 추가 (git 추적 제외) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberModel → Member 엔티티 리네이밍 (DDD 네이밍) - Value Object 도입: LoginId, Email, BirthDate, Password (@embeddable, 자가 검증) - Gender enum 추가 및 회원가입 시 성별 필수 처리 - PasswordPolicy 도메인 정책 분리 (순수 함수) - Service 얇은 조율 계층으로 리팩토링 (검증 로직 VO/Policy로 이동) - 포인트 조회 API 신규 구현 (GET /api/v1/points) - AuthMemberResolver 보안 에러 메시지 통일 - 단위 테스트 (LoginIdTest, EmailTest, BirthDateTest 등) - 통합 테스트 (MemberServiceIntegrationTest, @SpyBean) - E2E 테스트 (MemberV1ApiE2ETest, PointV1ApiE2ETest) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
DDD 리팩토링 + Value Object 도입 + 포인트 조회 구현
기능 요구사항에 해당하지 않는 Gender enum, 포인트 조회 API를 제거한다. Member 엔티티에서 gender/point 필드를 제거하고 관련 테스트를 정리한다. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
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: 9
🤖 Fix all issues with AI agents
In
`@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`:
- Around line 24-39: Wrap the call to memberRepository.save(member) inside a
try-catch to handle DB-level unique constraint failures: catch
DataIntegrityViolationException (and optionally its cause/constraint-violation
types) thrown by memberRepository.save in the register method and rethrow new
CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다."); keep the existing
LoginId/unique column as the source of truth, preserve the `@Transactional`
behavior, and ensure other exceptions continue to propagate normally.
- Around line 45-59: changePassword에서 전달받은 detached Member를 그대로 수정하면 영속성 컨텍스트에
속하지 않아 UPDATE가 발생하지 않으므로, 새 트랜잭션 경계(메서드의 `@Transactional`) 안에서 변경 전용으로 엔티티를 재조회하여
영속 상태로 만든 뒤 비밀번호를 변경하세요; 예를 들어 MemberService의 changePassword 내부에서
member.getId()로 memberRepository.findById(...) 또는 entityManager.find(...)로 영속
엔티티를 조회하고 해당 엔티티에 대해 member.changePassword(newPassword) (Password.create 사용) 를
호출하여 변경을 적용하고, 또한 MemberServiceIntegrationTest에 changePassword 통합 테스트를 추가해 실제
DB에 비밀번호 변경이 반영되는지를 검증하세요.
In
`@apps/commerce-api/src/main/java/com/loopers/domain/member/policy/PasswordPolicy.java`:
- Around line 16-43: validate() currently passes a potentially null birthDate to
extractBirthDateStrings() causing NPE; add a null-check and throw
CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수입니다.") either at the start of
validate() or inside extractBirthDateStrings(LocalDate) to return a consistent
BAD_REQUEST error instead of 500, and update/ add a unit test to assert that
calling validate(null) produces the CoreException; reference methods: validate,
extractBirthDateStrings, and the CoreException(ErrorType.BAD_REQUEST) usage when
implementing the change.
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/vo/BirthDate.java`:
- Around line 32-41: The BirthDate.from(String) method currently throws a
CoreException on DateTimeParseException without preserving the original cause;
update BirthDate.from to pass the caught DateTimeParseException as the cause
when constructing CoreException (add or use a CoreException constructor that
accepts a cause) so the original exception is retained for logging/monitoring,
and ensure the thrown CoreException keeps the user-facing message separate from
any log message. Also add/adjust BirthDateTest to supply an invalid date string
and assert that the thrown CoreException.getCause() is a DateTimeParseException.
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/vo/Email.java`:
- Around line 14-28: Summary: The Email value lacks a length check so strings
that match PATTERN but exceed the DB column length (Column(name="email", length
= 100)) can cause a DB error. Fix: inside the Email(String value) constructor
(the Email class and its value field), add a pre-check that value is not null
and value.length() <= 100 (before or alongside the PATTERN check) and throw new
CoreException(ErrorType.BAD_REQUEST, "올바른 이메일 형식이 아닙니다.") (or a clearer
length-specific message) when it exceeds 100; update any tests and add a unit
test that verifies an email of length 101+ is rejected with CoreException.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`:
- Around line 50-55: Remove the dedicated MethodArgumentNotValidException
handler (handleBadRequest) from ApiControllerAdvice and instead ensure
validation failures are translated into CoreException before reaching the
controller advice; specifically, delete or disable the handleBadRequest method
and update the request validation entrypoint (e.g., controller pre-processing or
service adapter) to catch MethodArgumentNotValidException and throw a
CoreException with the same errorCode/message so ApiControllerAdvice continues
to produce a single ApiResponse format and status; add/extend integration tests
to assert that a validation failure results in the same ApiResponse structure,
HTTP status, and error code as an equivalent CoreException-driven error path.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java`:
- Around line 48-51: ChangePasswordRequest currently uses Bean Validation
annotations which must be removed like in SignUpRequest; edit the
ChangePasswordRequest record to delete the `@NotBlank` annotations from
currentPassword and newPassword, and ensure any input validation for password
changes is handled via the central CoreException flow (ApiControllerAdvice)
rather than JSR-303 annotations so controllers/services throw CoreException on
invalid input.
- Around line 10-16: Remove the Bean Validation annotations from the DTO record
SignUpRequest (remove all `@NotBlank` on loginId, password, name, birthDate,
email) and delegate validation to the domain value objects and entity
constructors (e.g., LoginId, Password, BirthDate, Email) so they throw
CoreException on null/blank/invalid input; update the mapping code that converts
SignUpRequest into domain objects to construct those VOs (thereby triggering
domain validation) and ensure no other DTOs in this API layer use
javax.validation annotations.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceIntegrationTest.java`:
- Around line 86-87: The test DisplayName in MemberServiceIntegrationTest for
the test annotated with `@Test` and `@DisplayName`("해당 ID의 회원이 존재하지 않을 경우 null이
반환된다") is incorrect because the method actually returns Optional.empty(); update
the `@DisplayName` to accurately reflect the behavior (e.g., "해당 ID의 회원이 존재하지 않을
경우 Optional.empty()가 반환된다") so the test documentation matches the actual return
value from the MemberService lookup method.
🧹 Nitpick comments (12)
apps/commerce-api/src/main/java/com/loopers/support/auth/PasswordEncoderConfig.java (1)
8-14: BCrypt 강도 하드코딩으로 환경별 보안·성능 튜닝이 어렵다다운영 관점: 기본 강도는 트래픽 상황에 따라 과도한 CPU 사용이나 보안 수준 저하로 이어질 수 있는데, 코드 하드코딩이면 환경별 조정이 어려워 장애 대응이 늦어진다다.
수정안: 강도를 프로퍼티로 외부화하고 기본값을 지정해 환경별로 조정 가능하게 해야 한다다.
추가 테스트: 설정된 강도가 해시 문자열(예:$2a$12$)에 반영되는지 확인하는 구성 테스트를 추가하는 것이 좋다다.수정안 예시다
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; `@Configuration` public class PasswordEncoderConfig { `@Bean` - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + public PasswordEncoder passwordEncoder( + `@Value`("${security.password.bcrypt-strength:10}") int strength) { + return new BCryptPasswordEncoder(strength); } }apps/commerce-api/src/main/java/com/loopers/domain/member/vo/LoginId.java (1)
14-26: DB 컬럼 길이를 정책과 일치시키는 편이 안전하다
운영에서 DB 직접 적재나 마이그레이션으로 도메인 검증을 우회하면 10자 초과 ID가 저장될 수 있고, 이후 조회/인증에서 불일치로 실패할 수 있다.
수정안: 정책이 10자라면@Column(length = 10)으로 맞추거나, 20자가 요구사항이면 정규식을{1,20}으로 완화해 정책과 스키마를 일치시키라.
추가 테스트: LoginIdTest에 10자 성공/11자 실패 경계값을 명시적으로 추가해 회귀를 방지하라.수정 제안 diff
- `@Column`(name = "login_id", nullable = false, unique = true, length = 20) + `@Column`(name = "login_id", nullable = false, unique = true, length = 10)apps/commerce-api/src/test/java/com/loopers/domain/member/vo/LoginIdTest.java (1)
12-51: 경계값(최소 1자/최대 10자) 성공 케이스가 빠져 있다
운영 중 정책 변경이나 리팩토링 시 경계값이 깨져도 테스트가 잡지 못해 잘못된 ID가 유입될 위험이 있다.
수정안: 길이 10의 정상 케이스를 추가해 최대 경계값을 고정하라.
추가 테스트: 길이 1의 정상 케이스도 함께 추가해 최소 경계값을 검증하라.수정 제안 diff
+ `@DisplayName`("최대 길이(10자)는 생성에 성공한다") + `@Test` + void create_withMaxLength_succeeds() { + LoginId loginId = new LoginId("abcdefghij"); + assertThat(loginId.value()).isEqualTo("abcdefghij"); + } + + `@DisplayName`("최소 길이(1자)는 생성에 성공한다") + `@Test` + void create_withMinLength_succeeds() { + LoginId loginId = new LoginId("a"); + assertThat(loginId.value()).isEqualTo("a"); + }As per coding guidelines
**/*Test*.java: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다.apps/commerce-api/src/main/java/com/loopers/domain/member/vo/Password.java (1)
3-35: 도메인 VO가 Spring Security에 직접 의존해 계층 분리가 흐려진다
운영에서 배치/외부 채널 재사용 시 도메인 모듈이 Spring Security에 묶여 배포·테스트가 어려워지고, 인코딩 정책 변경이 도메인 변경으로 확산될 수 있다.
수정안: 인코딩과 PasswordEncoder 의존은 서비스 계층으로 이동하고, Password는 인코딩된 값만 받는 팩토리/생성자로 단순화하라.
추가 테스트: MemberService 통합 테스트에서 인코더가 적용된 해시가 저장되고 평문 매칭이 정상 동작하는지 확인하라.수정 제안 diff
-import com.loopers.domain.member.policy.PasswordPolicy; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.time.LocalDate; import java.util.Objects; @@ - public static Password create(String plain, LocalDate birthDate, - PasswordEncoder encoder) { - PasswordPolicy.validate(plain, birthDate); - return new Password(encoder.encode(plain)); - } + public static Password fromEncoded(String encoded) { + return new Password(encoded); + }As per coding guidelines
**/domain/**/*.java: 도메인 규칙과 인프라 관심사가 섞이면 분리하도록 제안한다.apps/commerce-api/src/main/java/com/loopers/support/auth/AuthMemberResolver.java (1)
49-58: 매 요청마다 DB 조회 + BCrypt 비교가 발생하여 고부하 시 병목이 될 수 있다.BCrypt는 의도적으로 느린 해싱 알고리즘이므로 트래픽 증가 시 인증 레이어가 성능 병목이 될 수 있다. 현재 구현은 학습/MVP 용도로 적합하나, 운영 환경에서는 다음을 고려해야 한다:
- 세션 또는 토큰 기반 인증으로 전환하여 매 요청 인증 비용 절감
- 인증 결과 캐싱 (단, 보안 trade-off 고려 필요)
또한, 반환된
Member엔티티가 트랜잭션 컨텍스트 밖에서 Lazy 로딩된 연관 엔티티에 접근 시LazyInitializationException이 발생할 수 있다. 현재Member에 연관 엔티티가 없다면 문제없으나, 향후 확장 시 주의가 필요하다.apps/commerce-api/src/test/java/com/loopers/domain/member/MemberTest.java (1)
14-68: Member 생성 시 다른 VO의 유효성 검사 실패 케이스가 누락되었다.현재 테스트는
name필드의 null/blank 케이스만 검증한다. 다음 경계값 테스트 추가를 권장한다:
- 잘못된 형식의
LoginId(예: 특수문자 포함, 길이 초과)- 잘못된 형식의
@누락)- 잘못된 형식의
BirthDate(예: 미래 날짜, 잘못된 포맷)이러한 테스트는 도메인 객체의 불변식이 실제로 보장되는지 확인하는 데 필수적이다.
추가 테스트 케이스 예시
`@DisplayName`("잘못된 이메일 형식이면 생성에 실패한다") `@Test` void create_withInvalidEmail_throwsException() { assertThatThrownBy(() -> new Member( new LoginId("user1"), new Password("encodedPw"), "홍길동", BirthDate.from("1990-01-15"), new Email("invalid-email") // @ 누락 )).isInstanceOf(CoreException.class); }apps/commerce-api/src/test/java/com/loopers/interfaces/api/member/MemberV1ApiE2ETest.java (2)
82-132: 잘못된 비밀번호로 인증 시도 시 401 반환 테스트가 누락되었다.존재하지 않는 ID 테스트는 있으나, 존재하는 ID + 잘못된 비밀번호 조합 테스트가 없다. 이 케이스는
AuthMemberResolver의 비밀번호 검증 분기를 커버하는 데 필수적이다.추가 테스트 케이스
`@DisplayName`("잘못된 비밀번호로 조회할 경우, 401 Unauthorized") `@Test` void getMyInfo_withWrongPassword_returnsUnauthorized() { // arrange signUp(validSignUpBody()); HttpHeaders headers = new HttpHeaders(); headers.set("X-Loopers-LoginId", "user1"); headers.set("X-Loopers-LoginPw", "WrongPassword!"); // act ResponseEntity<ApiResponse<Object>> response = testRestTemplate.exchange( "/api/v1/members/me", HttpMethod.GET, new HttpEntity<>(headers), new ParameterizedTypeReference<>() {} ); // assert assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); }
61-80: 회원가입 실패 케이스(중복 ID, 유효성 검사 실패) E2E 테스트가 누락되었다.성공 케이스만 검증되어 있다. 다음 실패 케이스 추가를 권장한다:
- 중복 ID로 가입 시도 → 409 Conflict
- 필수 필드 누락 시 → 400 Bad Request
이러한 테스트는
MemberService의 중복 검사 로직과 VO 유효성 검사가 API 레이어까지 올바르게 전파되는지 확인하는 데 필요하다.apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceIntegrationTest.java (1)
67-95:changePassword메서드에 대한 통합 테스트가 누락되었다.
MemberService.changePassword는 비밀번호 변경이라는 중요한 비즈니스 로직을 담당하지만, 이 통합 테스트 클래스에서 해당 메서드에 대한 테스트가 없다. 운영 관점에서 비밀번호 변경 실패 시 사용자 계정 접근 불가 문제가 발생할 수 있으므로, 다음 시나리오에 대한 테스트가 필요하다:
- 정상 케이스: 현재 비밀번호 일치 시 변경 성공
- 실패 케이스: 현재 비밀번호 불일치 시 예외 발생
- 실패 케이스: 새 비밀번호가 현재 비밀번호와 동일할 때 예외 발생
- 실패 케이스: 새 비밀번호가 정책 위반 시 예외 발생
apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java (3)
27-27:@Valid사용이 프로젝트 가이드라인과 충돌한다.Retrieved learnings에 따르면, 이 프로젝트에서는 DTO에 Bean Validation 어노테이션을 사용하지 않고 도메인 레이어(엔티티 생성자, PasswordPolicy 등)에서 검증을 수행해야 한다.
@Valid가 트리거하는MethodArgumentNotValidException은CoreException→ApiControllerAdvice를 통한 통합 오류 처리 패턴과 불일치하여 응답 포맷이 일관되지 않을 수 있다.현재 도메인 레이어(LoginId, Password, Email, BirthDate VO 생성자)에서 이미 검증이 수행되므로
@Valid는 중복이다.Based on learnings: "Do not use Bean Validation annotations on DTOs in this project. Move validation logic into the domain layer."
♻️ `@Valid` 제거 및 DTO 어노테이션 정리 제안
Controller에서
@Valid제거:-public ApiResponse<MemberV1Dto.SignUpResponse> signUp(`@Valid` `@RequestBody` MemberV1Dto.SignUpRequest request) { +public ApiResponse<MemberV1Dto.SignUpResponse> signUp(`@RequestBody` MemberV1Dto.SignUpRequest request) {MemberV1Dto에서
@NotBlank제거:public record SignUpRequest( - `@NotBlank` String loginId, - `@NotBlank` String password, - `@NotBlank` String name, - `@NotBlank` String birthDate, - `@NotBlank` String email + String loginId, + String password, + String name, + String birthDate, + String email ) {}
54-54:changePassword에서도 동일하게@Valid사용이 프로젝트 가이드라인과 충돌한다.위와 동일한 이유로
@Valid제거를 권장한다.Based on learnings: "enforce unified error handling by routing errors through CoreException to ApiControllerAdvice."
♻️ `@Valid` 제거 제안
public ApiResponse<Object> changePassword( `@AuthMember` Member member, - `@Valid` `@RequestBody` MemberV1Dto.ChangePasswordRequest request + `@RequestBody` MemberV1Dto.ChangePasswordRequest request ) {
46-48:getMyInfo에서 반환하는 Member 엔티티의 상태에 주의가 필요하다.
@AuthMember로 주입된Member는AuthMemberResolver에서 조회된 detached 엔티티일 수 있다. 현재는 읽기 전용으로 사용하므로 문제가 없으나, 향후 Lazy Loading 관계가 추가될 경우LazyInitializationException이 발생할 수 있다.현재 구현은 문제없으나, 문서화 또는 주석을 통해 이 제약 사항을 명시하는 것이 좋다.
| @Transactional | ||
| public Member register(String loginId, String plainPassword, String name, | ||
| String birthDate, String email) { | ||
| LoginId loginIdVo = new LoginId(loginId); | ||
|
|
||
| if (memberRepository.existsByLoginId(loginIdVo)) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다."); | ||
| } | ||
|
|
||
| BirthDate birthDateVo = BirthDate.from(birthDate); | ||
| Password password = Password.create(plainPassword, birthDateVo.value(), passwordEncoder); | ||
| Email emailVo = new Email(email); | ||
|
|
||
| Member member = new Member(loginIdVo, password, name, birthDateVo, emailVo); | ||
| return memberRepository.save(member); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find apps/commerce-api -type f -name "*.java" | xargs rg -l "class LoginId" 2>/dev/null | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 236
🏁 Script executed:
find apps/commerce-api -type f -name "LoginId.java"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 158
🏁 Script executed:
fd -e sql -o -e yaml | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 201
🏁 Script executed:
rg -n "class Member " apps/commerce-api/src/main/java/com/loopers/domain/member/ -A 30 | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3077
🏁 Script executed:
rg -i "unique\|constraint" --type sql apps/commerce-api/ 2>/dev/null || echo "Checking migration files..."Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 112
🏁 Script executed:
find apps/commerce-api -type f \( -name "*migration*" -o -name "*schema*" -o -name "*.sql" \) | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/vo/LoginId.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1458
🏁 Script executed:
find apps/commerce-api -type d -name "*migration*" -o -name "*flyway*" -o -name "*liquibase*"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -type f -name "*.sql"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -path "*/resources/db/*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg -l "MemberRepository" apps/commerce-api/src/main/java/ | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 414
🏁 Script executed:
rg -A 20 "interface MemberRepository" apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 653
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1176
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2724
🏁 Script executed:
rg -B 5 -A 20 "public Member register" apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1037
🏁 Script executed:
find apps/commerce-api -type f -name "*MemberServiceTest*" -o -name "*MemberTest*"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 158
🏁 Script executed:
rg -n "DataIntegrityViolationException" apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg -n "ExceptionHandler\|ControllerAdvice" apps/commerce-api/src/main/java/ | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -path "*/exception/*" -o -path "*/error/*" -type f | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 315
🏁 Script executed:
cat -n apps/commerce-api/src/test/java/com/loopers/domain/member/MemberTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2716
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 741
🏁 Script executed:
rg -B 5 -A 10 "catch.*DataIntegrity\|catch.*Constraint" apps/commerce-api/src/ 2>/dev/null | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -type f -name "*Controller*" | xargs rg -l "register" | head -3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 174
🏁 Script executed:
rg -B 3 -A 15 "RequestMapping.*register\|PostMapping.*register\|register.*Member" apps/commerce-api/src/main/java/ | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2623
🏁 Script executed:
rg -r "register" apps/commerce-api/src/main/java/com/loopers/interfaces/ -A 10 -B 2 | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -type f -name "*ExceptionHandler*" -o -name "*GlobalException*" -o -name "*ErrorHandler*"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
register 메서드의 동시성 이슈에 따른 예외 처리 개선이 필요하다.
existsByLoginId 체크와 save 사이의 race condition으로 인해 두 동시 요청이 동일한 loginId로 가입 시도 시 둘 다 체크를 통과한 후 하나의 요청은 DB 레벨의 유니크 제약에서 DataIntegrityViolationException을 발생시킨다. 현재 코드에서 이 예외가 처리되지 않아 사용자에게 일관되지 않은 오류 응답이 전달된다.
LoginId VO의 login_id 컬럼에 유니크 제약(@Column(name = "login_id", nullable = false, unique = true, length = 20))이 설정되어 있어 데이터 무결성은 보장되지만, memberRepository.save(member) 호출 시 발생하는 DataIntegrityViolationException을 catch하여 CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다.")로 변환해야 한다. 이를 통해 사용자에게 일관된 오류 메시지를 제공할 수 있다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`
around lines 24 - 39, Wrap the call to memberRepository.save(member) inside a
try-catch to handle DB-level unique constraint failures: catch
DataIntegrityViolationException (and optionally its cause/constraint-violation
types) thrown by memberRepository.save in the register method and rethrow new
CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다."); keep the existing
LoginId/unique column as the source of truth, preserve the `@Transactional`
behavior, and ensure other exceptions continue to propagate normally.
| @Transactional | ||
| public void changePassword(Member member, String currentPlain, String newPlain) { | ||
| if (!member.getPassword().matches(currentPlain, passwordEncoder)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다."); | ||
| } | ||
|
|
||
| if (member.getPassword().matches(newPlain, passwordEncoder)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "새 비밀번호는 현재 비밀번호와 달라야 합니다."); | ||
| } | ||
|
|
||
| Password newPassword = Password.create( | ||
| newPlain, member.getBirthDate().value(), passwordEncoder); | ||
| member.changePassword(newPassword); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 1. AuthMemberResolver의 구현 및 트랜잭션 설정 확인
find . -name "*AuthMemberResolver*" -type f
# 2. MemberService의 전체 구조 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
# 3. MemberRepository의 메서드 확인
find . -name "*MemberRepository*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2983
🏁 Script executed:
# AuthMemberResolver 찾기 (패턴 확대)
find . -name "*.java" -type f | xargs grep -l "AuthMemberResolver" | head -5
# MemberRepositoryImpl 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java
# changePassword를 호출하는 컨트롤러 찾기
rg -n "changePassword" --type java -A3 -B3
# Member 엔티티 확인 (detached 상태 판단을 위해)
find . -name "Member.java" -path "*/domain/member/*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 6479
🏁 Script executed:
# AuthMemberResolver 파일 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/AuthMemberResolver.java
# Member 엔티티 전체 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
# WebMvcConfig 확인 (AuthMemberResolver 등록)
cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/WebMvcConfig.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5606
🏁 Script executed:
# 통합 테스트 또는 엔드투엔드 테스트 존재 여부 확인
find . -name "*MemberServiceTest*" -o -name "*MemberIntegrationTest*" | head -5
# MemberServiceTest 또는 유사 테스트 파일 검색
find . -path "*/test/*" -name "*Member*Service*.java" -type f
# 마이그레이션 또는 다른 서비스에서 detached entity 처리 패턴 확인
rg -n "findByLoginId" --type java -A3 -B1 | grep -A3 -B1 "Transactional"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 610
🏁 Script executed:
# MemberServiceIntegrationTest 전체 내용 확인
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceIntegrationTest.java
# changePassword 테스트 케이스가 있는지 확인
rg -n "changePassword" ./apps/commerce-api/src/test/java/com/loopers/domain/member/ -A10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5119
🏁 Script executed:
# BaseEntity 확인 (ID 생성 방식, 버전 관리 등)
find . -name "BaseEntity.java" -type f | xargs cat -n
# MemberJpaRepository 확인 (Spring Data JPA 메서드 확인)
find . -name "MemberJpaRepository.java" -type f | xargs cat -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2728
changePassword 메서드는 detached entity로 인해 비밀번호 변경이 DB에 반영되지 않을 수 있다.
AuthMemberResolver에서 조회된 Member 엔티티는 Spring Data JPA의 기본 트랜잭션(readOnly)이 종료되면서 detached 상태가 된다. changePassword 메서드의 새로운 @Transactional 경계에서 이 detached entity를 수신할 때, 해당 객체는 현재 persistence context에 attach되지 않는다. 따라서 member.changePassword(newPassword) 호출 후 flush 시점에도 dirty checking이 감지하지 못해 UPDATE 쿼리가 발생하지 않는다.
비밀번호 변경이 DB에 반영되지 않으면 사용자가 새 비밀번호로 로그인할 수 없는 심각한 장애로 이어진다.
해결 방안: 현재 트랜잭션 컨텍스트에서 엔티티를 재조회하여 영속 상태를 보장한다.
수정 코드
`@Transactional`
public void changePassword(Member member, String currentPlain, String newPlain) {
+ // 현재 트랜잭션 컨텍스트에서 엔티티를 다시 조회하여 영속 상태 보장
+ Member managedMember = memberRepository.findByLoginId(member.getLoginId())
+ .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다."));
+
- if (!member.getPassword().matches(currentPlain, passwordEncoder)) {
+ if (!managedMember.getPassword().matches(currentPlain, passwordEncoder)) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
}
- if (member.getPassword().matches(newPlain, passwordEncoder)) {
+ if (managedMember.getPassword().matches(newPlain, passwordEncoder)) {
throw new CoreException(ErrorType.BAD_REQUEST,
"새 비밀번호는 현재 비밀번호와 달라야 합니다.");
}
Password newPassword = Password.create(
- newPlain, member.getBirthDate().value(), passwordEncoder);
- member.changePassword(newPassword);
+ newPlain, managedMember.getBirthDate().value(), passwordEncoder);
+ managedMember.changePassword(newPassword);
}추가로 MemberServiceIntegrationTest에 changePassword 통합 테스트를 추가하여 비밀번호 변경이 실제 DB에 반영되는지 검증해야 한다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`
around lines 45 - 59, changePassword에서 전달받은 detached Member를 그대로 수정하면 영속성 컨텍스트에
속하지 않아 UPDATE가 발생하지 않으므로, 새 트랜잭션 경계(메서드의 `@Transactional`) 안에서 변경 전용으로 엔티티를 재조회하여
영속 상태로 만든 뒤 비밀번호를 변경하세요; 예를 들어 MemberService의 changePassword 내부에서
member.getId()로 memberRepository.findById(...) 또는 entityManager.find(...)로 영속
엔티티를 조회하고 해당 엔티티에 대해 member.changePassword(newPassword) (Password.create 사용) 를
호출하여 변경을 적용하고, 또한 MemberServiceIntegrationTest에 changePassword 통합 테스트를 추가해 실제
DB에 비밀번호 변경이 반영되는지를 검증하세요.
| public static void validate(String plain, LocalDate birthDate) { | ||
| validateFormat(plain); | ||
| validateNotContainsSubstrings(plain, | ||
| extractBirthDateStrings(birthDate), | ||
| "비밀번호에 생년월일을 포함할 수 없습니다."); | ||
| } | ||
|
|
||
| public static void validateFormat(String plain) { | ||
| if (plain == null || !FORMAT_PATTERN.matcher(plain).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "비밀번호는 8~16자의 영문, 숫자, 특수문자만 허용됩니다."); | ||
| } | ||
| } | ||
|
|
||
| public static void validateNotContainsSubstrings( | ||
| String plain, List<String> forbidden, String errorMessage) { | ||
| for (String s : forbidden) { | ||
| if (plain.contains(s)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, errorMessage); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static List<String> extractBirthDateStrings(LocalDate birthDate) { | ||
| return List.of( | ||
| birthDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")), | ||
| birthDate.format(DateTimeFormatter.ofPattern("yyMMdd")) | ||
| ); |
There was a problem hiding this comment.
birthDate null 처리 누락으로 500이 발생할 수 있다다
운영 관점: birthDate가 null이면 format()에서 NPE가 발생해 500으로 떨어지며, 에러 응답 일관성이 깨져 운영 모니터링과 클라이언트 처리에 혼선을 준다다.
수정안: validate 또는 extractBirthDateStrings에서 null을 선제 검증해 CoreException(BAD_REQUEST)으로 반환해야 한다다.
추가 테스트: birthDate가 null인 경우 CoreException이 발생하는지 단위 테스트를 추가하는 것이 좋다다.
수정안 예시다
public class PasswordPolicy {
public static void validate(String plain, LocalDate birthDate) {
+ if (birthDate == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ "생년월일은 필수입니다.");
+ }
validateFormat(plain);
validateNotContainsSubstrings(plain,
extractBirthDateStrings(birthDate),
"비밀번호에 생년월일을 포함할 수 없습니다.");
}🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/domain/member/policy/PasswordPolicy.java`
around lines 16 - 43, validate() currently passes a potentially null birthDate
to extractBirthDateStrings() causing NPE; add a null-check and throw
CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수입니다.") either at the start of
validate() or inside extractBirthDateStrings(LocalDate) to return a consistent
BAD_REQUEST error instead of 500, and update/ add a unit test to assert that
calling validate(null) produces the CoreException; reference methods: validate,
extractBirthDateStrings, and the CoreException(ErrorType.BAD_REQUEST) usage when
implementing the change.
| public static BirthDate from(String dateString) { | ||
| if (dateString == null || dateString.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "생년월일은 필수입니다."); | ||
| } | ||
| try { | ||
| return new BirthDate(LocalDate.parse(dateString, FORMATTER)); | ||
| } catch (DateTimeParseException e) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "생년월일은 yyyy-MM-dd 형식이어야 합니다."); |
There was a problem hiding this comment.
파싱 예외의 cause가 누락되어 운영 추적성이 떨어진다
현재는 원인 예외가 손실되어 로그/모니터링에서 잘못된 입력 패턴을 빠르게 파악하기 어렵다.
수정안: CoreException 생성 시 cause를 보존해 원인 예외를 전달하라(필요 시 CoreException에 cause 생성자를 추가하라).
추가 테스트: BirthDateTest에서 잘못된 형식 입력 시 getCause()가 DateTimeParseException인지 검증하라.
수정 제안 diff
- } catch (DateTimeParseException e) {
- throw new CoreException(ErrorType.BAD_REQUEST,
- "생년월일은 yyyy-MM-dd 형식이어야 합니다.");
+ } catch (DateTimeParseException e) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ "생년월일은 yyyy-MM-dd 형식이어야 합니다.", e);
}As per coding guidelines **/*.java: 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/vo/BirthDate.java`
around lines 32 - 41, The BirthDate.from(String) method currently throws a
CoreException on DateTimeParseException without preserving the original cause;
update BirthDate.from to pass the caught DateTimeParseException as the cause
when constructing CoreException (add or use a CoreException constructor that
accepts a cause) so the original exception is retained for logging/monitoring,
and ensure the thrown CoreException keeps the user-facing message separate from
any log message. Also add/adjust BirthDateTest to supply an invalid date string
and assert that the thrown CoreException.getCause() is a DateTimeParseException.
| private static final Pattern PATTERN = | ||
| Pattern.compile("^[\\w-.]+@[\\w-]+(\\.[a-z]{2,})+$"); | ||
|
|
||
| @Column(name = "email", nullable = false, length = 100) | ||
| private String value; | ||
|
|
||
| protected Email() {} | ||
|
|
||
| public Email(String value) { | ||
| if (value == null || !PATTERN.matcher(value).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "올바른 이메일 형식이 아닙니다."); | ||
| } | ||
| this.value = value; | ||
| } |
There was a problem hiding this comment.
email 길이 제한 누락으로 DB 저장 실패 가능성이 있다다
운영 관점: 컬럼 길이 100을 초과하는 값이 정규식만 통과하면 저장 시 DB 오류가 발생해 500으로 터질 수 있어 운영 장애로 이어질 수 있다다.
수정안: 생성 시 길이 상한을 검증해 BAD_REQUEST로 차단해야 한다다.
추가 테스트: 길이 101 이상의 이메일 입력이 CoreException으로 거부되는 케이스를 추가하는 것이 좋다다.
수정안 예시다
public class Email {
+ private static final int MAX_LENGTH = 100;
private static final Pattern PATTERN =
Pattern.compile("^[\\w-.]+@[\\w-]+(\\.[a-z]{2,})+$");
`@Column`(name = "email", nullable = false, length = 100)
private String value;
public Email(String value) {
- if (value == null || !PATTERN.matcher(value).matches()) {
+ if (value == null || value.length() > MAX_LENGTH
+ || !PATTERN.matcher(value).matches()) {
throw new CoreException(ErrorType.BAD_REQUEST,
"올바른 이메일 형식이 아닙니다.");
}
this.value = value;
}🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/vo/Email.java`
around lines 14 - 28, Summary: The Email value lacks a length check so strings
that match PATTERN but exceed the DB column length (Column(name="email", length
= 100)) can cause a DB error. Fix: inside the Email(String value) constructor
(the Email class and its value field), add a pre-check that value is not null
and value.length() <= 100 (before or alongside the PATTERN check) and throw new
CoreException(ErrorType.BAD_REQUEST, "올바른 이메일 형식이 아닙니다.") (or a clearer
length-specific message) when it exceeds 100; update any tests and add a unit
test that verifies an email of length 101+ is rejected with CoreException.
| @ExceptionHandler | ||
| public ResponseEntity<ApiResponse<?>> handleBadRequest(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.
Bean Validation 예외 핸들러 추가로 오류 응답 포맷이 분기된다
운영 관점에서 오류 응답 경로가 CoreException과 분리되어 포맷/코드 일관성이 깨지고, 모니터링·알림 규칙도 이중화될 수 있다. 수정안으로 해당 핸들러를 제거하고, 검증 실패는 도메인에서 CoreException으로 변환되도록 상위 계층(요청 검증/서비스 진입)에서 처리하는 흐름으로 통일하는 것이 필요하다. 추가 테스트로 ① 검증 실패 시 CoreException 경로로 동일한 ApiResponse 포맷이 반환되는지 ② 기존 CoreException 처리와 동일한 상태 코드/에러 코드가 유지되는지 통합 테스트를 보강해 달라. Based on learnings, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format.
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`
around lines 50 - 55, Remove the dedicated MethodArgumentNotValidException
handler (handleBadRequest) from ApiControllerAdvice and instead ensure
validation failures are translated into CoreException before reaching the
controller advice; specifically, delete or disable the handleBadRequest method
and update the request validation entrypoint (e.g., controller pre-processing or
service adapter) to catch MethodArgumentNotValidException and throw a
CoreException with the same errorCode/message so ApiControllerAdvice continues
to produce a single ApiResponse format and status; add/extend integration tests
to assert that a validation failure results in the same ApiResponse structure,
HTTP status, and error code as an equivalent CoreException-driven error path.
| public record SignUpRequest( | ||
| @NotBlank String loginId, | ||
| @NotBlank String password, | ||
| @NotBlank String name, | ||
| @NotBlank String birthDate, | ||
| @NotBlank String email | ||
| ) {} |
There was a problem hiding this comment.
DTO에 Bean Validation 어노테이션(@NotBlank) 사용은 프로젝트 지침에 위배된다.
프로젝트 학습 내용에 따르면, 유효성 검사 로직은 도메인 레이어(엔티티 생성자, VO, PasswordPolicy 등)에 배치해야 한다. DTO에 Bean Validation을 사용하면:
MethodArgumentNotValidException이 발생하여CoreException기반의 통일된 에러 핸들링 패턴이 깨진다- 도메인 레이어와 API 레이어에 유효성 검사가 중복된다
도메인 VO들(LoginId, Email, BirthDate, Password)이 이미 생성 시점에 자가 검증을 수행하므로, DTO의 @NotBlank를 제거하고 도메인 레이어의 검증에 위임하는 것이 일관성 있다.
수정안
public record SignUpRequest(
- `@NotBlank` String loginId,
- `@NotBlank` String password,
- `@NotBlank` String name,
- `@NotBlank` String birthDate,
- `@NotBlank` String email
+ String loginId,
+ String password,
+ String name,
+ String birthDate,
+ String email
) {}도메인 레이어에서 null/blank 입력 시 CoreException을 던지도록 보장되어야 한다.
Based on learnings: "Do not use Bean Validation annotations (e.g., Valid, NotBlank, Email) on DTOs in this project. Move validation logic into the domain layer so validation is enforced regardless of entrypoint."
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java`
around lines 10 - 16, Remove the Bean Validation annotations from the DTO record
SignUpRequest (remove all `@NotBlank` on loginId, password, name, birthDate,
email) and delegate validation to the domain value objects and entity
constructors (e.g., LoginId, Password, BirthDate, Email) so they throw
CoreException on null/blank/invalid input; update the mapping code that converts
SignUpRequest into domain objects to construct those VOs (thereby triggering
domain validation) and ensure no other DTOs in this API layer use
javax.validation annotations.
| public record ChangePasswordRequest( | ||
| @NotBlank String currentPassword, | ||
| @NotBlank String newPassword | ||
| ) {} |
There was a problem hiding this comment.
ChangePasswordRequest에도 동일하게 @NotBlank 제거가 필요하다.
위 SignUpRequest와 동일한 이유로 Bean Validation 어노테이션을 제거해야 한다.
수정안
public record ChangePasswordRequest(
- `@NotBlank` String currentPassword,
- `@NotBlank` String newPassword
+ String currentPassword,
+ String newPassword
) {}Based on learnings: "In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice."
📝 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.
| public record ChangePasswordRequest( | |
| @NotBlank String currentPassword, | |
| @NotBlank String newPassword | |
| ) {} | |
| public record ChangePasswordRequest( | |
| String currentPassword, | |
| String newPassword | |
| ) {} |
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java`
around lines 48 - 51, ChangePasswordRequest currently uses Bean Validation
annotations which must be removed like in SignUpRequest; edit the
ChangePasswordRequest record to delete the `@NotBlank` annotations from
currentPassword and newPassword, and ensure any input validation for password
changes is handled via the central CoreException flow (ApiControllerAdvice)
rather than JSR-303 annotations so controllers/services throw CoreException on
invalid input.
| @DisplayName("해당 ID의 회원이 존재하지 않을 경우 null이 반환된다") | ||
| @Test |
There was a problem hiding this comment.
DisplayName이 실제 동작과 불일치한다.
"null이 반환된다"라고 명시되어 있으나, 실제로는 Optional.empty()가 반환된다. 테스트 문서화 정확성을 위해 수정이 필요하다.
📝 DisplayName 수정 제안
-@DisplayName("해당 ID의 회원이 존재하지 않을 경우 null이 반환된다")
+@DisplayName("해당 ID의 회원이 존재하지 않을 경우 빈 Optional이 반환된다")📝 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.
| @DisplayName("해당 ID의 회원이 존재하지 않을 경우 null이 반환된다") | |
| @Test | |
| `@DisplayName`("해당 ID의 회원이 존재하지 않을 경우 빈 Optional이 반환된다") | |
| `@Test` |
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceIntegrationTest.java`
around lines 86 - 87, The test DisplayName in MemberServiceIntegrationTest for
the test annotated with `@Test` and `@DisplayName`("해당 ID의 회원이 존재하지 않을 경우 null이
반환된다") is incorrect because the method actually returns Optional.empty(); update
the `@DisplayName` to accurately reflect the behavior (e.g., "해당 ID의 회원이 존재하지 않을
경우 Optional.empty()가 반환된다") so the test documentation matches the actual return
value from the MemberService lookup method.
📌 Summary
🧭 Context & Decision
검증 로직의 위치
→ A 선택. 검증 규칙이 VO에 캡슐화되므로 Service 컨텍스트 없이 도메인 규칙을 독립 테스트 가능. 테스트 용이성 확보.
트레이드오프: Java record가 VO에 적합하나,
@Embeddable은 기본 생성자 필수 + QueryDSL Q-class 비호환으로 class 채택. equals/hashCode 직접 작성 비용 발생인증 방식
요구사항이 커스텀 헤더(
X-Loopers-LoginId/Pw) 기반 인증HandlerMethodArgumentResolver+ 커스텀 헤더 — 가볍고 요구사항에 부합→ 요구사항인 B 선택. 단, 매 요청마다 DB 조회 + BCrypt 비교가 발생하므로 실서비스 적용 시 JWT토큰 기반 전환 필요
도메인 계층의 프레임워크 의존
Password.create()가 Spring Security의PasswordEncoder를 파라미터로 수신PasswordEncoder직접 사용 — 이미 인터페이스이므로 구현 교체 자유. 간접층 없이 간결→ A 선택. 실용성 우선
🏗️ Design Overview
변경 범위
주요 컴포넌트 책임
LoginId, Email, BirthDate, Password: 생성 시점 자가 검증 Value ObjectPasswordPolicy: 비밀번호 정책(형식, 생년월일 포함 금지)을 순수 함수로 제공Member: VO 조합으로 도메인 무결성을 보장하는 EntityMemberService: 유스케이스 조율(가입, 조회, 비밀번호 수정) + 트랜잭션 경계AuthMemberResolver: 커스텀 헤더 기반 인증 처리, Member 객체를 컨트롤러에 주입🔁 Flow Diagram
핵심 경로
회원가입 (시퀀스)
내 정보 조회 (시퀀스)
비밀번호 변경 (시퀀스)
예외 흐름
회원가입
sequenceDiagram autonumber participant Client participant Controller participant Service participant VO rect rgb(255, 240, 240) Note over VO: VO 검증 실패 Client->>Controller: POST /api/v1/members Controller->>Service: register(...) Service->>VO: new LoginId("한글아이디") VO-->>Service: CoreException (BAD_REQUEST) Service-->>Controller: 예외 전파 Controller-->>Client: 400 Bad Request end rect rgb(255, 240, 240) Note over Service: 중복 ID Client->>Controller: POST /api/v1/members Controller->>Service: register(...) Service->>Service: existsByLoginId → true Service-->>Controller: CoreException (CONFLICT) Controller-->>Client: 409 Conflict end내 정보 조회
비밀번호 변경 (플로우)
flowchart TD A{현재 비밀번호 일치?} -- No --> A1[400: 현재 비밀번호가 일치하지 않습니다] B{새 비밀번호 ≠ 현재 비밀번호?} -- No --> B1[400: 새 비밀번호는 현재 비밀번호와 달라야 합니다] C{형식 검증 통과?} -- No --> C1[400: 8~16자의 영문, 숫자, 특수문자만 허용] D{생년월일 포함 여부?} -- Yes --> D1[400: 비밀번호에 생년월일을 포함할 수 없습니다]