Skip to content

Conversation

@juoklee
Copy link

@juoklee juoklee commented Feb 5, 2026

📌 Summary

  • 배경: 회원 관리 기능(가입/조회/비밀번호 수정)을 TDD로 구현하면서 설계 결정과 테스트 전략에 대한 고민 필요
  • 목표:
    • claude 을 나의 스타일에 맞게 설정.
    • Layered Architecture + CQRS 스타일로 테스트 가능한 구조 설계
  • 결과: 3개 API 구현 완료, E2E + 단위 테스트 병행으로 검증, 도메인 로직은 엔티티에 위치하여 캡슐화 유지

🧭 Context & Decision

선택지와 결정

1. 왜 User가 아니라 Member인가?

문제: 엔티티 이름을 User로 할지 Member로 할지

이름 장점 단점
User 일반적, 익숙함 Spring Security User와 충돌, 너무 포괄적
Member 도메인 용어와 일치 ("회원") 덜 익숙할 수 있음

결정: Member 사용

이유:

  • 도메인 용어: "회원가입", "회원 정보" 등 비즈니스 용어와 일치
  • 충돌 방지: org.springframework.security.core.userdetails.User와 import 충돌 없음
  • 명확한 의미: User는 "사용자" (시스템 관점), Member는 "회원" (비즈니스 관점)

2. TDD 접근법 선택

문제: TDD를 어떤 방향으로 진행할 것인가?

방식 설명
Outside-In E2E → Controller → Service → Domain
Inside-Out Domain → Service → Controller → E2E

결정: Inside-Out 방식 채택

MemberTest → MemberServiceTest → MemberV1ApiE2ETest

이유: 의존성이 없는 도메인부터 견고하게 만들 수 있고, Mock/Stub 사용 최소화


3. 비밀번호 변경 로직 위치

문제: changePassword()를 Member 엔티티 vs MemberService 어디에 둘 것인가?

기준 엔티티 서비스
캡슐화 ✅ setter 노출 안 함 ❌ setter 필요
일관성 Member.create()와 동일 패턴 다른 패턴
의존성 encoder 파라미터 전달 서비스가 주입

결정: 엔티티에 위치 (Rich Domain Model)

트레이드오프: encoder를 파라미터로 넘겨야 하지만, Member.create()와 일관성 유지


4. Facade 필요성 의문

문제: Facade가 단순 위임만 하는데 필요한가?

// 현재 - 너무 단순해 보임
public MemberInfo register(...) {
    Member member = memberService.register(...);
    return MemberInfo.from(member);
}

결정: 유지

이유:

  • 현재는 단순하지만 일관된 아키텍처 유지
  • 복잡한 Use Case 생기면 여러 Service 조합 가능
  • Controller가 Domain 객체 직접 노출하는 것 방지

5. PasswordEncoder 인터페이스 분리

문제: Spring Security의 PasswordEncoder를 직접 쓰면 안 되나?

결정: Domain 레이어에 커스텀 인터페이스 정의

❌ Domain → Spring Security (외부 의존)
✅ Domain ← Infrastructure (의존성 역전)

이유: 도메인을 순수 Java로 유지 → Stub으로 쉽게 테스트 가능


6. Reader/Repository 분리 (CQRS 스타일)

문제: 일반적인 JPA 방식처럼 하나의 Repository에서 조회/저장을 모두 처리할지, 분리할지

결정: 조회(Reader)와 저장(Repository)으로 분리

이유:

  • 테스트 용이: 필요한 인터페이스만 Fake로 구현 가능
  • 의존성 명확: 생성자만 봐도 조회만 하는지, 저장도 하는지 알 수 있음
  • CQRS 확장 가능: 나중에 읽기/쓰기 DB 분리 시 구현체만 교체

트레이드오프: 현재 규모에서는 오버엔지니어링처럼 보일 수 있지만, 테스트 작성 편의성과 의존성 명확화를 위해 채택

🏗️ Design Overview

  • 영향 받는 모듈/도메인: apps/commerce-api - member 도메인
  • 신규 추가:
    • Domain: Member, MemberService, MemberReader, MemberRepository, PasswordEncoder
    • Infrastructure: MemberJpaRepository, MemberReaderImpl, MemberRepositoryImpl, PasswordEncoderImpl
    • Application: MemberFacade, MemberInfo
    • Interfaces: MemberV1Controller, MemberV1Dto
    • Support: MemberAuthFilter, SecurityConfig
    • Test: MemberTest, MemberServiceTest, MemberV1ApiE2ETest
    • API Test: http/commerce-api/member-v1.http
  • 수정:
    • apps/commerce-api/build.gradle.kts - Spring Security 의존성 추가
  • 리팩토링:
    • MemberModelMember로 rename (더 명확한 네이밍)

핵심 구조

interfaces/api/member/
├── MemberV1Controller.java     # REST API 엔드포인트
└── MemberV1Dto.java            # Request/Response DTO

application/member/
├── MemberFacade.java           # Use Case 조율
└── MemberInfo.java             # 도메인 → 애플리케이션 변환 객체

domain/member/
├── Member.java                 # 핵심 엔티티
├── MemberService.java          # 도메인 서비스
├── MemberReader.java           # 조회 인터페이스
├── MemberRepository.java       # 저장 인터페이스
└── PasswordEncoder.java        # 암호화 인터페이스

infrastructure/member/
├── MemberJpaRepository.java    # Spring Data JPA
├── MemberReaderImpl.java       # Reader 구현체
├── MemberRepositoryImpl.java   # Repository 구현체
└── PasswordEncoderImpl.java    # BCrypt 구현체

support/
├── auth/MemberAuthFilter.java  # 인증 필터
└── config/SecurityConfig.java  # Spring Security 설정

주요 컴포넌트 책임

