[volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현#30
[volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현#30APapeIsName wants to merge 17 commits intoLoopers-dev-lab:APapeIsNamefrom
Conversation
setting: gitignore 수 test: AI 를 활용한 TDD Red 코드 작성 feature: member 회원가입 도메인 기능 구현 test: 아이디 포맷 관련 테스트 작성 필요없는 mock 종속성 삭제 test: 아이디 test 작성 및 exception message 값 추가 fix: lombok @Getter 제거하고, message() 라는 메소드로 구현해 message 필드 캡슐화(추후 필드 형태가 바뀌더라도 그대로 message()로 불러올 수 있도록) fix: 가독성 향상을 위해 내부 이넘으로 변경 fix: 비밀번호 암호화 관련 메시지 추가 test: 비밀번호 RED 테스트 작성 feature: PasswordEncryptor 유틸성 클래스 구현 delete: password -> membertest 안에 통합 test: 이름, 이메일, 생년월일 테스트 코드 작성 및 메시지 추가
feature: 정책 인터페이스화 -> 구체화 객체들 작성 refactor: 정책 인터페이스 제거 후, 하나의 Policy 안에 내부 정적 클래스로 변경 refactor: MemberPolicy 생성 feature: MemberPolicy 구현 fix: MemberPolicy 및 MemberExceptionMessage 잘못된 부분 수정
test: 회원가입 서비스 로직 Red Test 작성 feature: MemberRepository 구현 fix: @ExtendWith(MockitoExtension.class) 없어서 오류 발생 -> 추가
test: 회원가입 성공 시 test 추가 - red feature: MemberService save 로직 구현 - green refactor: 회원가입 시 아이디 중복 불가 코드 수정 - builder 사용해 필요한 값만 받도록 수정 - refactor
test: 회원가입 통합테스트 - red fix: 회원가입 테스트 오류 해결 - Member 에 @table 추가 및 Service 에 @transactional 추가
test: 내 정보 조회 단위 Test 작성 test: 내 정보 조회 단위 Test 시 미존재 회원 조회 시 코드 수정 test: 내 정보 조회 단위 Test Member, MemberService 추가 작성 test: 내 정보 조회 단위 Test MemberService 누락된 부분 수정
feature: 내 정보 조회 기능 구현 test: 테스트 코드 내 비밀번호 암호화 추가 fix: 예외 메시지 수정
|
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 |
|
|
||
| //then | ||
| verify(memberRepository).save(memberCaptor.capture()); | ||
| assertThat(memberCaptor.getValue().getLoginId()).isEqualTo(request.loginId()); |
There was a problem hiding this comment.
현재는 위와 같이 getter 를 사용해서 request 의 Id 와 생성된 회원의 Id를 비교해서 테스트 성공 여부를 확인하고 있습니다. 이 코드는 캡슐화가 지켜지지 않아 변경 시에 테스트 코드를 수정해야 한다는 단점이 있다고 생각합니다.
위와 같이 테스트 코드 내부에서 getter를 사용하는 상황이 있다면 어떻게 테스트 코드를 작성하시는지 궁금합니다.
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Fix all issues with AI agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`:
- Around line 54-60: The current login flow in MemberService differentiates
missing accounts (memberRepository.findByLoginId -> CANNOT_LOGIN) from wrong
passwords (member.isSamePassword -> PASSWORD_INCORRECT), which leaks account
existence; change the password-check branch to throw the same CANNOT_LOGIN error
as the not-found branch (keep detailed cause in an internal log using the member
id or login attempt and exception text), ensure the throw uses
MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message(), and add/adjust an
integration test to assert that both non-existent user and incorrect password
produce identical external responses while internal logs capture the distinct
reasons.
- Around line 31-40: Replace the direct IllegalArgumentException throws in
MemberService.getMyInfo with CoreException-based errors that use the appropriate
MemberExceptionMessage (e.g., ExistsMember.CANNOT_LOGIN) so the API error schema
is consistent with ApiControllerAdvice; specifically, throw new
CoreException(...) (or the project’s CoreException factory) and pass the
original cause when wrapping (preserve cause) rather than throwing raw
IllegalArgumentException, separate the error instances for "not found" vs
"authentication failed" using the correct MemberExceptionMessage constants, and
add/update controller/E2E tests to assert the unified error payload (code,
message, field) is returned for duplicate/auth failures.
- Around line 19-28: 현재 MemberService.register에서
memberRepository.existsByLoginId(...) 조회와 memberRepository.save(...) 사이에 경쟁 조건이
있어 동시 가입 시 중복이 발생합니다; DB 스키마에 login_id에 UNIQUE 제약을 추가하고 MemberService.register에서
save 호출을 감싸 DataIntegrityViolationException을 잡아
MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS로 변환해 던지도록 처리하며
existsByLoginId는 UX용 조기 피드백으로만 유지하세요; 또한 Member 엔티티의 매핑과 마이그레이션을 통해 login_id에
유니크 제약을 추가하고, 통합 테스트를 작성해 여러 스레드/프로세스로 병렬 등록을 시도할 때 하나만 성공하고 나머지는 중복 예외로 실패하는지
검증하십시오.
In
`@apps/commerce-api/src/main/java/com/loopers/controller/MemberController.java`:
- Around line 21-50: MemberController's header parameters (getMyInfo and
updatePassword) currently rely on required=true which throws
MissingRequestHeaderException and bypasses ApiControllerAdvice's CoreException
handler; change the `@RequestHeader` annotations for "X-Loopers-LoginId" and
"X-Loopers-LoginPw" in MemberController.getMyInfo and
MemberController.updatePassword to required=false, then add explicit null/blank
checks that throw new CoreException(ErrorType.BAD_REQUEST, "<clear message>")
when missing; ensure these checks are applied to both loginId and password (and
currentPassword where applicable) so ApiControllerAdvice handles them and add
unit/E2E tests asserting BAD_REQUEST and the standard error body for missing
header and missing body cases.
- Around line 31-48: The controller currently reads raw passwords from the
custom header "X-Loopers-LoginPw" in MemberController.getMyInfo and
updatePassword; remove this practice by eliminating the
`@RequestHeader`("X-Loopers-LoginPw") parameters and instead accept credentials
via a standard mechanism (preferably Authorization header with Bearer/Basic
handled by your security layer or session/cookie authentication) and update
memberService.getMyInfo and memberService.updatePassword signatures to take the
authenticated principal or token-derived user identity and a request body for
new passwords (e.g., PasswordUpdateRequest); if you cannot migrate immediately,
add a Servlet filter/logging mask that strips or masks "X-Loopers-LoginPw" from
request/response logs and ensure all logging frameworks (Logback/SLF4J) are
configured to exclude or mask that header, then add security tests verifying no
sensitive header values appear in logs on success/failure.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`:
- Around line 116-122: handleIllegalArgumentException in ApiControllerAdvice
currently maps all IllegalArgumentException to 401 which misrepresents
validation vs auth failures; change error handling so IllegalArgumentException
is not blanket-mapped to 401: introduce or use specific exception types (e.g.,
AuthenticationException / PasswordIncorrectException for auth failures,
ValidationException for input format errors, DuplicateResourceException for
duplicate-id) or wrap into a CoreException with an error code, then add/modify
handlers in ApiControllerAdvice (methods handling CoreException,
AuthenticationException, ValidationException, DuplicateResourceException) to
return appropriate status codes (401 for auth, 400 for validation, 409 for
conflicts) and consistent ApiResponse payloads; update any throw sites to throw
the specific exceptions or CoreException and extend E2E tests to assert correct
HTTP status for duplicate-id, invalid-format, and password-incorrect cases.
In
`@apps/commerce-api/src/test/java/com/loopers/application/MemberServiceIntegrationTest.java`:
- Around line 101-109: 현재 테스트가 memberRepository.save(...)에
평문("correctPassword")을 저장해 해시 비교 로직을 검증하지 못하므로, MemberServiceIntegrationTest의
데이터 준비에서 저장 비밀번호를 PasswordEncryptor.encode("correctPassword")로 암호화해 실제 운영 저장 포맷과
일치시키세요; 또한 동일한 로그인Id로 해시 저장 조건에서 올바른 비밀번호로 인증이 성공하는 케이스(성공 테스트)와 잘못된 비밀번호로 인증이
실패하는 케이스(현재 실패 테스트)를 각각 분리해 검증하도록 테스트를 추가/정리하세요.
In
`@apps/commerce-api/src/test/java/com/loopers/application/MemberServiceTest.java`:
- Around line 100-122: The test 내_정보_조회_시_이름_마스킹 currently only asserts loginId;
update the assertions after calling memberService.getMyInfo(loginId, password)
to verify the name is masked by checking response.name() (for example:
assertThat(response.name()).startsWith("공") and
assertThat(response.name()).doesNotContain("명선") or
assertThat(response.name()).matches(expectedMaskRegex) so the test actually
validates the masking behavior for the Member built with name "공명선").
- Around line 124-142: The test uses a plain-text password in
Member.builder().password("correctPasswo") so
member.isSamePassword()/PasswordEncryptor.matches will fail; update the mock
setup so the stored Member has an encoded password (e.g., use the same
PasswordEncryptor.encode(...) used in production or mock
PasswordEncryptor.matches to return true for the expected pair) before stubbing
memberRepository.findByLoginId(loginId), and add a complementary success test
that verifies memberService.updatePassword(loginId, "correctPasswo",
"newPass123!") succeeds and that the repository/save method (or member state)
reflects the updated encoded password; reference Member.builder(),
memberRepository.findByLoginId(...), memberService.updatePassword(...),
Member.isSamePassword(), and PasswordEncryptor.matches().
In `@modules/jpa/build.gradle.kts`:
- Around line 21-27: Pin the test fixture dependency versions so they are
explicit and reproducible: update the
testFixturesImplementation/testFixturesRuntimeOnly entries for
"org.junit.jupiter:junit-jupiter-api", "org.junit.jupiter:junit-jupiter-params",
"org.junit.jupiter:junit-jupiter-engine" and "org.assertj:assertj-core" to
include explicit versions (or define those versions in gradle.properties and
reference them) so the current coordinates in modules/jpa/build.gradle.kts are
no longer versionless.
In `@modules/jpa/src/main/java/com/loopers/domain/member/Member.java`:
- Around line 25-26: Member 클래스의 loginId 필드에 DB 수준 유니크 제약이 빠져 TOCTOU로 동시 가입이
허용되므로 Member.loginId에 `@Column`(nullable = false, unique = true) 어노테이션을 추가하고 (클래스:
Member, 필드: loginId) 프로덕션 마이그레이션 스크립스키마에 실제 유니크 인덱스 생성 항목을 추가하여 DB가 중복을 차단하도록
하세요; 또한 서비스 레이어의 memberRepository.existsByLoginId() 체크만으로는 불충분하니 동시 등록을 시뮬레이트하는
통합 테스트를 추가해 동일한 loginId로 동시 저장 시 DataIntegrityViolationException이
발생하는지(assertThrows) 검증하도록 구현하세요.
- Around line 13-18: The Member entity currently exposes public constructors and
a public Lombok builder which bypass domain validation; change the Lombok
annotations on Member—@NoArgsConstructor, `@AllArgsConstructor`, and `@Builder`—to
use protected (or private where supported) access (e.g.,
`@NoArgsConstructor`(access = AccessLevel.PROTECTED), `@AllArgsConstructor`(access =
AccessLevel.PROTECTED), `@Builder`(access = AccessLevel.PRIVATE/PROTECTED) so
external code cannot call new Member(...) or Member.builder() directly, and
ensure the only public creation path is the existing register() factory method;
after that add an ArchUnit test that forbids direct constructor or builder calls
for com.loopers.domain.member.Member and asserts all Member instances are
created via Member.register().
In
`@modules/jpa/src/main/java/com/loopers/domain/member/policy/MemberPolicy.java`:
- Around line 27-48: The Password.validate method can NPE when birthDate is
null; add an explicit null check at the start of Password.validate(String
password, LocalDate birthDate) and throw a clear IllegalArgumentException using
the appropriate MemberExceptionMessage (e.g., create/choose a message like
MemberExceptionMessage.Password.INVALID_BIRTHDATE or reuse an existing one)
before any use of birthDate, or call/ensure BirthDate.validate(birthDate) first;
update unit tests to assert that calling Password.validate with birthDate ==
null throws the expected IllegalArgumentException with the correct message.
In `@modules/jpa/src/main/java/com/loopers/utils/PasswordEncryptor.java`:
- Around line 10-36: The current PasswordEncryptor (methods encode and matches)
must be replaced to use a slow KDF with per-password random salt and iterations
(e.g., PBKDF2WithHmacSHA256, iterations=65536, salt length>=16) instead of raw
SHA-256: implement a new encode that generates a SecureRandom salt, derives the
key with SecretKeyFactory/PBEKeySpec, and returns a versioned encoded string
containing version, base64(salt), and base64(hash); update matches to parse that
format, re-derive the hash with the stored salt/iterations and compare using
MessageDigest.isEqual for constant-time equality; ensure null/invalid-format
inputs return false safely; and add unit tests that verify (1) same password
yields different hashes due to salt, (2) matches correctly accepts/rejects, and
(3) invalid/null formats safely fail—also keep
Member.register()/updatePassword() compatibility by using the new encode/matches
and include a version prefix to allow future migration.
In
`@modules/jpa/src/testFixtures/java/com/loopers/testcontainers/domain/member/MemberTest.java`:
- Around line 106-114: The test method 아이디는_중복_가입할_수_없음 in MemberTest is empty
and should either be removed or disabled/implemented; update the MemberTest
class by either deleting the empty 아이디는_중복_가입할_수_없음() method or annotate it with
`@Disabled`("통합 테스트로 이동 필요") if you intend to move it to an integration test
suite, or implement the actual duplicate-ID assertion logic against the
service/repository used in other tests so the method performs real verification.
- Around line 374-388: The test stores a raw password into Member.password
causing a false-positive because Member.isSamePassword uses
PasswordEncryptor.matches; update the isSamePassword_Fail setup to encode the
saved password the same way as isSamePassword_Success (use the same
PasswordEncryptor.encrypt/encode call or helper used in the other test) before
building the Member instance so the comparison path mirrors real usage in
Member.isSamePassword and the test reliably asserts a mismatch when passed
"wrongPassword".
🧹 Nitpick comments (6)
modules/jpa/src/testFixtures/java/com/loopers/testcontainers/domain/member/MemberTest.java (2)
424-518: 중복된 테스트 코드 제거 필요
UpdatePasswordPolicy.PasswordFormatValidation내부 클래스의 테스트들이 외부PasswordFormatValidation클래스(Lines 164-259)의 테스트와 거의 동일하다. 차이점은Member.register()대신member.updatePassword()를 호출한다는 점뿐이다.중복 테스트는 유지보수 비용 증가 및 정책 변경 시 동기화 누락 위험을 초래한다.
수정안:
- 비밀번호 형식 검증이 동일 정책(
MemberPolicy.Password)을 사용한다면, 하나의 테스트 세트로 통합하거나- 파라미터화 테스트(
@ParameterizedTest)로 리팩토링하여register와updatePassword양쪽을 검증한다.
471-481: 미사용 변수userBirthDate제거 필요Lines 473, 487에서
userBirthDate변수를 선언했으나 실제로는 사용하지 않는다.throwIfWrongPasswordInput헬퍼 메서드(Lines 509-516) 내부에서LocalDate.of(2001, 2, 9)로 하드코딩되어 있다.이는 테스트 의도 파악을 어렵게 하고, 향후 생년월일을 변경해야 할 때 버그를 유발할 수 있다.
수정 제안
// 2-1 `@Test` public void 사용자_생년월일_YYYYMMDD가_비밀번호_포함_불가() throws Exception { //given - LocalDate userBirthDate = LocalDate.of(2001, 2, 9); String wrongPassword = "pwd20010209!"; //when //then throwIfWrongPasswordInput(wrongPassword) .hasMessage(MemberExceptionMessage.Password.PASSWORD_CONTAINS_BIRTHDATE.message()); }또는 헬퍼 메서드에서 birthDate를 파라미터로 받도록 수정한다.
Also applies to: 483-495
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1)
119-119: 로그에 스택 트레이스 누락다른 핸들러(
handle(CoreException e),handle(Throwable e))와 달리 스택 트레이스를 로깅하지 않는다. 운영 환경에서 예외 발생 원인 추적이 어려워질 수 있다.수정 제안
- log.warn("IllegalArgumentException : {}", e.getMessage()); + log.warn("IllegalArgumentException : {}", e.getMessage(), e);apps/commerce-api/src/main/java/com/loopers/application/service/dto/MyMemberInfoResponse.java (1)
5-10: 필드명 일관성 검토:birthdatevsbirthDate도메인 모델(
Member.birthDate)과 DTO(birthdate)의 필드명 케이스가 다르다. JSON 응답에서birthdate로 직렬화되며, 이후 다른 DTO에서birthDate를 사용할 경우 API 응답 규격 불일치가 발생할 수 있다.프로젝트 전반의 네이밍 컨벤션을 확인하고 통일하는 것을 권장한다.
apps/commerce-api/src/test/java/com/loopers/controller/MemberE2ETest.java (2)
74-78: 예상 상태 코드 불확실성 해소 필요주석에 "또는 400 에러 (설정한 예외 처리에 따름)"라고 명시되어 있어 테스트 의도가 불명확하다.
ApiControllerAdvice의IllegalArgumentException핸들러가 401을 반환하므로 현재는 테스트가 통과하지만, 핸들러 수정 시 테스트가 실패할 수 있다.E2E 테스트는 API 규약을 검증하는 계약 역할을 하므로, 예상 상태 코드를 명확히 정의하고 주석을 제거해야 한다.
수정 제안
// [5] 기존 비밀번호로 조회 시도 (Should Fail) mockMvc.perform(get("/api/members/me") .header("X-Loopers-LoginId", loginId) .header("X-Loopers-LoginPw", initialPw)) // Old Password - .andExpect(status().isUnauthorized()); // 또는 400 에러 (설정한 예외 처리에 따름) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("UNAUTHORIZED"));
31-79: 추가 테스트 케이스 권장현재 테스트는 정상 흐름(happy path)과 단일 실패 케이스만 포함한다. 운영 안정성을 위해 다음 시나리오 추가를 권장한다:
- 중복 회원가입 시도: 동일
loginId로 재등록 시 적절한 에러 응답 확인- 잘못된 비밀번호 형식으로 회원가입: 정책 위반 시 400 응답 확인
- 존재하지 않는 회원 조회: 404 또는 401 응답 확인
- 동일 비밀번호로 변경 시도: 정책 위반 에러 확인
각 시나리오별로 별도 테스트 메서드로 분리하면 실패 시 원인 파악이 용이하다.
| @Transactional | ||
| public void register(MemberRegisterRequest request) { | ||
| boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId()); | ||
|
|
||
| if (isLoginIdAlreadyExists) { | ||
| throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message()); | ||
| } | ||
|
|
||
| memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email())); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "MemberService.java" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 169
🏁 Script executed:
find . -type f -name "*.sql" -o -name "*.ddl" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -type f -path "**/*/Service.java" -o -path "**/*Service.java" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 250
🏁 Script executed:
fd -t f "MemberService" --extension javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 336
🏁 Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3628
🏁 Script executed:
find . -type f -name "Member.java" -o -name "*Member*.java" | grep -E "(entity|domain)" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 404
🏁 Script executed:
find . -path "**/resources/*" -name "*.sql" -o -name "*.ddl" -o -name "schema*" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2535
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/MemberExceptionMessage.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4192
🏁 Script executed:
find . -path "**/resources/*" -type f \( -name "*.sql" -o -name "*.xml" \) | xargs grep -l "login_id\|loginId" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 69
🏁 Script executed:
cat -n ./apps/commerce-api/src/test/java/com/loopers/application/MemberServiceIntegrationTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 8912
🏁 Script executed:
find . -path "**/resources/*" -type f | grep -E "\.(sql|xml|properties|yaml|yml)$" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 967
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/support/error/CoreException.java 2>/dev/null || echo "File not found"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 99
🏁 Script executed:
grep -r "unique.*login" --include="*.java" --include="*.sql" . 2>/dev/null | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -type f -name "*.sql" | xargs cat 2>/dev/null | head -100Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -type f -name "CoreException.java" -o -name "*Exception.java" | grep -E "support|error|exception" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 163
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | grep -A 5 -B 5 "@Column"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "DataIntegrityViolationException" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "@Column.*unique" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -path "*/schema*" -o -path "*/migration*" -o -path "*/flyway*" | 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/support/error/CoreException.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 741
🏁 Script executed:
find . -type d -name "db" -o -name "migration" -o -name "flyway" -o -name "liquibase" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -type f \( -name "*.sql" -o -name "*.ddl" \) 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "class.*Repository" --type java | grep Member | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./modules/jpa/src/main/resources/jpa.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2903
🏁 Script executed:
find . -type f -name "application*.yml" -o -name "application*.yaml" | xargs grep -l "jpa\|hibernate" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 256
🏁 Script executed:
cat -n ./apps/commerce-api/src/main/resources/application.yml | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1297
🏁 Script executed:
rg "hibernate\.ddl-auto|spring\.jpa\.hibernate" --type yamlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/infrastructure/member/MemberRepository.java 2>/dev/null || find . -name "MemberRepository.java"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 174
🏁 Script executed:
rg "catch.*DataIntegrityViolationException" --type java -A 3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "class.*Service" --type java | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 683
🏁 Script executed:
find . -type f -name "*Repository.java" -path "*/infrastructure/*" | head -5 | xargs cat | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 672
🏁 Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 524
🏁 Script executed:
find . -path "*/db/migration*" -o -path "*/flyway*" -o -path "*/changelog*" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "^CREATE TABLE.*member" -A 20 --type sqlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 233
🏁 Script executed:
rg "@Transactional.*Propagation" --type java | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./apps/commerce-api/src/test/java/com/loopers/application/MemberServiceTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5710
🏁 Script executed:
rg "@Column.*unique\|@UniqueConstraint" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "Propagation\|Isolation" --type java | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
grep -n "loginId" ./modules/jpa/src/main/java/com/loopers/domain/member/Member.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 234
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | grep -A 2 -B 2 "loginId"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 773
🏁 Script executed:
rg "IllegalArgumentException" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1107
🏁 Script executed:
rg "UNIQUE.*login_id\|login_id.*UNIQUE" --type sql --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | head -35Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1002
🏁 Script executed:
find . -type f -name "*.java" -path "*/test/*" | xargs grep -l "concurrent\|parallel\|ExecutorService\|CountDownLatch" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "class.*Service.*{" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type java -A 5 | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 182
🏁 Script executed:
rg "throw new.*Exception" ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 430
🏁 Script executed:
find . -path "*/infrastructure/*" -name "*Repository*.java" | xargs grep -l "save\|persist" | head -3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "@Column.*loginId" ./modules/jpa/src/main/java/com/loopers/domain/member/Member.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "class.*Repository" ./apps/commerce-api/src/main/java/com/loopers/infrastructure/ --type java -A 10 | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1102
🏁 Script executed:
rg "catch.*DataIntegrityViolationException\|catch.*ConstraintViolation" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find . -name "*.java" -path "*/service/*" | xargs grep -l "@Transactional" | head -5 | xargs grep -A 20 "DataIntegrityViolation\|ConstraintViolation"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 69
🏁 Script executed:
rg "synchronized\|Lock\|ReentrantLock" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2535
🏁 Script executed:
rg "CoreException" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type java -B 2 -A 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 759
🏁 Script executed:
find . -name "ErrorType.java" -o -name "*ErrorCode*.java" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 159
🏁 Script executed:
grep -r "DataIntegrityViolation" . --include="*.java" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
rg "exists.*then.*save\|check.*then.*insert" --type java | head -10Repository: 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/support/error/ErrorType.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 949
🏁 Script executed:
rg "CoreException" ./apps/commerce-api/src/main/java/com/loopers/ --type java | grep "throw\|new" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 750
🏁 Script executed:
grep -n "import.*CoreException\|import.*ErrorType" ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 134
🏁 Script executed:
rg "findByLoginId\|existsByLoginId" ./apps/commerce-api/src/main/java/com/loopers/ --type javaRepository: 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/application/service/MemberService.java | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1457
동시 가입 요청 시 중복 로그인 ID 검증이 원자적이지 않아 경쟁 조건이 발생한다
existsByLoginId() 조회 후 save() 사이에 다른 요청이 동일 ID로 가입하면 중복 레코드가 생성되거나 DataIntegrityViolationException으로 500 오류가 발생해 운영에서 간헐 장애로 보일 수 있다.
현재 Member 엔티티에는 login_id 컬럼에 유니크 제약이 없으므로 다음을 함께 수행해야 한다:
- DB 스키마에 login_id에 UNIQUE 제약을 추가한다
- save() 호출 시 DataIntegrityViolationException을 캐치해 중복 오류로 변환한다
- existsByLoginId는 UX용 조기 피드백으로만 유지한다
동시 가입 요청을 병렬로 실행하고 하나만 성공, 나머지는 중복 오류로 떨어지는지 검증하는 통합 테스트를 추가한다.
🔧 수정 제안
+import org.springframework.dao.DataIntegrityViolationException;
+
`@Transactional`
public void register(MemberRegisterRequest request) {
boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId());
@@ -26,7 +27,11 @@ public class MemberService {
if (isLoginIdAlreadyExists) {
throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message());
}
-
- memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
+ try {
+ memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
+ } catch (DataIntegrityViolationException e) {
+ throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message(), e);
+ }
}📝 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.
| @Transactional | |
| public void register(MemberRegisterRequest request) { | |
| boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId()); | |
| if (isLoginIdAlreadyExists) { | |
| throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message()); | |
| } | |
| memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email())); | |
| } | |
| `@Transactional` | |
| public void register(MemberRegisterRequest request) { | |
| boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId()); | |
| if (isLoginIdAlreadyExists) { | |
| throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message()); | |
| } | |
| try { | |
| memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email())); | |
| } catch (DataIntegrityViolationException e) { | |
| throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message(), e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 19 - 28, 현재 MemberService.register에서
memberRepository.existsByLoginId(...) 조회와 memberRepository.save(...) 사이에 경쟁 조건이
있어 동시 가입 시 중복이 발생합니다; DB 스키마에 login_id에 UNIQUE 제약을 추가하고 MemberService.register에서
save 호출을 감싸 DataIntegrityViolationException을 잡아
MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS로 변환해 던지도록 처리하며
existsByLoginId는 UX용 조기 피드백으로만 유지하세요; 또한 Member 엔티티의 매핑과 마이그레이션을 통해 login_id에
유니크 제약을 추가하고, 통합 테스트를 작성해 여러 스레드/프로세스로 병렬 등록을 시도할 때 하나만 성공하고 나머지는 중복 예외로 실패하는지
검증하십시오.
| public MyMemberInfoResponse getMyInfo(String userId, String password) { | ||
| // 1. 회원 조회 (없으면 예외 발생 - MemberExceptionMessage.Common.NOT_FOUND 사용) | ||
| Member member = memberRepository.findByLoginId(userId) | ||
| .orElseThrow(() -> new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message())); | ||
|
|
||
| // 2. 비밀번호 일치 여부 확인 (도메인 모델의 isSamePassword 활용) | ||
| if (!member.isSamePassword(password)) { | ||
| // 비밀번호 불일치 시 예외 발생 (인증 관련 메시지 사용) | ||
| throw new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message()); | ||
| } |
There was a problem hiding this comment.
IllegalArgumentException 사용으로 오류 포맷과 원인 보존이 일관되지 않다
현재 IllegalArgumentException을 직접 던지면 에러 코드/포맷이 CoreException 경로와 달라져 운영에서 클라이언트 파싱과 장애 분석이 흔들릴 위험이 있다.
중복/인증 실패 등은 CoreException으로 통일하고, 하위 예외가 있는 경우 cause를 보존해 래핑하는 것이 안전하다.
중복 ID, 인증 실패 각각에 대해 동일한 에러 스키마(코드·메시지·필드)가 내려오는지 컨트롤러/E2E 테스트를 추가해야 한다.
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. This guidance applies to all Java files under the commerce-api module (source code under apps/commerce-api/src/main/java/)라는 지침을 따랐다.
As per coding guidelines: 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다.
Also applies to: 55-60
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 31 - 40, Replace the direct IllegalArgumentException throws in
MemberService.getMyInfo with CoreException-based errors that use the appropriate
MemberExceptionMessage (e.g., ExistsMember.CANNOT_LOGIN) so the API error schema
is consistent with ApiControllerAdvice; specifically, throw new
CoreException(...) (or the project’s CoreException factory) and pass the
original cause when wrapping (preserve cause) rather than throwing raw
IllegalArgumentException, separate the error instances for "not found" vs
"authentication failed" using the correct MemberExceptionMessage constants, and
add/update controller/E2E tests to assert the unified error payload (code,
message, field) is returned for duplicate/auth failures.
| Member member = memberRepository.findByLoginId(userId) | ||
| .orElseThrow(() -> new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message())); | ||
|
|
||
| // 2. 본인 확인 (기존 비밀번호 일치 여부) | ||
| if (!member.isSamePassword(currentPassword)) { | ||
| throw new IllegalArgumentException(MemberExceptionMessage.Password.PASSWORD_INCORRECT.message()); // 적절한 메시지로 변경 가능 | ||
| } |
There was a problem hiding this comment.
비밀번호 오류 메시지 차이로 계정 존재 여부가 노출될 수 있다
존재하지 않는 아이디는 CANNOT_LOGIN, 비밀번호 오류는 PASSWORD_INCORRECT로 구분되어 응답이 달라져 계정 열거 공격에 악용될 수 있다.
외부로는 동일한 메시지/코드(CANNOT_LOGIN)로 통일하고, 상세 원인은 내부 로그로만 남기는 방식이 안전하다.
잘못된 아이디와 잘못된 비밀번호 두 케이스 모두 동일한 에러 응답이 내려오는지 통합 테스트를 추가해야 한다.
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 54 - 60, The current login flow in MemberService differentiates
missing accounts (memberRepository.findByLoginId -> CANNOT_LOGIN) from wrong
passwords (member.isSamePassword -> PASSWORD_INCORRECT), which leaks account
existence; change the password-check branch to throw the same CANNOT_LOGIN error
as the not-found branch (keep detailed cause in an internal log using the member
id or login attempt and exception text), ensure the throw uses
MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message(), and add/adjust an
integration test to assert that both non-existent user and incorrect password
produce identical external responses while internal logs capture the distinct
reasons.
| @PostMapping("/register") | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| public void register(@RequestBody MemberRegisterRequest request) { | ||
| memberService.register(request); | ||
| } | ||
|
|
||
| /** | ||
| * 내 정보 조회 | ||
| * 헤더 인증 (ID, PW) 기반 | ||
| */ | ||
| @GetMapping("/me") | ||
| public MyMemberInfoResponse getMyInfo( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String password | ||
| ) { | ||
| return memberService.getMyInfo(loginId, password); | ||
| } | ||
|
|
||
| /** | ||
| * 비밀번호 수정 | ||
| * 헤더 인증 (ID, 기존 PW) + 바디 (새 PW) | ||
| */ | ||
| @PatchMapping("/password") | ||
| @ResponseStatus(HttpStatus.NO_CONTENT) | ||
| public void updatePassword( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String currentPassword, | ||
| @RequestBody PasswordUpdateRequest request | ||
| ) { | ||
| memberService.updatePassword(loginId, currentPassword, request.newPassword()); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find apps/commerce-api/src/main -name "*Controller.java" -type f | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 253
🏁 Script executed:
find apps/commerce-api/src/main -name "*ControllerAdvice.java" -o -name "*ExceptionHandler.java" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 168
🏁 Script executed:
find apps -name "CoreException.java" -o -name "*Exception.java" | grep -i core | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 161
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 7255
🏁 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:
cat -n apps/commerce-api/src/main/java/com/loopers/controller/MemberController.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2029
필수 헤더 누락 시 표준 에러 포맷이 깨질 수 있다
@RequestHeader의 기본값은 required=true이므로, 헤더가 누락되면 MissingRequestHeaderException이 발생한다. 그런데 ApiControllerAdvice에는 이 예외에 대한 명시적 핸들러가 없어서, 제네릭 Throwable 핸들러로 빠진다. 결과적으로 INTERNAL_ERROR 상태코드와 일반 메시지를 반환하게 되어, 다른 검증 오류들(MissingServletRequestParameterException, HttpMessageNotReadableException)의 BAD_REQUEST 포맷과 일치하지 않는다.
LoginId, LoginPw 헤더가 필수인 경우, required=false로 설정한 후 null/blank를 수동으로 검증하고 CoreException으로 변환해야 한다. 이렇게 하면 ApiControllerAdvice의 CoreException 핸들러를 거쳐 일관된 BAD_REQUEST 응답이 반환된다.
<details>
<summary>예시</summary>
```java
`@GetMapping`("/me")
public MyMemberInfoResponse getMyInfo(
`@RequestHeader`(value = "X-Loopers-LoginId", required = false) String loginId,
`@RequestHeader`(value = "X-Loopers-LoginPw", required = false) String password
) {
if (loginId == null || loginId.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "필수 헤더 'X-Loopers-LoginId'가 누락되었습니다.");
}
if (password == null || password.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "필수 헤더 'X-Loopers-LoginPw'가 누락되었습니다.");
}
return memberService.getMyInfo(loginId, password);
}헤더/바디 누락 케이스에서 BAD_REQUEST 상태코드와 표준 에러 포맷이 반환되는지 단위 테스트와 E2E 테스트를 추가한다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/controller/MemberController.java`
around lines 21 - 50, MemberController's header parameters (getMyInfo and
updatePassword) currently rely on required=true which throws
MissingRequestHeaderException and bypasses ApiControllerAdvice's CoreException
handler; change the `@RequestHeader` annotations for "X-Loopers-LoginId" and
"X-Loopers-LoginPw" in MemberController.getMyInfo and
MemberController.updatePassword to required=false, then add explicit null/blank
checks that throw new CoreException(ErrorType.BAD_REQUEST, "<clear message>")
when missing; ensure these checks are applied to both loginId and password (and
currentPassword where applicable) so ApiControllerAdvice handles them and add
unit/E2E tests asserting BAD_REQUEST and the standard error body for missing
header and missing body cases.
| @GetMapping("/me") | ||
| public MyMemberInfoResponse getMyInfo( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String password | ||
| ) { | ||
| return memberService.getMyInfo(loginId, password); | ||
| } | ||
|
|
||
| /** | ||
| * 비밀번호 수정 | ||
| * 헤더 인증 (ID, 기존 PW) + 바디 (새 PW) | ||
| */ | ||
| @PatchMapping("/password") | ||
| @ResponseStatus(HttpStatus.NO_CONTENT) | ||
| public void updatePassword( | ||
| @RequestHeader("X-Loopers-LoginId") String loginId, | ||
| @RequestHeader("X-Loopers-LoginPw") String currentPassword, | ||
| @RequestBody PasswordUpdateRequest request |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 대상 파일의 전체 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/controller/MemberController.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2029
🏁 Script executed:
# 로그 필터 또는 마스킹 설정 파일 검색
find . -type f \( -name "*Log*" -o -name "*Filter*" \) -path "*/commerce-api/*" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 보안/인증 관련 설정 파일 검색
find . -type f \( -name "*Security*" -o -name "*Auth*" -o -name "*Config*" \) -path "*/commerce-api/*" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 헤더 마스킹이나 로깅 관련 설정 검색
rg -i "mask|header|password|sensitive" --type java -A 2 apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 6587
🏁 Script executed:
# MemberService.java 전체 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3628
🏁 Script executed:
# 테스트 코드 확인
find . -type f -name "*MemberControllerTest*" -o -name "*MemberServiceTest*" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 165
🏁 Script executed:
# 로깅 설정 파일 검색
find . -type f \( -name "logback.xml" -o -name "logback-spring.xml" -o -name "log4j*.xml" -o -name "application*.yml" -o -name "application*.yaml" \) -path "*/commerce-api/*"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 139
🏁 Script executed:
# 헤더 로깅 관련 필터 구현 여부 확인
rg -i "filter|interceptor|logging" --type java -l apps/commerce-api/src/main/java/ | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# application.yml 로깅 설정 확인
cat -n apps/commerce-api/src/main/resources/application.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1450
🏁 Script executed:
# MemberServiceTest.java 확인
cat -n apps/commerce-api/src/test/java/com/loopers/application/MemberServiceTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5710
🏁 Script executed:
# 전체 프로젝트의 Filter/Interceptor 구현 검색
find . -type f -name "*Filter*.java" -o -name "*Interceptor*.java" | grep -v node_modules | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 헤더 로그 마스킹 관련 구현 확인
rg "X-Loopers" --type java -B 2 -A 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4666
비밀번호를 커스텀 헤더로 전달하면 운영 환경에서 심각한 정보 유출 위험이 있다
비밀번호를 X-Loopers-LoginPw 헤더로 전달하면 접근 로그, APM, 프록시, 네트워크 모니터링 도구에서 헤더가 그대로 수집되어 보안 사고로 이어질 수 있다. 현재 구조에서는 비밀번호 마스킹을 위한 로그 필터가 구현되지 않았다.
표준 Authorization 헤더(예: Bearer 토큰, Basic 인증) 또는 세션/쿠키 기반 인증으로 전환해야 한다. 불가피하게 헤더 기반 인증을 유지하려면 다음을 필수로 적용해야 한다:
- 로그 필터에서 X-Loopers-LoginPw 헤더를 마스킹 처리
- 모든 로깅 라이브러리(Logback, SLF4J 등)에서 민감 헤더를 제외하거나 마스킹
인증 실패/성공 시나리오와 민감 헤더가 로그에 기록되지 않음을 검증하는 보안 테스트를 추가해야 한다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/controller/MemberController.java`
around lines 31 - 48, The controller currently reads raw passwords from the
custom header "X-Loopers-LoginPw" in MemberController.getMyInfo and
updatePassword; remove this practice by eliminating the
`@RequestHeader`("X-Loopers-LoginPw") parameters and instead accept credentials
via a standard mechanism (preferably Authorization header with Bearer/Basic
handled by your security layer or session/cookie authentication) and update
memberService.getMyInfo and memberService.updatePassword signatures to take the
authenticated principal or token-derived user identity and a request body for
new passwords (e.g., PasswordUpdateRequest); if you cannot migrate immediately,
add a Servlet filter/logging mask that strips or masks "X-Loopers-LoginPw" from
request/response logs and ensure all logging frameworks (Logback/SLF4J) are
configured to exclude or mask that header, then add security tests verifying no
sensitive header values appear in logs on success/failure.
| private String loginId; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n modules/jpa/src/main/java/com/loopers/domain/member/Member.java | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1622
🏁 Script executed:
# DB 마이그레이션 및 스키마 파일 검색
find . -type f \( -name "*.sql" -o -name "*Migration*" -o -name "*.yml" -o -name "*.yaml" -o -name "*.xml" \) | grep -E "(schema|migration|member|flyway|liquibase)" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# member 테이블 정의 확인
rg -i "member" -A 10 -B 2 -g "*.sql" | head -100Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 233
🏁 Script executed:
# 전체 파일 목록 확인
git ls-files | grep -E "\.(sql|xml|yml|yaml)$" | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1118
🏁 Script executed:
# 마이그레이션 디렉토리 확인
find . -type d -name "*migration*" -o -type d -name "*schema*" -o -type d -name "*db*" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# SQL 파일 검색
find . -type f -name "*.sql" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# JPA 관련 설정 파일 검색
find . -type f \( -name "application*.yml" -o -name "application*.yaml" \) 2>/dev/null | xargs catRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2710
🏁 Script executed:
# jpa.yml 내용 확인
cat modules/jpa/src/main/resources/jpa.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2231
🏁 Script executed:
# 리소스 디렉토리 구조 확인
find modules/jpa/src/main/resources -type f | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 123
🏁 Script executed:
# Flyway/Liquibase 마이그레이션 폴더 확인
find . -type d -name "db" -o -type d -name "migration" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 모든 SQL 파일 검색
find . -type f -name "*.sql" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Member 클래스 전체 확인 (특히 `@Table`, `@Column` 등)
cat -n modules/jpa/src/main/java/com/loopers/domain/member/Member.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2535
🏁 Script executed:
# MemberPolicy 검증 로직 확인
find . -type f -name "*MemberPolicy*" -o -name "*member*policy*" 2>/dev/null | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 163
🏁 Script executed:
# 회원 등록 서비스 중복 체크 로직
find . -type f -name "*MemberService*" -o -name "*MemberRepository*" 2>/dev/null | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 432
🏁 Script executed:
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/policy/MemberPolicy.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4669
🏁 Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3628
🏁 Script executed:
cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 524
loginId에 DB 유니크 제약이 없어 동시 가입 시 중복 회원이 저장될 수 있다
현재 코드는 서비스 레이어에서 memberRepository.existsByLoginId()로 중복을 체크하지만, 체크와 저장 사이의 TOCTOU 레이스 조건으로 인해 동시 요청 시 중복 회원이 저장될 수 있다. 더욱이 프로덕션 환경은 ddl-auto: none으로 설정되어 있으므로, 수동 마이그레이션 없이는 DB 제약이 전혀 생성되지 않는다.
loginId 컬럼에 @Column(nullable = false, unique = true) 어노테이션을 추가하고, 프로덕션 스키마 마이그레이션에서 실제 유니크 인덱스를 생성해야 한다. 동일 loginId로 동시 등록 시 DataIntegrityViolationException이 발생하는지 통합 테스트로 확인하도록 한다.
수정 제안 diff
- private String loginId;
+ `@Column`(nullable = false, unique = true)
+ private String loginId;📝 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.
| private String loginId; | |
| `@Column`(nullable = false, unique = true) | |
| private String loginId; |
🤖 Prompt for AI Agents
In `@modules/jpa/src/main/java/com/loopers/domain/member/Member.java` around lines
25 - 26, Member 클래스의 loginId 필드에 DB 수준 유니크 제약이 빠져 TOCTOU로 동시 가입이 허용되므로
Member.loginId에 `@Column`(nullable = false, unique = true) 어노테이션을 추가하고 (클래스:
Member, 필드: loginId) 프로덕션 마이그레이션 스크립스키마에 실제 유니크 인덱스 생성 항목을 추가하여 DB가 중복을 차단하도록
하세요; 또한 서비스 레이어의 memberRepository.existsByLoginId() 체크만으로는 불충분하니 동시 등록을 시뮬레이트하는
통합 테스트를 추가해 동일한 loginId로 동시 저장 시 DataIntegrityViolationException이
발생하는지(assertThrows) 검증하도록 구현하세요.
| public static class Password { | ||
| public static void validate(String password, LocalDate birthDate) { | ||
| // 1-1. 길이 제한 (8~16자) | ||
| if (password == null || password.length() < 8 || password.length() > 16) { | ||
| throw new IllegalArgumentException(MemberExceptionMessage.Password.INVALID_PASSWORD_LENGTH.message()); | ||
| } | ||
|
|
||
| // 1-2. 조합 규칙 수정: "한글 등 허용되지 않은 문자"가 포함되었는지만 체크 | ||
| // 영문, 숫자, 특수문자(@$!%*?&)만 허용하는 정규식으로 변경 (필수 포함 조건 삭제) | ||
| String allowedCharsRegex = "^[A-Za-z\\d@$!%*?&]*$"; | ||
| if (!password.matches(allowedCharsRegex)) { | ||
| throw new IllegalArgumentException(MemberExceptionMessage.Password.INVALID_PASSWORD_COMPOSITION.message()); | ||
| } | ||
|
|
||
| // 2. 생년월일 포함 금지 규칙 (Zero-Birthdate Policy) | ||
| // 이 로직에 도달하기 전에 위 정규식에서 튕기지 않도록 테스트 데이터가 수정되거나 정규식이 유연해야 합니다. | ||
| String yyyyMMdd = birthDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")); | ||
| String yyMMdd = yyyyMMdd.substring(2); | ||
| if (password.contains(yyyyMMdd) || password.contains(yyMMdd)) { | ||
| throw new IllegalArgumentException(MemberExceptionMessage.Password.PASSWORD_CONTAINS_BIRTHDATE.message()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Password.validate에서 birthDate가 null이면 NPE가 발생한다
- 운영 관점: 검증 단계에서 NPE가 터지면 500 응답으로 전환되어 오류 응답 형식이 깨지고 장애 분석이 어려워진다.
- 수정안: birthDate null을 먼저 검사해 명시적 IllegalArgumentException을 던지거나 BirthDate.validate를 선행 호출해야 한다.
- 추가 테스트: birthDate=null 입력 시 기대 메시지로 예외가 발생하는지 단위 테스트를 추가해야 한다.
수정 제안 diff다
public static void validate(String password, LocalDate birthDate) {
+ if (birthDate == null) {
+ throw new IllegalArgumentException("생년월일은 필수입니다.");
+ }
// 1-1. 길이 제한 (8~16자)
if (password == null || password.length() < 8 || password.length() > 16) {
throw new IllegalArgumentException(MemberExceptionMessage.Password.INVALID_PASSWORD_LENGTH.message());
}As per coding guidelines **/*.java: null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다.
🤖 Prompt for AI Agents
In `@modules/jpa/src/main/java/com/loopers/domain/member/policy/MemberPolicy.java`
around lines 27 - 48, The Password.validate method can NPE when birthDate is
null; add an explicit null check at the start of Password.validate(String
password, LocalDate birthDate) and throw a clear IllegalArgumentException using
the appropriate MemberExceptionMessage (e.g., create/choose a message like
MemberExceptionMessage.Password.INVALID_BIRTHDATE or reuse an existing one)
before any use of birthDate, or call/ensure BirthDate.validate(birthDate) first;
update unit tests to assert that calling Password.validate with birthDate ==
null throws the expected IllegalArgumentException with the correct message.
| private static final String ALGORITHM = "SHA-256"; | ||
|
|
||
| /** | ||
| * 비밀번호를 SHA-256으로 해싱함. | ||
| * (주의: 이 예제는 순수 해싱만 수행합니다. 실무에선 Salt를 추가해야 안전합니다.) | ||
| */ | ||
| public static String encode(String rawPassword) { | ||
| try { | ||
| MessageDigest digest = MessageDigest.getInstance(ALGORITHM); | ||
| byte[] encodedHash = digest.digest(rawPassword.getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| // 바이트 배열을 읽기 쉬운 문자열(Base64)로 변환 | ||
| return Base64.getEncoder().encodeToString(encodedHash); | ||
| } catch (NoSuchAlgorithmException e) { | ||
| throw new RuntimeException("암호화 알고리즘을 찾을 수 없습니다.", e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 일치 여부 확인 | ||
| */ | ||
| public static boolean matches(String rawPassword, String encodedPassword) { | ||
| if (rawPassword == null || encodedPassword == null) return false; | ||
|
|
||
| // 입력받은 원문을 똑같이 해싱해서 결과가 같은지 비교 | ||
| String newHash = encode(rawPassword); | ||
| return newHash.equals(encodedPassword); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 파일 구조 확인
find . -type f -name "PasswordEncryptor.java" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 153
🏁 Script executed:
# PasswordEncryptor 사용처 검색
rg "PasswordEncryptor" --type java -A 2 -B 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 11135
🏁 Script executed:
# User.java 확인 (learnings에서 언급)
find . -type f -name "User.java" -path "*/domain/user/*" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BCrypt 관련 클래스/import 검색
rg "BCrypt|PasswordEncoder" --type java | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# User 엔티티 찾기
find . -type f -name "User.java" 2>/dev/null | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Member 엔티티 확인
cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | head -80Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2535
🏁 Script executed:
# PasswordEncryptor 전체 코드 확인
cat -n ./modules/jpa/src/main/java/com/loopers/utils/PasswordEncryptor.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1528
🏁 Script executed:
# Member 디렉토리 구조 확인
find ./modules/jpa/src/main/java/com/loopers/domain/member -type f -name "*.java" | sortRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 311
🏁 Script executed:
# User.java 경로 재확인
find . -type f -name "*.java" -path "*domain/user*" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# commerce-api의 User 관련 클래스 검색
find ./apps/commerce-api -type f -name "*.java" -path "*user*" 2>/dev/null | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BCrypt 또는 Spring Security PasswordEncoder 사용 여부 확인
rg "PasswordEncoder|BCryptPasswordEncoder" --type java | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# pom.xml이나 build.gradle에서 보안 라이브러리 확인
find . -name "pom.xml" -o -name "build.gradle" | xargs grep -l "spring-security\|bcrypt" 2>/dev/null | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 프로젝트 구조 전체 확인 - User 엔티티 위치 찾기
find . -type f -name "*.java" | xargs grep -l "class User" | grep -E "(domain|entity)" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# gradle/maven 파일 직접 확인
find . \( -name "build.gradle*" -o -name "pom.xml" \) | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 252
🏁 Script executed:
# PasswordEncryptor encode/matches 동작 확인 - 같은 비밀번호로 여러 번 해싱 시 결과 일관성
python3 << 'PYEOF'
import hashlib
import base64
# SHA-256 동작 확인
password = "test123"
for i in range(3):
h = hashlib.sha256(password.encode('utf-8')).digest()
encoded = base64.b64encode(h).decode('utf-8')
print(f"Iteration {i+1}: {encoded}")
print("\n결론: 같은 비밀번호는 항상 같은 해시를 생성 → salt 없음 확인")
PYEOFRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 298
🏁 Script executed:
# 보안 정책/가이드 확인
find . -type f \( -name "*.md" -o -name "*.txt" \) | xargs grep -i "password\|hash\|encrypt" 2>/dev/null | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1080
🏁 Script executed:
# gradle.kts 확인 - 의존성 및 보안 라이브러리 확인
cat ./modules/jpa/build.gradle.kts | grep -A 20 "dependencies"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 909
비밀번호 해시가 단순 SHA‑256이라 계정 유출 위험이 크다
현재 PasswordEncryptor는 salt와 반복 없는 SHA-256을 사용하고 있으며, 동일한 비밀번호는 항상 동일한 해시를 생성한다. 이는 Member 엔티티의 register()와 updatePassword() 메서드에서 실제로 사용되는 프로덕션 코드로, 대규모 계정 탈취 및 컴플라이언스 위반의 직접적인 위험요소다.
레인보우 테이블 공격과 GPU 기반 브루트포스에 취약하므로, PBKDF2(예: PBKDF2WithHmacSHA256)나 BCrypt로 즉시 교체해야 한다. PBKDF2는 충분한 반복 횟수(예: 65,536회)와 무작위 salt(16바이트 이상)를 사용하고, 해시 문자열에 버전·salt를 함께 저장해 기존 데이터의 점진적 마이그레이션이 가능하도록 구현해야 한다.
추가로 다음 검증 테스트를 필수로 추가하라:
- 동일 비밀번호가 매번 다른 해시를 생성하는가
- matches() 메서드가 정상/오류 비밀번호를 정확히 구분하는가
- 잘못된 포맷이나 null 입력이 안전하게 실패하는가(타이밍 공격 방지를 위해 MessageDigest.isEqual() 사용)
수정 제안 diff
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.MessageDigest;
public class PasswordEncryptor {
- private static final String ALGORITHM = "SHA-256";
+ private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
+ private static final int ITERATIONS = 65_536;
+ private static final int KEY_LENGTH = 256;
+ private static final int SALT_LENGTH = 16;
+ private static final SecureRandom RANDOM = new SecureRandom();
/**
- * 비밀번호를 SHA-256으로 해싱함.
- * (주의: 이 예제는 순수 해싱만 수행합니다. 실무에선 Salt를 추가해야 안전합니다.)
+ * 비밀번호를 PBKDF2로 해싱함.
*/
public static String encode(String rawPassword) {
+ if (rawPassword == null) {
+ throw new IllegalArgumentException("비밀번호는 필수입니다.");
+ }
try {
- MessageDigest digest = MessageDigest.getInstance(ALGORITHM);
- byte[] encodedHash = digest.digest(rawPassword.getBytes(StandardCharsets.UTF_8));
-
- // 바이트 배열을 읽기 쉬운 문자열(Base64)로 변환
- return Base64.getEncoder().encodeToString(encodedHash);
- } catch (NoSuchAlgorithmException e) {
+ byte[] salt = new byte[SALT_LENGTH];
+ RANDOM.nextBytes(salt);
+ byte[] hash = pbkdf2(rawPassword.toCharArray(), salt);
+ return Base64.getEncoder().encodeToString(salt) + ":" +
+ Base64.getEncoder().encodeToString(hash);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("암호화 알고리즘을 찾을 수 없습니다.", e);
}
}
/**
* 일치 여부 확인
*/
public static boolean matches(String rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword == null) return false;
-
- // 입력받은 원문을 똑같이 해싱해서 결과가 같은지 비교
- String newHash = encode(rawPassword);
- return newHash.equals(encodedPassword);
+ String[] parts = encodedPassword.split(":");
+ if (parts.length != 2) return false;
+ try {
+ byte[] salt = Base64.getDecoder().decode(parts[0]);
+ byte[] expected = Base64.getDecoder().decode(parts[1]);
+ byte[] actual = pbkdf2(rawPassword.toCharArray(), salt);
+ return MessageDigest.isEqual(actual, expected);
+ } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeySpecException e) {
+ return false;
+ }
+ }
+
+ private static byte[] pbkdf2(char[] password, byte[] salt)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
+ return SecretKeyFactory.getInstance(ALGORITHM).generateSecret(spec).getEncoded();
}
}🧰 Tools
🪛 ast-grep (0.40.5)
[warning] 17-17: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: MessageDigest.getInstance(ALGORITHM)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA
(desede-is-deprecated-java)
[warning] 17-17: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: MessageDigest.getInstance(ALGORITHM)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html
(use-of-aes-ecb-java)
🤖 Prompt for AI Agents
In `@modules/jpa/src/main/java/com/loopers/utils/PasswordEncryptor.java` around
lines 10 - 36, The current PasswordEncryptor (methods encode and matches) must
be replaced to use a slow KDF with per-password random salt and iterations
(e.g., PBKDF2WithHmacSHA256, iterations=65536, salt length>=16) instead of raw
SHA-256: implement a new encode that generates a SecureRandom salt, derives the
key with SecretKeyFactory/PBEKeySpec, and returns a versioned encoded string
containing version, base64(salt), and base64(hash); update matches to parse that
format, re-derive the hash with the stored salt/iterations and compare using
MessageDigest.isEqual for constant-time equality; ensure null/invalid-format
inputs return false safely; and add unit tests that verify (1) same password
yields different hashes due to salt, (2) matches correctly accepts/rejects, and
(3) invalid/null formats safely fail—also keep
Member.register()/updatePassword() compatibility by using the new encode/matches
and include a version prefix to allow future migration.
| @Test | ||
| public void 아이디는_중복_가입할_수_없음() throws Exception { | ||
| //given | ||
|
|
||
| //when | ||
|
|
||
| //then | ||
|
|
||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
빈 테스트 메서드 제거 또는 구현 필요
테스트 본문이 비어 있어 실제로 어떤 검증도 수행하지 않는다. 주석에 "서비스의 통합 테스트"라고 명시되어 있으나, 빈 테스트 메서드는 테스트 커버리지 착시를 유발하고 CI 파이프라인에서 실패 없이 통과되어 운영 배포 시 검증 누락을 야기할 수 있다.
수정안:
- 해당 테스트를
@Disabled("통합 테스트로 이동 필요")처리하거나 - 테스트 메서드를 완전히 삭제하고, 통합 테스트 파일에서 구현한다.
수정 제안
- // 1-3 -> 레포지토리 가져오니까 서비스의 통합 테스트
- `@Test`
- public void 아이디는_중복_가입할_수_없음() throws Exception {
- //given
-
- //when
-
- //then
-
- }
+ // 1-3: 아이디 중복 검증은 MemberServiceIntegrationTest에서 수행📝 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.
| @Test | |
| public void 아이디는_중복_가입할_수_없음() throws Exception { | |
| //given | |
| //when | |
| //then | |
| } | |
| // 1-3: 아이디 중복 검증은 MemberServiceIntegrationTest에서 수행 |
🤖 Prompt for AI Agents
In
`@modules/jpa/src/testFixtures/java/com/loopers/testcontainers/domain/member/MemberTest.java`
around lines 106 - 114, The test method 아이디는_중복_가입할_수_없음 in MemberTest is empty
and should either be removed or disabled/implemented; update the MemberTest
class by either deleting the empty 아이디는_중복_가입할_수_없음() method or annotate it with
`@Disabled`("통합 테스트로 이동 필요") if you intend to move it to an integration test
suite, or implement the actual duplicate-ID assertion logic against the
service/repository used in other tests so the method performs real verification.
| @Test | ||
| @DisplayName("입력받은 비밀번호가 저장된 비밀번호와 다르면 false를 반환한다") | ||
| void isSamePassword_Fail() { | ||
| // given | ||
| String savedPassword = "password123!"; | ||
| Member member = Member.builder() | ||
| .password(savedPassword) | ||
| .build(); | ||
|
|
||
| // when | ||
| boolean result = member.isSamePassword("wrongPassword"); | ||
|
|
||
| // then | ||
| assertThat(result).isFalse(); | ||
| } |
There was a problem hiding this comment.
테스트 데이터 설정 불일치로 인한 테스트 신뢰성 문제
isSamePassword_Fail 테스트에서 savedPassword를 인코딩하지 않고 저장하고 있다. isSamePassword 메서드는 PasswordEncryptor.matches()를 사용하여 비교할 가능성이 높으므로, 인코딩되지 않은 비밀번호와 비교 시 항상 false를 반환하여 테스트가 의미 없이 통과할 수 있다.
Line 360-372의 isSamePassword_Success 테스트와 비교했을 때, 인코딩 처리가 일관되지 않다.
수정 제안
`@Test`
`@DisplayName`("입력받은 비밀번호가 저장된 비밀번호와 다르면 false를 반환한다")
void isSamePassword_Fail() {
// given
String savedPassword = "password123!";
Member member = Member.builder()
- .password(savedPassword)
+ .password(PasswordEncryptor.encode(savedPassword))
.build();
// when
boolean result = member.isSamePassword("wrongPassword");
// then
assertThat(result).isFalse();
}🤖 Prompt for AI Agents
In
`@modules/jpa/src/testFixtures/java/com/loopers/testcontainers/domain/member/MemberTest.java`
around lines 374 - 388, The test stores a raw password into Member.password
causing a false-positive because Member.isSamePassword uses
PasswordEncryptor.matches; update the isSamePassword_Fail setup to encode the
saved password the same way as isSamePassword_Success (use the same
PasswordEncryptor.encrypt/encode call or helper used in the other test) before
building the Member instance so the comparison path mirrors real usage in
Member.isSamePassword and the test reliably asserts a mismatch when passed
"wrongPassword".
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
|
|
||
| public class MemberPolicy { |
There was a problem hiding this comment.
이전 멘토링을 통해 도메인 객체에서 값 객체를 사용하면 값 객체 고유 로직(ex. 정책 검증 등)을 풀어내는 데 유지보수성이 좋다는 피드백, 다만 JPA에서는 사용하기 까다롭다는 단점도 있다는 피드백을 해주셨습니다.
이에 이 코드와 같이 정책 관련된 사항을 검증하는 객체를 따로 분리해 적용해보았습니다. 이렇게 따로 분리하는 방법이 값 객체를 대체할 만한지, 아니면 오버 엔지니어링일지에 대한 멘토님의 의견이 궁금합니다.
📌 Summary
💬 리뷰 포인트
✅ Checklist
📎 기타 참고 사항