레이어 컴포넌트 책임
Interfaces MemberV1Controller HTTP 요청/응답 처리, DTO 변환
MemberV1Dto Request/Response 정의 (Java Record)
Application MemberFacade Service 호출 + MemberInfo 변환
MemberInfo 도메인 객체 → 애플리케이션 객체 변환, 이름 마스킹
Domain Member 엔티티 + 검증 로직 + 비밀번호 변경 (Rich Domain Model)
MemberService 중복 체크 + Member 생성/저장 조율
MemberReader 조회 전용 인터페이스 (existsByLoginId, findByLoginId)
MemberRepository 저장 전용 인터페이스 (save)
PasswordEncoder 암호화 인터페이스 (encode, matches)
Infrastructure MemberJpaRepository Spring Data JPA 인터페이스
MemberReaderImpl MemberReader 구현체
MemberRepositoryImpl MemberRepository 구현체
PasswordEncoderImpl BCrypt 기반 암호화 구현체
Support MemberAuthFilter 헤더 인증 (X-Loopers-LoginId/Pw)
SecurityConfig Spring Security 설정 (CSRF 비활성화 등)

🔁 Flow Diagram

Main Flow

회원가입 흐름

sequenceDiagram
    autonumber
    participant Client
    participant Controller as MemberV1Controller
    participant Facade as MemberFacade
    participant Service as MemberService
    participant Reader as MemberReader
    participant Member
    participant Repository as MemberRepository

    Client->>Controller: POST /api/v1/members
    Controller->>Facade: register(loginId, password, ...)
    Facade->>Service: register(loginId, password, ...)
    Service->>Reader: existsByLoginId(loginId)
    Reader-->>Service: false
    Service->>Member: Member.create(loginId, password, ..., encoder)
    Note over Member: 정적 팩토리 메서드에서<br/>모든 검증 수행
    Member-->>Service: member
    Service->>Repository: save(member)
    Repository-->>Service: member
    Service-->>Facade: member
    Facade->>Facade: MemberInfo.from(member)
    Facade-->>Controller: MemberInfo
    Controller-->>Client: 201 Created + MemberResponse
Loading

비밀번호 변경 흐름

sequenceDiagram
    autonumber
    participant Client
    participant Filter as MemberAuthFilter
    participant Controller as MemberV1Controller
    participant Facade as MemberFacade
    participant Service as MemberService
    participant Member
    participant Repository as MemberRepository

    Client->>Filter: PATCH /api/v1/members/me/password<br/>+ X-Loopers-LoginId/Pw 헤더
    Filter->>Filter: 인증 검증
    Filter->>Controller: request.setAttribute("authenticatedMember", member)
    Controller->>Facade: changePassword(member, currentPw, newPw)
    Facade->>Service: changePassword(member, currentPw, newPw)
    Service->>Member: member.changePassword(currentPw, newPw, encoder)
    Note over Member: 1. 현재 비밀번호 일치 확인<br/>2. 새 비밀번호 ≠ 현재<br/>3. 새 비밀번호 규칙 검증
    Member-->>Service: void (상태 변경됨)
    Service->>Repository: save(member)
    Repository-->>Service: member
    Service-->>Facade: void
    Facade-->>Controller: void
    Controller-->>Client: 200 OK
Loading

내 정보 조회 흐름

sequenceDiagram
    autonumber
    participant Client
    participant Filter as MemberAuthFilter
    participant Controller as MemberV1Controller
    participant Facade as MemberFacade

    Client->>Filter: GET /api/v1/members/me<br/>+ X-Loopers-LoginId/Pw 헤더
    Filter->>Filter: 인증 검증 (Reader + PasswordEncoder)
    Filter->>Controller: request.setAttribute("authenticatedMember", member)
    Controller->>Facade: getMe(authenticatedMember)
    Facade->>Facade: MemberInfo.fromWithMaskedName(member)
    Note over Facade: 이름 마스킹 처리<br/>"홍길동" → "홍길*"
    Facade-->>Controller: MemberInfo (마스킹된 이름)
    Controller-->>Client: 200 OK + MemberResponse
Loading

변경 목적: Spring Security 의존을 도입해 회원가입, 내 정보 조회, 비밀번호 수정 기능을 도메인 주도(Inside-Out TDD)로 구현하여 인증·회원 관리를 마련함.

핵심 변경점: Member 도메인에 엄격한 유효성 검증(로그인ID 영숫자, 비밀번호 8–16자·생년월일 미포함, 이름 한글/영문 정규화, 이메일 형식)과 changePassword 로직 포함; 도메인 PasswordEncoder 인터페이스 + BCrypt 구현으로 보안 분리; MemberReader/MemberRepository(CQRS 스타일)와 MemberFacade, Controller, JPA 리포지토리 및 unique index 추가; 커스텀 헤더(X-Loopers-LoginId/X-Loopers-LoginPw) 기반 인증 필터와 SecurityConfig 적용.

리스크/주의사항: 커스텀 헤더 인증은 표준 인증 방식이 아니며 SecurityConfig가 현재 모든 요청을 허용하고 CSRF를 비활성화해 운영 환경용 보안 설정 보완이 필요; 비밀번호에 대한 생년월일 검사(문자열 매칭)와 이름 마스킹/정규화 로직의 국제화/형식 변경 영향 검토 권장; 로그인Id 고유 제약(DB 인덱스) 충돌 처리 경로 확인 필요 — 이들에 대해 운영 정책(토큰 기반 인증 도입 계획 등)이 있나요?

테스트/검증: 도메인 단위(MemberTest), 서비스 단위(MemberServiceTest) 유닛 테스트로 검증 규칙과 변경 비밀번호 로직 확인 및 E2E(MemberV1ApiE2ETest)로 회원가입·인증·비밀번호 변경의 성공/오류 시나리오와 데이터 격리 검증, 또한 http/commerce-api/member-v1.http에 사용 예시 및 오류 케이스 수록.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

도메인, 인프라, 애플리케이션, 인터페이스 계층 전반에 걸쳐 회원가입, 인증(커스텀 필터 기반), 내정보 조회, 비밀번호 변경 기능과 관련된 엔티티·서비스·리포지토리·컨트롤러·보안 설정을 추가했다다.

Changes

Cohort / File(s) Summary
빌드 설정
apps/commerce-api/build.gradle.kts
Spring Security 스타터(spring-boot-starter-security) 의존성 추가.
도메인 - 엔티티 및 규칙
apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
Member JPA 엔티티 생성, 생성/비밀번호 변경 시 입력 검증 및 비밀번호 규칙 구현.
도메인 - 포트
apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java, .../MemberReader.java, .../PasswordEncoder.java
영속성 및 조회 포트와 비밀번호 인코더 계약 인터페이스 추가.
도메인 - 서비스
apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
회원가입 중복체크, Member 생성 위임 및 비밀번호 변경 처리 로직 추가.
인프라 구현체
apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java, .../MemberReaderImpl.java, .../MemberRepositoryImpl.java, .../PasswordEncoderImpl.java
Spring Data JPA 리포지토리 정의 및 도메인 포트 구현, BCrypt 기반 PasswordEncoder 구현 추가.
애플리케이션 계층
apps/commerce-api/src/main/java/com/loopers/application/member/MemberFacade.java, .../MemberInfo.java
Facade 도입으로 서비스 호출 감싸기 및 Member→MemberInfo 변환(마스킹 포함) 추가.
인터페이스 / API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java, .../MemberV1Dto.java
가입(POST), 내정보 조회(GET), 비밀번호 변경(PATCH) REST 엔드포인트 및 DTO 추가.
보안/필터링
apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java, .../config/SecurityConfig.java
요청 헤더 기반 커스텀 인증 필터 추가 및 SecurityFilterChain에 필터 삽입.
공통/에러
apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
새 에러 타입 UNAUTHORIZED(HTTP 401) 추가.
테스트 및 샘플
apps/commerce-api/src/test/.../MemberTest.java, MemberServiceTest.java, MemberV1ApiE2ETest.java, http/commerce-api/member-v1.http
도메인 단위 테스트, 서비스 테스트, E2E 통합테스트 및 HTTP 컬렉션 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as MemberV1Controller
    participant Facade as MemberFacade
    participant Service as MemberService
    participant Member as Member
    participant Encoder as PasswordEncoder
    participant Repo as MemberRepository
    participant DB as Database

    Client->>Controller: POST /api/v1/members (register)
    Controller->>Facade: register(...)
    Facade->>Service: register(...)
    Service->>Service: existsByLoginId(loginId)?
    Service->>Member: Member.create(..., encoder)
    Member->>Encoder: encode(rawPassword)
    Encoder-->>Member: encodedPassword
    Service->>Repo: save(member)
    Repo->>DB: INSERT
    DB-->>Repo: persisted member
    Repo-->>Service: member
    Service-->>Facade: member
    Facade-->>Controller: MemberInfo
    Controller-->>Client: 201 Created (MemberResponse)
Loading
sequenceDiagram
    participant Client
    participant Filter as MemberAuthFilter
    participant Reader as MemberReader
    participant DB as Database
    participant Encoder as PasswordEncoder
    participant Controller as MemberV1Controller
    participant Facade as MemberFacade
    participant Service as MemberService

    Client->>Filter: GET /api/v1/members/me + auth headers
    Filter->>Reader: findByLoginId(loginId)
    Reader->>DB: SELECT member
    DB-->>Reader: member
    Reader-->>Filter: member
    Filter->>Encoder: matches(rawPw, encodedPw)
    Encoder-->>Filter: true/false
    Filter->>Filter: request.setAttribute("authenticatedMember", member)
    Filter-->>Controller: proceed
    Controller->>Facade: getMe(authenticatedMember)
    Facade->>Facade: toMemberInfo(with masked name)
    Facade-->>Controller: MemberInfo
    Controller-->>Client: 200 OK (MemberResponse)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

운영 관점 검토 의견

  • 인증 실패 원인 불명확성 — 문제: MemberAuthFilter가 모든 인증 실패를 동일한 401 응답으로 처리하면 운영·모니터링·디버깅 시 실패 원인(헤더 누락/사용자 없음/비밀번호 불일치)을 식별하기 어렵다. 수정안: ErrorType에 세부 실패 코드를 추가하거나 응답 body에 실패 코드·shortMessage 필드를 포함하여 원인별로 구분된 에러를 반환하도록 변경한다. 추가 테스트: 각 실패 원인별 E2E 테스트(헤더 누락, 존재하지 않는 loginId, 잘못된 비밀번호)를 추가한다다.
  • 인증 로직 중복 및 책임 분산 — 문제: PasswordEncoder.matches 호출이 필터와 도메인(changePassword) 양쪽에 분산되어 있어 검증 규칙 변경 시 유지보수가 어렵다. 수정안: MemberService에 authenticate(MemberCredentials) 같은 전용 메서드를 두어 인증 책임을 중앙화하고 필터는 해당 서비스 호출로 대체한다. 추가 테스트: 서비스 수준 인증 유닛 테스트와 필터의 통합 테스트를 분리해 보장한다다.
  • 입력 검증 경계와 DB 제약 미반영 — 문제: 도메인 레벨 검증은 있지만 DB 제약(예: loginId 유니크 인덱스)이 코드와 별개로 적용되지 않으면 동시성 경쟁에서 중복 가입이 발생할 수 있다. 수정안: DB에 unique constraint/index를 추가하고 회원가입 처리 시 데이터베이스 예외를 서비스에서 캡처하여 적절한 400/409 응답으로 변환한다. 추가 테스트: 동시성 가입 시나리오(멀티스레드/트랜잭션)를 통한 레이스 컨디션 검증과 DB 예외 핸들링 E2E 테스트를 추가한다다.
  • 에러 메시지/국제화 관리 — 문제: ErrorType에 한글 메시지가 하드코딩되어 있어 다국어 또는 운영 로그 일관성에 제약이 있다. 수정안: 메시지 키 기반의 메시지 소스 사용으로 메시지 관리·로깅을 분리하고, 운영 로그에는 기계 판독용 코드/키를 포함하도록 변경한다. 추가 테스트: 다양한 로케일에서의 응답 내용 검증 테스트를 추가한다다.
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 Pull Request의 핵심 변경사항인 '회원가입, 내 정보 조회, 비밀번호 수정 기능 구현'을 명확하게 요약한다.
Description check ✅ Passed PR 설명은 요약, 설계 의사결정, 컴포넌트 책임, 시퀀스 다이어그램을 포함하여 템플릿의 핵심 섹션을 대부분 충족한다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java (1)

65-76: validatePassword에서 비밀번호에 생년월일 포함 여부를 YYYYMMDD 형식만 검사한다.

Line 72에서 birthDate.toString().replace("-", "")으로 19900115 형태만 검사하는데, 0115, 900115, 1990-01-15 등 다른 부분 문자열 조합은 통과된다. 비밀번호 정책의 의도가 생년월일 관련 모든 변형을 차단하는 것이라면, 추가 패턴(MMDD, YYMMDD 등)도 검사하는 것을 권장한다.

apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java (2)

49-57: 인증 실패 시 회원 미존재와 비밀번호 불일치를 동일하게 처리하는 것은 보안상 적절하다.

타이밍 공격 방어 측면에서 한 가지 참고: findByLoginId가 회원을 찾지 못한 경우 BCrypt 비교 없이 즉시 반환되므로, 존재하는 회원과 존재하지 않는 회원의 응답 시간 차이로 사용자 존재 여부가 유추될 수 있다. 운영 환경에서 이를 완화하려면, 회원 미존재 시에도 더미 BCrypt 비교를 수행하는 방안을 고려할 수 있다.


31-62: 모든 인증 요청마다 DB 조회 + BCrypt 비교가 수행된다.

BCrypt는 의도적으로 느린 해시 알고리즘이며, 매 요청마다 DB 조회와 BCrypt 비교를 수행하면 트래픽 증가 시 병목이 된다. 현재 프로젝트 단계에서 즉시 개선이 필요하지는 않으나, 운영 확장 시 세션 또는 토큰 기반 인증으로의 전환을 고려해야 한다.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/MemberV1ApiE2ETest.java (2)

428-442: registerMember 헬퍼가 ChangePassword 내부 클래스에만 정의되어 있다.

GetMe 테스트에서도 동일한 회원가입 로직이 반복된다(Lines 163-175). 중복을 줄이기 위해 registerMember를 상위 클래스 레벨로 이동하면 GetMeChangePassword 모두에서 재사용할 수 있다.


54-151: 필수 입력값 누락(null/blank) 시나리오에 대한 E2E 테스트가 없다.

Member.create()에 null/blank 가드가 추가되었으나, 해당 검증이 API 레벨에서 400을 반환하는지 확인하는 E2E 테스트가 부족하다. 예: loginId가 null 또는 공백, password가 null, name이 공백 등의 케이스를 추가하면 회귀 방지에 도움이 된다. As per coding guidelines: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java (1)

63-69: "authenticatedMember" 문자열 리터럴이 MemberAuthFilter와 중복된다.

MemberAuthFilter(Line 60)와 MemberV1Controller(Line 64)에서 동일한 매직 스트링 "authenticatedMember"를 사용한다. 한쪽에서 오타가 발생하면 런타임에서만 발견되므로, 공유 상수로 추출하는 것을 권장한다.

수정 예시
// MemberAuthFilter.java 또는 공통 상수 클래스
+public static final String AUTHENTICATED_MEMBER_ATTR = "authenticatedMember";

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@juoklee
Copy link
Author

juoklee commented Feb 5, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java`:
- Around line 32-81: The constructor factory Member.create and its validators
(validatePassword, validateLoginId, normalizeName, validateName, validateEmail)
lack null/blank checks causing NPEs; add guard clauses at the start of create to
validate non-null and non-blank loginId, rawPassword, name, email and non-null
birthDate and throw CoreException(ErrorType.BAD_REQUEST, "...") with appropriate
messages, and update validatePassword to explicitly check birthDate != null
before using it; ensure name is trimmed/blank-checked before normalizeName and
keep Member immutable (defensive copy if needed) and add tests asserting
400/CoreException for null or blank required fields.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`:
- Around line 18-25: Add a database-unique constraint on the Member entity's
loginId by annotating the loginId field (Member.loginId) with `@Column`(unique =
true); update MemberService.register to wrap the memberRepository.save(member)
call in a try/catch that catches DataIntegrityViolationException and throws new
CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 로그인ID입니다."); and add an
integration/concurrency test that fires concurrent register requests using the
same loginId and asserts that exactly one request succeeds while the others
receive CoreException/ErrorType.BAD_REQUEST responses.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java`:
- Around line 8-10: The Member repository queries (existsByLoginId and
findByLoginId in MemberJpaRepository) will cause full table scans unless the
Member.loginId column has a uniqueness constraint and/or an index; update the
Member entity by adding either `@Column`(unique = true) on the loginId field or
add a class-level `@Table`(indexes = {`@Index`(columnList = "login_id")}) so the
database enforces uniqueness and creates an index to speed up existsByLoginId
and findByLoginId lookups.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java`:
- Around line 40-57: The controller assumes
request.getAttribute("authenticatedMember") is a Member and casts it directly in
getMe and changePassword, which can throw a ClassCastException/produce a 500
when the attribute is missing or invalid; update ErrorType to add an
UNAUTHORIZED entry (HttpStatus.UNAUTHORIZED) and then in MemberV1Controller
check the attribute returned by request.getAttribute("authenticatedMember")
before casting — if it's null or not an instance of Member throw a CoreException
with ErrorType.UNAUTHORIZED so the API returns a 401 standard error response;
apply this check in both getMe and changePassword (and any other handlers using
request.getAttribute("authenticatedMember")) and add tests that call /me and
/me/password without auth to assert 401 and the standard error body.

In
`@apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java`:
- Around line 64-75: The requiresAuthentication method currently uses
request.getRequestURI(), which fails when a context path is present; change it
to use request.getServletPath() for path comparisons, keep the POST exact-match
exception for "/api/v1/members" (only skip auth when method=="POST" &&
servletPath.equals("/api/v1/members")), and retain the authentication
requirement for any servletPath startingWith "/api/v1/members/"; update tests to
assert POST /api/v1/members is allowed and GET /api/v1/members is authenticated
in a context-path-enabled environment.
🧹 Nitpick comments (3)
apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceTest.java (2)

34-79: 도메인 유효성 검증 실패 케이스 테스트가 누락되어 있다.

현재 테스트는 중복 loginId와 정상 가입만 검증한다. 도메인 계층에서 수행되는 비밀번호 규칙 위반, 이메일 형식 오류, 이름 유효성 검증 실패 케이스를 단위 테스트에 추가하면, E2E 테스트보다 빠른 피드백을 얻을 수 있다. 장애 발생 시 원인 파악이 용이해진다.


94-98: FakeMemberReader.findByLoginId가 항상 Optional.empty()를 반환한다.

비밀번호 변경 기능 테스트 시 해당 Fake 구현체를 재사용하려면, 저장된 Member를 조회할 수 있도록 확장이 필요하다. 현재 구조에서는 비밀번호 변경 단위 테스트를 작성하기 어렵다.

♻️ 개선안
 static class FakeMemberReader implements MemberReader {
-    private final Map<String, Boolean> existingLoginIds = new HashMap<>();
+    private final Map<String, Member> members = new HashMap<>();

     void addExistingLoginId(String loginId) {
-        existingLoginIds.put(loginId, true);
+        members.put(loginId, null); // existence only
+    }
+
+    void addMember(Member member) {
+        members.put(member.getLoginId(), member);
     }

     `@Override`
     public boolean existsByLoginId(String loginId) {
-        return existingLoginIds.containsKey(loginId);
+        return members.containsKey(loginId);
     }

     `@Override`
     public Optional<Member> findByLoginId(String loginId) {
-        return Optional.empty();
+        return Optional.ofNullable(members.get(loginId));
     }
 }
apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java (1)

19-25: 무상태 인증인데 기본 세션이 생성될 수 있어 운영 비용이 늘 수 있다

현재 설정은 HttpSession 기반 SecurityContext 저장소가 활성화된 상태라 익명 인증도 세션에 저장되어 JSESSIONID가 발급될 가능성이 있고, 이는 메모리 사용 및 세션 복제 비용 증가로 이어질 수 있다.
수정안으로 무상태 세션 정책과 기본 로그인 기능 비활성화를 추가하는 것이 안전하다.
추가 테스트로 /api/v1/members/me 호출 후 Set-Cookie(JSESSIONID) 헤더 부재와 기존 인증 플로우 정상 동작을 E2E로 검증하는 것을 권장한다.

🔧 수정안 예시
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

 `@RequiredArgsConstructor`
 `@Configuration`
 `@EnableWebSecurity`
 public class SecurityConfig {
@@
     `@Bean`
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         return http
             .csrf(csrf -> csrf.disable())
+            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+            .formLogin(form -> form.disable())
+            .httpBasic(basic -> basic.disable())
             .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
             .addFilterBefore(memberAuthFilter, UsernamePasswordAuthenticationFilter.class)
             .build();
     }
 }

Comment on lines +18 to +25
public Member register(String loginId, String rawPassword, String name,
LocalDate birthDate, String email) {
if (memberReader.existsByLoginId(loginId)) {
throw new CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 로그인ID입니다.");
}

Member member = Member.create(loginId, rawPassword, name, birthDate, email, passwordEncoder);
return memberRepository.save(member);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MemberService.java" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

find . -type f -name "Member.java" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 156


🏁 Script executed:

fd -t f "MemberService" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 242


🏁 Script executed:

wc -l ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
wc -l ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 242


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1427


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4823


🏁 Script executed:

find . -type f -name "MemberRepository.java" -o -name "MemberReader.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 244


🏁 Script executed:

grep -r "DataIntegrityViolationException" ./apps/commerce-api/src --include="*.java" | head -20

Repository: 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/domain/member/MemberServiceTest.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4987


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 225


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberReader.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 335


🏁 Script executed:

grep -r "unique\|@Column\|@Index" ./apps/commerce-api/src/main/java/com/loopers/domain/member --include="*.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

find ./apps/commerce-api -type f -name "*.java" | xargs grep -l "implements MemberRepository\|implements MemberReader" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 351


🏁 Script executed:

find ./apps/commerce-api -type f -name "*MemberRepository*.java" -o -name "*MemberReader*.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 428


🏁 Script executed:

grep -r "class.*MemberRepository\|class.*MemberReader" ./apps/commerce-api/src --include="*.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 682


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 699


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberReaderImpl.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 929


🏁 Script executed:

grep -n "Column\|Index\|unique\|constraint" ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

find ./apps/commerce-api -type f -name "MemberJpaRepository.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 177


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 511


🏁 Script executed:

find ./apps/commerce-api -type f \( -name "*.sql" -o -name "*Migration*.java" -o -name "schema*" \) | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

find ./apps -type f -name "*.xml" -o -name "*.sql" | xargs grep -l "member" 2>/dev/null | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

grep -r "DataIntegrityViolationException\|ConstraintViolationException" ./apps/commerce-api/src/main/java --include="*.java" -A 3 | head -30

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

grep -r "@Transactional\|@Column" ./apps/commerce-api/src/main/java/com/loopers/domain/member --include="*.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

find ./apps/commerce-api -type f -name "application*.yml" -o -name "application*.yaml" | xargs cat 2>/dev/null | grep -A 20 "jpa\|hibernate" | head -30

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 356


동시 가입 요청에서 로그인ID 중복 경쟁 상태 처리 필수

현재 코드는 existsByLoginId 검사 후 저장하는 사이에 다른 요청이 동일 loginId를 저장할 수 있어, 데이터베이스 중복 제약 위반 시 DataIntegrityViolationException이 발생하면 500 에러로 노출된다. 운영 안정성을 위해 다음을 필수 처리해야 한다:

  1. Member 엔티티의 loginId 필드에 @Column(unique=true) 추가로 데이터베이스 수준 중복 방지
  2. MemberService.register 메서드에서 DataIntegrityViolationException을 포착하여 CoreException(ErrorType.BAD_REQUEST)로 변환해 일관된 400 응답 반환
  3. 동시 등록 시나리오 테스트 추가 (동일 loginId로 다중 요청 시 하나만 성공, 나머지는 400 반환 확인)
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`
around lines 18 - 25, Add a database-unique constraint on the Member entity's
loginId by annotating the loginId field (Member.loginId) with `@Column`(unique =
true); update MemberService.register to wrap the memberRepository.save(member)
call in a try/catch that catches DataIntegrityViolationException and throws new
CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 로그인ID입니다."); and add an
integration/concurrency test that fires concurrent register requests using the
same loginId and asserts that exactly one request succeeds while the others
receive CoreException/ErrorType.BAD_REQUEST responses.

Comment on lines +64 to +75
private boolean requiresAuthentication(HttpServletRequest request) {
String path = request.getRequestURI();
String method = request.getMethod();

// POST /api/v1/members (회원가입)는 인증 불필요
if ("POST".equals(method) && "/api/v1/members".equals(path)) {
return false;
}

// /api/v1/members/** 경로는 인증 필요
return path.startsWith("/api/v1/members/");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "MemberAuthFilter.java" -type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 165


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3203


🌐 Web query:

HttpServletRequest getRequestURI vs getServletPath difference context path

💡 Result:

In the Servlet API, these three values are different slices of the incoming request path:

  • request.getRequestURI(): the full request path within the server, from the start of the path up to (but not including) the query string. (So it includes the context path + servlet mapping + any extra path info.) [1]

  • request.getContextPath(): the webapp’s context root (the portion of the URI that identifies which app is being addressed). It starts with /, does not end with /, and is "" for the root (default) context. [2]

  • request.getServletPath(): the portion of the URL that matched the servlet mapping (the “servlet’s own path”), excluding any extra path info and query string. If the servlet was matched by the "/*" mapping, this can be "". [1]

A common way to relate them is:

getRequestURI() = getContextPath() + getServletPath() + getPathInfo() (where getPathInfo() may be null)

Example

Request URL: http://host:8080/shop/api/orders/123?x=1

  • getContextPath()/shop
  • getServletPath()/api (depending on your servlet mapping)
  • getPathInfo()/orders/123 (if the mapping is /api/*)
  • getRequestURI()/shop/api/orders/123 [1][2]

Sources: Jakarta Servlet HttpServletRequest Javadoc for getRequestURI(), getServletPath(), getContextPath() [1][2].

[1] (jakarta.ee)
[2] (jakarta.ee)

Citations:


컨텍스트 경로 환경에서 회원가입 엔드포인트 인증 요구 문제

getRequestURI() 사용 시 컨텍스트 경로가 포함되므로, 컨텍스트 경로가 있는 배포 환경에서 POST /api/v1/members(회원가입) 요청이 경로 비교 실패로 인해 인증을 요구하게 되어 의도한 공개 엔드포인트가 차단된다.

수정안: request.getServletPath() 기준으로 처리하고, "/api/v1/members" 정확 일치 조건에서 POST만 예외 처리한다.

추가 테스트: 컨텍스트 경로가 있는 환경에서 POST /api/v1/members가 401로 차단되지 않는지, GET /api/v1/members는 401로 차단되는지 검증한다.

🔧 수정 예시
    private boolean requiresAuthentication(HttpServletRequest request) {
-        String path = request.getRequestURI();
+        String path = request.getServletPath();
        String method = request.getMethod();

-        // POST /api/v1/members (회원가입)는 인증 불필요
-        if ("POST".equals(method) && "/api/v1/members".equals(path)) {
-            return false;
-        }
-
-        // /api/v1/members/** 경로는 인증 필요
-        return path.startsWith("/api/v1/members/");
+        if ("/api/v1/members".equals(path)) {
+            return !"POST".equals(method);
+        }
+        return path.startsWith("/api/v1/members/");
    }
📝 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.

Suggested change
private boolean requiresAuthentication(HttpServletRequest request) {
String path = request.getRequestURI();
String method = request.getMethod();
// POST /api/v1/members (회원가입)는 인증 불필요
if ("POST".equals(method) && "/api/v1/members".equals(path)) {
return false;
}
// /api/v1/members/** 경로는 인증 필요
return path.startsWith("/api/v1/members/");
}
private boolean requiresAuthentication(HttpServletRequest request) {
String path = request.getServletPath();
String method = request.getMethod();
if ("/api/v1/members".equals(path)) {
return !"POST".equals(method);
}
return path.startsWith("/api/v1/members/");
}
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java`
around lines 64 - 75, The requiresAuthentication method currently uses
request.getRequestURI(), which fails when a context path is present; change it
to use request.getServletPath() for path comparisons, keep the POST exact-match
exception for "/api/v1/members" (only skip auth when method=="POST" &&
servletPath.equals("/api/v1/members")), and retain the authentication
requirement for any servletPath startingWith "/api/v1/members/"; update tests to
assert POST /api/v1/members is allowed and GET /api/v1/members is authenticated
in a context-path-enabled environment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/commerce-api/build.gradle.kts`:
- Around line 12-14: Spring Security 6.4.4 (managed by the Spring Boot BOM) is
vulnerable to CVE-2025-22234; override the managed version by adding a
dependencyManagement entry that forces
org.springframework.security:spring-security-core to 6.4.5 or later (use
dependencyManagement and specify the
org.springframework.security:spring-security-core:6.4.5 coordinate), then update
CI to run a dependency-vulnerability scan task and add smoke tests for signup,
login, and password-change authorization flows to validate the fix.

In `@apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java`:
- Around line 32-43: Validate null inputs at the start of Member.create and
Member.changePassword: check that encoder is not null in create, and that
encoder, currentPassword, and newRawPassword are not null in changePassword; if
any are null, throw a CoreException with BAD_REQUEST to ensure unified error
handling via ApiControllerAdvice. Update Member.create (method name: create) and
Member.changePassword (method name: changePassword) to perform these null-guards
before any processing (e.g., before calling encoder.encode or password checks).
Add unit tests asserting that calling Member.create with a null encoder and
Member.changePassword with null encoder/currentPassword/newRawPassword each
result in a CoreException with BAD_REQUEST. Ensure messages in exceptions
provide minimal context for debugging while keeping the error type consistent.

In
`@apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java`:
- Around line 36-57: MemberAuthFilter's authentication failures currently send
only a 401 status without the standard ApiResponse JSON; update the filter
(doFilterInternal in MemberAuthFilter) to serialize the standard ApiResponse
with ErrorType.UNAUTHORIZED into the response body instead of returning empty
status: ensure ErrorType includes an UNAUTHORIZED(401) entry, set
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED), set
response.setContentType("application/json;charset=UTF-8"), write the
JSON-serialized ApiResponse (containing error code/message) to
response.getWriter() and return without proceeding the filter chain on
header-missing, user-not-found (memberReader.findByLoginId), and
password-mismatch (passwordEncoder.matches) branches; add tests verifying
header-missing, non-existent user, and wrong-password scenarios return 401 plus
the standard ApiResponse JSON.

In
`@apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java`:
- Around line 19-25: Security is too lax: in securityFilterChain(HttpSecurity)
you must stop accepting plaintext passwords via headers and tighten
CSRF/authorization; replace the current permissive config
(authorizeHttpRequests(...).anyRequest().permitAll()) and the memberAuthFilter
approach with a token-based authentication flow (e.g., JWT/Bearer via a
JwtAuthenticationFilter) or at minimum HTTP Basic, enable CSRF unless you
explicitly choose stateless JWT (document the reason if you call
csrf().disable()), and register an
AuthenticationEntryPoint/AuthenticationFailureHandler to log authentication
failures (log non-sensitive context such as loginId and failure reason but never
the password). Locate SecurityFilterChain securityFilterChain, memberAuthFilter,
addFilterBefore(..., UsernamePasswordAuthenticationFilter.class) and update them
accordingly to implement token parsing/validation, proper authorization rules,
CSRF handling, and secure failure logging.
🧹 Nitpick comments (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/member/PasswordEncoderImpl.java (1)

7-19: BCrypt 강도 하드코딩은 성능 튜닝을 제한한다.

BCryptPasswordEncoder의 기본 strength는 10이며, 각 +1마다 해시/검증 시간이 약 2배 증가한다. 기본값 고정 시 트래픽 증가에 따른 로그인 지연이 발생하면, 하드웨어 성능에 맞춰 강도를 조정할 수 없어 운영 유연성이 떨어진다. Spring Security 권장사항에 따르면 본 운영 환경에서 검증이 약 1초 이내로 완료되는 최대 강도를 선택하고, 이후 하드웨어 개선 시 점진적으로 증가시켜야 한다.

strength를 설정값으로 주입해 환경별 조정을 가능하게 하고, PasswordEncoderImpl은 주입받은 BCryptPasswordEncoder 인스턴스를 사용하도록 변경한다.

수정 예시
 import com.loopers.domain.member.PasswordEncoder;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Component;

 `@Component`
 public class PasswordEncoderImpl implements PasswordEncoder {
-    private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
+    private final BCryptPasswordEncoder bCryptPasswordEncoder;
+
+    public PasswordEncoderImpl(`@Value`("${security.bcrypt.strength:10}") int strength) {
+        this.bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
+    }

application.yml(또는 환경별 프로파일)에 설정값을 추가하고, application-test.yml에는 더 낮은 strength(예: 4)를 지정한 뒤 인코딩/매칭이 정상 동작하는지 스프링 컨텍스트 통합 테스트를 작성한다. 또한 본 운영 환경에서 목표 검증 시간에 맞는 strength 값을 벤치마킹으로 결정해야 한다.

Comment on lines +12 to +14

// security
implementation("org.springframework.boot:spring-boot-starter-security")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n apps/commerce-api/build.gradle.kts

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1271


🏁 Script executed:

# Find root build.gradle.kts or gradle configuration
fd -t f "build.gradle.kts" | head -5

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 242


🏁 Script executed:

cat -n build.gradle.kts

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4538


🏁 Script executed:

cat -n gradle.properties

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 747


🌐 Web query:

Spring Boot 3.4.4 spring-boot-starter-security version CVE

💡 Result:

Spring Boot 3.4.4’s spring-boot-starter-security pulls in Spring Security 6.4.4 (via the Spring Boot BOM). [1]

CVE context for Spring Security 6.4.4

So, if you’re on Boot 3.4.4, you’re on Spring Security 6.4.4 (includes fixes for CVE-2025-22223/22228, but is impacted by CVE-2025-22234). [1][2][3]

What to do

  • Upgrade Spring Boot to a release that manages Spring Security ≥ 6.4.5, or override the managed version to 6.4.5+. [3]

References:
[1] (github.com)
[2] (spring.io)
[3] (spring.io)

Citations:


🏁 Script executed:

# Search for dependency scanning or security tasks in gradle files
rg -i "(dependenc.*scan|security.*scan|owasp|snyk|checkmarx)" --type gradle

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 233


🏁 Script executed:

# Search for CI configuration and security scanning
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".gradle" \) | grep -E "(github|gitlab|workflow|ci)" | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 110


🏁 Script executed:

# Search for GitHub workflows
find .github/workflows -type f 2>/dev/null | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Check if there are any security scanning plugins in any gradle file
rg "dependency.*check|owasp|snyk" --type-list

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 474


🏁 Script executed:

# Check build gradle files for any security-related configuration
find . -name "build.gradle.kts" -type f | xargs grep -l "security\|cve\|vulnerability" 2>/dev/null

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 121


Spring Security 6.4.4의 CVE-2025-22234 취약점 해결이 필수다.

Spring Boot 3.4.4 BOM에 의해 Spring Security 6.4.4가 관리되고 있으나, 해당 버전은 CVE-2025-22234에 영향을 받는다. 이 취약점은 인증/인가 처리 과정에서 발생하는 보안 결함이며, 상용 환경에서 미해결 시 인증 우회 등의 위험에 노출된다.

수정안으로 dependencyManagement 블록에서 Spring Security를 6.4.5 이상으로 명시적으로 오버라이드한다.

dependencyManagement {
    dependencies {
        dependency("org.springframework.security:spring-security-core:6.4.5")
    }
}

이후 의존성 취약점 스캔 태스크를 CI에 포함하고, 회원가입/로그인/비밀번호 변경 API의 인증/인가 관련 스모크 테스트를 추가한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/build.gradle.kts` around lines 12 - 14, Spring Security
6.4.4 (managed by the Spring Boot BOM) is vulnerable to CVE-2025-22234; override
the managed version by adding a dependencyManagement entry that forces
org.springframework.security:spring-security-core to 6.4.5 or later (use
dependencyManagement and specify the
org.springframework.security:spring-security-core:6.4.5 coordinate), then update
CI to run a dependency-vulnerability scan task and add smoke tests for signup,
login, and password-change authorization flows to validate the fix.

Comment on lines 32 to 43
public static Member create(String loginId, String rawPassword,
String name, LocalDate birthDate,
String email, PasswordEncoder encoder) {
validateLoginId(loginId);
validatePassword(rawPassword, birthDate);
String normalizedName = normalizeName(name);
validateName(normalizedName);
validateEmail(email);

String encodedPassword = encoder.encode(rawPassword);
return new Member(loginId, encodedPassword, normalizedName, birthDate, email);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

비밀번호 변경/생성 입력값 null 가드가 필요하다.
encoder 또는 currentPassword/newRawPassword가 null이면 NPE로 500이 발생해 운영에서 표준 오류 포맷이 깨지고 원인 분석이 어려워진다.
수정안으로 create와 changePassword 시작부에서 null을 검사해 CoreException(BAD_REQUEST)로 통일한다.

수정 예시
     public static Member create(String loginId, String rawPassword,
                                 String name, LocalDate birthDate,
                                 String email, PasswordEncoder encoder) {
+        if (encoder == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 인코더가 필요합니다.");
+        }
         validateLoginId(loginId);
         validatePassword(rawPassword, birthDate);
         String normalizedName = normalizeName(name);
         validateName(normalizedName);
         validateEmail(email);

         String encodedPassword = encoder.encode(rawPassword);
         return new Member(loginId, encodedPassword, normalizedName, birthDate, email);
     }

     public void changePassword(String currentPassword, String newRawPassword,
                                PasswordEncoder encoder) {
+        if (currentPassword == null || newRawPassword == null || encoder == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 입력값이 누락되었습니다.");
+        }
         // 현재 비밀번호 확인
         if (!encoder.matches(currentPassword, this.password)) {
             throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
         }

추가 테스트로 currentPassword/newRawPassword/encoder가 null일 때 BAD_REQUEST가 발생하는지 단위 테스트를 추가한다.
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 'null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다'를 근거로 했다.

Also applies to: 83-97

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java` around
lines 32 - 43, Validate null inputs at the start of Member.create and
Member.changePassword: check that encoder is not null in create, and that
encoder, currentPassword, and newRawPassword are not null in changePassword; if
any are null, throw a CoreException with BAD_REQUEST to ensure unified error
handling via ApiControllerAdvice. Update Member.create (method name: create) and
Member.changePassword (method name: changePassword) to perform these null-guards
before any processing (e.g., before calling encoder.encode or password checks).
Add unit tests asserting that calling Member.create with a null encoder and
Member.changePassword with null encoder/currentPassword/newRawPassword each
result in a CoreException with BAD_REQUEST. Ensure messages in exceptions
provide minimal context for debugging while keeping the error type consistent.

Comment on lines +19 to +25
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.addFilterBefore(memberAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "SecurityConfig.java" --type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

wc -l apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 166


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1339


🏁 Script executed:

fd "MemberAuthFilter.java" --type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

wc -l apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 166


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3203


비밀번호를 HTTP 헤더로 직접 전달하는 것은 보안상 매우 위험하다.

X-Loopers-LoginPw 헤더로 비밀번호를 평문 형태로 전달하면 HTTP 헤더 로그, 프록시 로그, 네트워크 추적 등에서 민감정보가 노출될 수 있다. 또한 CSRF 보호를 비활성화한 상태에서 인가 검증(anyRequest().permitAll())이 없으므로 보안 방어층이 미흡하다.

개선안:

  • Bearer 토큰 기반 인증(JWT) 또는 HTTP Basic 인증(Base64 인코딩)으로 변경
  • CSRF 보호를 활성화하거나 stateless 설정(JWT) 선택 시 disable 이유 문서화
  • 인증 실패 로그를 추가해 보안 모니터링 가능하도록 구성 (시도한 loginId, 실패 사유 등)
  • 로그에는 민감정보(비밀번호) 기록 금지
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java`
around lines 19 - 25, Security is too lax: in securityFilterChain(HttpSecurity)
you must stop accepting plaintext passwords via headers and tighten
CSRF/authorization; replace the current permissive config
(authorizeHttpRequests(...).anyRequest().permitAll()) and the memberAuthFilter
approach with a token-based authentication flow (e.g., JWT/Bearer via a
JwtAuthenticationFilter) or at minimum HTTP Basic, enable CSRF unless you
explicitly choose stateless JWT (document the reason if you call
csrf().disable()), and register an
AuthenticationEntryPoint/AuthenticationFailureHandler to log authentication
failures (log non-sensitive context such as loginId and failure reason but never
the password). Locate SecurityFilterChain securityFilterChain, memberAuthFilter,
addFilterBefore(..., UsernamePasswordAuthenticationFilter.class) and update them
accordingly to implement token parsing/validation, proper authorization rules,
CSRF handling, and secure failure logging.

- MemberV1Controller: getAuthenticatedMember() 헬퍼 메서드 추가로 null 체크
- MemberInfo: maskName()에서 null 입력 시 빈 문자열 반환
- MemberAuthFilter: Optional.get() 대신 filter().orElse(null) 패턴 적용
- Member: create(), changePassword()에 null/blank guard clause 추가
- MemberTest: 필수값 검증 테스트 8개 추가
- ExampleV1Dto: 중첩 구조를 flat 구조로 변경
- MemberV1ApiE2ETest: 중첩 DTO 접근자 .member() 추가
- DB 인덱스 생성으로 existsByLoginId/findByLoginId 쿼리 성능 개선
@juoklee
Copy link
Author

juoklee commented Feb 12, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@juoklee juoklee merged commit e394581 into Loopers-dev-lab:juoklee Feb 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants