Skip to content

Conversation

@pable91
Copy link

@pable91 pable91 commented Feb 6, 2026

📌 Summary

배경:

  • 사용자 인증 및 회원 관리 기능이 필요하여 회원가입, 내 정보 조회, 비밀번호 변경 API를 구현함.

목표:

  • 레이어드 아키텍처 기반의 User 도메인 API 구현
  • 헤더 기반 인증 처리를 ArgumentResolver로 분리하여 재사용성 확보
  • 비밀번호 보안 정책(암호화, 생년월일 포함 금지) 적용

결과:

  • 3개 API 구현 완료 (POST /users, GET /users/me, PATCH /users/me/password)
  • CredentialsHeadersArgumentResolver: 회원가입 시 헤더 검증
  • AuthUserArgumentResolver: 인증이 필요한 API에서 사용자 식별
  • 비밀번호는 BCrypt로 암호화, 생년월일 포함 시 거부

🧭 Context & Decision

문제 정의

현재 동작/제약:

  • 인증 정보를 HTTP 헤더(X-Loopers-LoginId, X-Loopers-LoginPw)로 전달
  • JWT 등 토큰 기반 인증이 아닌 매 요청마다 인증 수행
  • Request DTO에 Bean Validation 애노테이션(@notblank, @SiZe, @pattern)으로 형식 검증

문제(또는 리스크):

  • 컨트롤러에 헤더 파싱/검증 로직이 중복될 수 있음
  • 인증 로직과 비즈니스 로직이 섞일 위험
  • Bean Validation은 HTTP 진입점에서만 동작하여, 다른 진입점(Kafka, 배치 등) 추가 시 검증 누락 가능

성공 기준(완료 정의):

  • 컨트롤러는 비즈니스 로직에만 집중
  • 인증/검증은 ArgumentResolver에서 처리
  • 형식 검증 실패 시에도 CoreException과 동일한 응답 포맷 유지
  • 단위 테스트 커버리지 확보

선택지와 결정

고려한 대안:
A: Filter에서 인증 처리

  • 모든 요청에 적용되어 세밀한 제어 어려움
  • 컨트롤러 파라미터로 인증 정보 주입이 불편

B: ArgumentResolver에서 인증 처리

  • 특정 파라미터 타입(@authuser AuthUserPrincipal)에만 적용
  • 컨트롤러 시그니처만 보고 인증 필요 여부 파악 가능

C: DTO Bean Validation vs Service 직접 검증

  • Bean Validation: 선언적, 간결, 빠른 실패
  • Service 검증: 모든 진입점 보호, 코드량 증가

최종 결정: B (ArgumentResolver) + DTO Bean Validation
트레이드오프:

  • 인증이 필요한 API마다 @authuser 어노테이션 필요
  • 하지만 명시적이고 테스트하기 쉬움
  • Bean Validation은 HTTP 진입점만 보호하지만, 현재는 HTTP만 사용하므로 충분
  • ApiControllerAdvice에서 MethodArgumentNotValidException을 동일 포맷으로 처리하여 응답 일관성 확보

추후 개선 여지:

  • JWT 토큰 기반 인증으로 전환 시 ArgumentResolver만 수정하면 됨
  • Spring Security 도입 시 점진적 마이그레이션 가능
  • HTTP 외 진입점 추가 시 형식 검증을 Service 레이어로 이동 검토

🏗️ Design Overview

영향 받는 모듈/도메인:

  • apps/commerce-api

신규 추가:

  • interfaces/api/AuthUser - 인증 사용자 주입 어노테이션
  • interfaces/api/AuthUserArgumentResolver - 인증 처리
  • interfaces/api/CredentialsHeaders - 로그인 헤더 DTO
  • interfaces/api/CredentialsHeadersArgumentResolver - 헤더 바인딩
  • application/user/AuthUserPrincipal - 인증된 사용자 식별 정보
  • application/user/UserFacade - 유스케이스 조합
  • application/user/UserInfo - 사용자 정보 응답 DTO
  • application/user/SignUpCommand - 회원가입 커맨드
  • domain/user/UserService - 도메인 서비스
  • domain/user/UserRepository - 레포지토리 인터페이스
  • domain/user/UserModel - JPA 엔티티

제거/대체:

  • 없음 (신규 구현)

🔁 Flow Diagram

1. 회원가입 (POST /users)

sequenceDiagram
    participant Client
    participant Controller
    participant Facade
    participant Service
    participant Repository
    participant PasswordEncoder

    Client->>Controller: POST /users<br/>(loginId, loginPw, birthDate, name, email)
    Controller->>Controller: SignUpCommand 생성
    Controller->>Facade: signUp(command)
    Facade->>Service: createUser(loginId, loginPw, birthDate, name, email)

    Service->>Repository: existsByLoginId(loginId)
    Repository-->>Service: boolean

    Service->>Repository: existsByEmail(email)
    Repository-->>Service: boolean

    Note over Service: validatePasswordNotContainsBirthDate()

    Service->>PasswordEncoder: encode(rawPassword)
    PasswordEncoder-->>Service: encodedPassword

    Service->>Repository: save(userModel)
    Repository-->>Service: UserModel

    Service-->>Facade: UserModel
    Facade-->>Controller: UserInfo
    Controller-->>Client: 200 OK {id}
Loading

2. 내 정보 조회 (GET /users/me)

sequenceDiagram
    participant Client
    participant Controller
    participant AuthResolver
    participant Facade
    participant Service
    participant Repository

    Client->>Controller: GET /users/me<br/>(X-Loopers-LoginId, X-Loopers-LoginPw)

    Controller->>AuthResolver: @AuthUser 바인딩
    AuthResolver->>Service: authenticate(loginId, loginPw)
    Service-->>AuthResolver: UserModel
    AuthResolver-->>Controller: AuthUserPrincipal

    Controller->>Facade: getMyInfo(userId)
    Facade->>Service: findById(userId)
    Service->>Repository: findById(userId)
    Repository-->>Service: UserModel
    Service-->>Facade: UserModel
    Facade-->>Controller: UserInfo

    Note over Controller: MaskingUtils.maskLastCharacter(name)

    Controller-->>Client: 200 OK {loginId, name(masked), birthDate, email}
Loading

3. 비밀번호 변경 (PATCH /users/me/password)

sequenceDiagram
    participant Client
    participant Controller
    participant AuthResolver
    participant Facade
    participant Service
    participant Repository
    participant PasswordEncoder

    Client->>Controller: PATCH /users/me/password<br/>(currentPassword, newPassword)

    Controller->>AuthResolver: @AuthUser 바인딩
    AuthResolver-->>Controller: AuthUserPrincipal

    Controller->>Facade: changePassword(userId, request)
    Facade->>Service: changePassword(userId, currentPw, newPw)

    Service->>Repository: findById(userId)
    Repository-->>Service: UserModel

    Service->>PasswordEncoder: matches(currentPw, user.password)
    PasswordEncoder-->>Service: true

    Service->>PasswordEncoder: matches(newPw, user.password)
    PasswordEncoder-->>Service: false (새 비밀번호 != 기존)

    Note over Service: validatePasswordNotContainsBirthDate()

    Service->>PasswordEncoder: encode(newPassword)
    PasswordEncoder-->>Service: newEncodedPassword

    Service->>Service: user.changePassword(newEncodedPassword)

    Note over Service: @Transactional - 변경 감지로 자동 저장

    Service-->>Facade: void
    Facade-->>Controller: void
    Controller-->>Client: 200 OK
Loading

API 요약

API 인증 방식 주요 흐름
POST /users CredentialsHeaders (검증만) 중복 체크 → 비밀번호 검증 → 암호화 → 저장
GET /users/me @authuser (인증 필수) 인증 → ID로 조회 → UserInfo 반환
PATCH /users/me/password @authuser (인증 필수) 인증 → 현재 비밀번호 확인 → 새 비밀번호 검증 → 변경

변경 목적

레이어드 아키텍처 기반으로 사용자 회원가입, 내 정보 조회, 비밀번호 변경 기능을 구현하며, 헤더 기반 인증 메커니즘을 ArgumentResolver로 분리하여 향후 인증 방식 변경에 대한 확장성을 확보합니다.

핵심 변경점

  • 인증 처리 분리: CredentialsHeadersArgumentResolver(헤더 검증), AuthUserArgumentResolver(사용자 식별)를 통한 ArgumentResolver 기반 인증 구현
  • 도메인 계층 확충: UserModel(JPA 엔티티), UserService(비즈니스 로직), UserRepository(추상화), UserRepositoryImpl(구현체) 추가
  • 애플리케이션 계층: UserFacade를 통한 인증 및 비즈니스 로직 조정, AuthUserPrincipal, SignUpCommand, UserInfo 추가
  • 3개 API 구현: POST /users(회원가입), GET /users/me(내 정보 조회), PATCH /users/me/password(비밀번호 변경)
  • 보안 정책: BCrypt 암호화, 생년월일 포함 검증, 비밀번호 변경 시 기존 비밀번호 확인 등

리스크/주의사항

  • HTTP 진입하지 않으면, DTO 검증이 제대로 잘 이루어 지지 않을 가능성이 있음
  • 헤더 기반 인증의 한계: 매 요청마다 ID/PW를 헤더로 전송하는 방식으로, 세션/토큰 기반 대비 보안 취약점 존재
  • 단일 예외 타입 사용: 모든 비즈니스 예외가 CoreException으로 처리되어, 클라이언트에서 에러 유형별 분기 처리가 어려움

테스트/검증

UsersApiE2ETest에서 회원가입, 내 정보 조회, 비밀번호 변경에 대한 E2E 테스트 실시

  • 회원가입 성공/실패(loginId 중복, email 중복, 생년월일 포함 비밀번호)
  • 내 정보 조회 성공
  • 비밀번호 변경 성공/실패(현재 비밀번호 불일치, 동일 비밀번호, 생년월일 포함)

UserControllerTest에서 MockMvc 기반 컨트롤러 단위 테스트로 입력 검증 커버

  • 이메일 형식, 이름 형식(길이/숫자/특수문자), 생년월일 미래 날짜
  • 비밀번호 길이(8-16자), 한글 포함, 공백 포함 검증
  • 헤더 누락(loginId, loginPw) 검증

UserDtoValidationTest에서 Bean Validation 어노테이션 단위 테스트

UserServiceTest에서 서비스 레이어 비즈니스 로직 단위 테스트

  • 중복 loginId/email 검증, 생년월일 포함 비밀번호 검증
  • 비밀번호 변경 시 현재 비밀번호 일치/불일치, 동일 비밀번호 검증

UserModelTest에서 도메인 엔티티 guard 검증

  • 필수 필드 null 체크 (loginId, password, birthDate, name, email)

김용권 added 30 commits February 2, 2026 15:57
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@pable91 pable91 changed the title Feature/round1 [volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현 Feb 6, 2026
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: 10

🤖 Fix all issues with AI agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java`:
- Around line 18-33: Add a DB-level unique constraint on the UserModel.email
column and handle unique-key violations in UserService.createUser: keep the
existsByEmail check but catch DataIntegrityViolationException (or the underlying
constraint violation) in createUser and translate it to throw new
CoreException(ErrorType.BAD_REQUEST, "...email already registered...") so
concurrent sign-ups return a controlled 400 instead of a 500; alternatively
document/implement a higher isolation/locking strategy if preferred and add a
concurrency test to assert only one of simultaneous signUp calls (via
UserFacade.signUp -> userService.createUser) succeeds while others receive the
mapped CoreException.

In `@apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java`:
- Around line 26-44: Add database-level unique constraints for both loginId and
email in the UserModel entity: modify the class-level mapping (UserModel) to
declare unique constraints for the login_id and email columns (e.g., via
`@Table`(uniqueConstraints=...)) or mark each field as unique via `@Column`(unique =
true) on the loginId and email fields, and ensure column names match "login_id"
and "email". After adding constraints, add/adjust an integration test to attempt
saving duplicate login_id and duplicate email and assert that a
DataIntegrityViolationException (or the JPA persistence exception your stack
throws) is raised when violating each constraint. Ensure UserFacade.signUp
behavior remains compatible (it can keep existsByEmail() but rely on DB
constraint for safety).

In `@apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java`:
- Around line 18-58: createUser currently skips password policy checks and may
pass a raw password into UserModel.create; call
PasswordPolicy.validate(rawPassword) and
validatePasswordNotContainsBirthDate(rawPassword, birthDate) before encoding,
then encode and pass only the BCrypt hash to UserModel.create (ensure
UserModel.create expects a hash, not plain text). Also add
PasswordPolicy.validate(newPassword) in changePassword before encoding (in
method changePassword), and add tests: in UserServiceTest assert createUser
rejects policy-violating or birthdate-containing passwords and that
passwordEncoder.encode is not called on invalid input, and in UserFacadeTest add
a signup policy-violation failure case.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`:
- Around line 54-61: The handleBadRequest(MethodArgumentNotValidException)
handler in ApiControllerAdvice violates the project's error-handling pattern and
has runtime risks; remove the entire handleBadRequest method from
ApiControllerAdvice so validation errors are routed via the domain-layer
CoreException flow, or if you must keep it, make it defensive: check
e.getBindingResult().getFieldErrors() for emptiness before accessing get(0),
null-check fieldError.getDefaultMessage() and replace with a safe message, and
rethrow or wrap into the project's CoreException/ErrorType.BAD_REQUEST to
preserve consistent response formatting (reference:
ApiControllerAdvice.handleBadRequest, MethodArgumentNotValidException,
CoreException, ErrorType.BAD_REQUEST).

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java`:
- Around line 36-39: Replace the direct throw of IllegalStateException in
AuthUserArgumentResolver (where request =
webRequest.getNativeRequest(HttpServletRequest.class) is null) with throwing a
CoreException constructed with an appropriate ErrorType (e.g.,
INTERNAL_SERVER_ERROR) so the error flows through ApiControllerAdvice and
preserves the standard API error format and monitoring; update
AuthUserArgumentResolver's resolveArgument path to create and throw that
CoreException when request == null, and add a unit test or MVC-slice test to
verify that when request is null the controller advice returns the unified error
response format.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java`:
- Around line 3-27: Remove all Bean Validation annotations from the
CredentialsHeaders DTO (e.g., `@NotBlank`, `@Size`, `@Pattern` on loginId/loginPw) and
instead implement the same header format checks inside
CredentialsHeadersArgumentResolver: validate presence and patterns for loginId
and loginPw and throw CoreException(ErrorType.BAD_REQUEST) when validation
fails; update CredentialsHeadersArgumentResolverTest to assert CoreException is
thrown for missing/invalid headers and update UserControllerTest to assert the
API error response format remains the unified CoreException-based format. Ensure
references: class CredentialsHeaders, method in
CredentialsHeadersArgumentResolver that builds/validates the DTO, and tests
CredentialsHeadersArgumentResolverTest and UserControllerTest.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java`:
- Around line 20-46: Remove all Bean Validation annotations from the
CredentialsHeaders DTO and stop calling the injected Validator from
CredentialsHeadersArgumentResolver.resolveArgument; instead construct the
CredentialsHeaders (or pass raw headers) and delegate validation to the
domain/application layer (e.g., User constructor or a new CredentialsValidator
in the domain package) which should throw CoreException on invalid input;
replace the current throws of ConstraintViolationException and
IllegalStateException in resolveArgument with propagation of CoreException (or
throw CoreException when request is null) so the error format is consistent with
ApiControllerAdvice; keep LoopersHeaders.X_LOOPERS_LOGIN_ID and
X_LOOPERS_LOGIN_PW usage for header extraction and ensure integration tests
assert ApiControllerAdvice handles missing/invalid headers and null request
cases.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java`:
- Around line 3-18: Remove the Bean Validation annotations from the
ChangePasswordRequest record (drop `@NotBlank`, `@Size`, `@Pattern`) and implement
equivalent format checks inside UserService.changePassword(): validate
currentPassword/newPassword length (8–16) and allowed characters
(^[A-Za-z0-9\p{P}\p{S}]+$) and on any violation throw the appropriate
CoreException so ApiControllerAdvice will produce the unified error
response/monitoring path; add unit tests in UserServiceTest for invalid-length
and disallowed-character cases and a UsersController slice test asserting the
same CoreException-backed error response format is returned through the
controller.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java`:
- Around line 23-25: In UsersSignUpRequestDto update the `@Size` annotation on the
name field so the constraint and message match: either set max=30 to reflect the
existing message or change the message to "이름은 2자 이상 10자 이하여야 합니다." to reflect
max=10; locate the `@Size`(...) on the name property in UsersSignUpRequestDto and
make the chosen change so validation rule and error text are consistent.

In
`@apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java`:
- Around line 71-407: Remove all Bean Validation annotations from
UsersSignUpRequestDto and remove `@Valid` from UsersController method signature so
DTOs are plain; move validation logic into the domain (User constructor or
SignUpCommand and PasswordPolicy) and ensure UserFacade.signUp throws
CoreException with ErrorType.BAD_REQUEST on validation failures; update tests in
UserControllerTest to mock UserFacade.signUp to throw
CoreException(ErrorType.BAD_REQUEST) for invalid inputs and assert
ApiControllerAdvice’s unified error response; finally remove the
MethodArgumentNotValidException handler from ApiControllerAdvice so only
CoreException handling remains.
🧹 Nitpick comments (3)
apps/commerce-api/src/main/java/com/loopers/config/PasswordEncoderConfig.java (1)

3-13: BCrypt 강도 값을 설정으로 분리하는 편이 안전하다

  • 운영 관점: 고정된 cost는 트래픽 증가 시 CPU 병목이나 보안 강도 저하를 초래할 수 있어 환경별 튜닝이 어렵다.
  • 수정안: 설정값으로 strength를 주입해 BCryptPasswordEncoder(strength)를 생성하고 기본값을 둔다.
  • 추가 테스트: 다른 strength 설정에서 회원가입/로그인 해시 검증이 정상인지 통합 테스트로 확인한다.
🔧 수정 예시
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;

 `@Configuration`
 public class PasswordEncoderConfig {

     `@Bean`
-    public PasswordEncoder passwordEncoder() {
-        return new BCryptPasswordEncoder();
+    public PasswordEncoder passwordEncoder(
+        `@Value`("${security.password.bcrypt.strength:10}") int strength
+    ) {
+        return new BCryptPasswordEncoder(strength);
     }
 }
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)

54-61: 비밀번호 검증 책임을 서비스로 고정하는 편이 낫다

  • 운영 관점: 엔티티에서 null/blank만 확인하면 평문 비밀번호가 유입돼도 탐지하지 못하고, 검증 책임 분산으로 오류 응답 일관성이 깨질 수 있다.
  • 수정안: UserModel.guard()의 password 체크를 제거하고, UserService에서 정책 검증 후 인코딩된 값만 create/changePassword로 전달하도록 계약을 문서화한다.
  • 추가 테스트: UserService에서 정책 위반 시 예외가 발생하고 정상 입력은 인코딩 후 저장되는지 검증하는 테스트를 추가한다.
🔧 수정 예시
-        if (password == null || password.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수입니다.");
-        }
Based on learnings: 'User 엔티티 생성자의 password 파라미터는 BCrypt 해시를 받는다. 원본 비밀번호 검증은 UserService에서 PasswordPolicy.validate()로 수행한 후 암호화된다. 따라서 User 생성자 시점에는 항상 유효한 BCrypt 해시 문자열이 전달되며, 생성자에서 null/공백 검증은 불필요하다'를 근거로 했다.
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)

39-45: password null 검증은 서비스 레벨로 이동하는 편이 낫다

  • 운영 관점: 엔티티에서 password null을 강제하면 서비스 정책 변경 시 테스트가 지속적으로 깨져 운영 배포 지연을 유발할 수 있다.
  • 수정안: 이 테스트는 제거하거나 UserService의 정책 검증 테스트로 이동한다.
  • 추가 테스트: UserService에서 평문 입력이 정책 위반으로 실패하는지와 인코딩 후 저장되는지를 검증한다.
🔧 수정 예시
-    `@DisplayName`("password가 null이면 guard에서 예외가 발생한다.")
-    `@Test`
-    void guard_fail_when_password_is_null() {
-        UserModel user = UserModel.create("user123", null, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com");
-
-        assertThatThrownBy(user::guard).isInstanceOf(CoreException.class);
-    }
Based on learnings: 'User 엔티티 생성자의 password 파라미터는 BCrypt 해시를 받는다. 원본 비밀번호 검증은 UserService에서 PasswordPolicy.validate()로 수행한 후 암호화된다. 따라서 User 생성자 시점에는 항상 유효한 BCrypt 해시 문자열이 전달되며, 생성자에서 null/공백 검증은 불필요하다'를 근거로 했다.

Comment on lines 18 to 33
public UserInfo signUp(SignUpCommand command) {
if (userService.existsByEmail(command.getEmail())) {
throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입되어 있는 아이디 입니다.");
}

// TODO
// 만약 또다른 패스워드 검증 조건이 생기거나 다른 클래스에서도 같이 사용한다면 클래스로 분리해야함
validatePasswordContent(command.getLoginPw(), command.getBirthDate());

UserModel userModel = userService.createUser(
command.getLoginId(),
command.getLoginPw(),
command.getBirthDate(),
command.getName(),
command.getEmail()
);
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:

# 이메일 unique 제약 확인
find . -type f -name "*.java" | head -20 | xargs grep -l "User" | head -5

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

Length of output: 470


🏁 Script executed:

# UserFacade가 있는 디렉토리 구조 파악
find . -type f -name "UserFacade.java" -o -name "UserService.java" -o -name "User.java" | head -10

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

Length of output: 238


🏁 Script executed:

# unique 제약 및 마이그레이션 파일 검색
fd -e java -e sql -e xml | head -30

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

Length of output: 2529


🏁 Script executed:

# 이메일 유니크 제약/인덱스 및 마이그레이션 확인
rg "UniqueConstraint|@Column.*unique|unique\s*=\s*true" --type java -A 2 -B 2

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

Length of output: 67


🏁 Script executed:

# SQL 마이그레이션 파일에서 unique 확인
rg "create table|alter table|unique" --type sql -i

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

Length of output: 233


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java

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

Length of output: 3051


🏁 Script executed:

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

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

Length of output: 513


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

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

Length of output: 3136


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java

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

Length of output: 2318


🏁 Script executed:

# DB 마이그레이션 파일 검색
find . -type f \( -name "*.sql" -o -name "V*.sql" -o -name "*.xml" \) | grep -i migrat

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

Length of output: 67


이메일 중복 확인만으로는 레이스 컨디션을 막기 어렵다

동시 가입 요청이 existsByEmail을 통과한 후 createUser가 호출되면 중복 사용자가 생성되거나 DB 무결성 위반으로 500 에러가 발생한다. 현재 UserModel의 email 필드는 nullable 제약만 있고 unique 제약이 없으며(line 43), UserService.createUser에서 중복키 예외를 처리하지 않는다(line 19-22).

수정안: DB 레벨에 email 컬럼에 unique 제약을 추가하고, createUser에서 DataIntegrityViolationException을 CoreException(BAD_REQUEST)으로 변환하여 처리하거나 트랜잭션 격리 수준을 상향 조정하라. 추가로 동일 이메일 동시 요청 시 하나만 성공하고 나머지는 적절한 예외로 반환되는지 동시성 테스트로 검증하라.

참고로 password 검증 정책(생년월일 포함 여부)은 signUp과 changePassword에서 일관되게 적용되고 있다(UserService line 55, UserFacade line 43-53).

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java`
around lines 18 - 33, Add a DB-level unique constraint on the UserModel.email
column and handle unique-key violations in UserService.createUser: keep the
existsByEmail check but catch DataIntegrityViolationException (or the underlying
constraint violation) in createUser and translate it to throw new
CoreException(ErrorType.BAD_REQUEST, "...email already registered...") so
concurrent sign-ups return a controlled 400 instead of a 500; alternatively
document/implement a higher isolation/locking strategy if preferred and add a
concurrency test to assert only one of simultaneous signUp calls (via
UserFacade.signUp -> userService.createUser) succeeds while others receive the
mapped CoreException.

Comment on lines +26 to +44
@Comment("아이디")
@Column(name = "login_id", nullable = false)
private String loginId;

@Comment("비밀번호")
@Column(name = "password", nullable = false)
private String password;

@Comment("생년월일")
@Column(name = "birth", nullable = false)
private LocalDate birthDate;

@Comment("이름")
@Column(name = "name", nullable = false)
private String name;

@Comment("이메일")
@Column(name = "email", nullable = false)
private String 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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 155


🏁 Script executed:

fd "UserModel" --type f apps/commerce-api

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

Length of output: 230


🏁 Script executed:

git ls-files "apps/commerce-api" | grep -i user | head -20

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

Length of output: 1672


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java

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

Length of output: 3051


🏁 Script executed:

cat -n apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java

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

Length of output: 3097


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java

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

Length of output: 464


🏁 Script executed:

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

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

Length of output: 1209


🏁 Script executed:

find apps/commerce-api -name "*.sql" -o -name "*schema*" -o -name "*migration*" | head -20

Repository: 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/infrastructure/UserJpaRepository.java

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

Length of output: 513


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

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

Length of output: 3136


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java

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

Length of output: 2318


🏁 Script executed:

cat -n apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java

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

Length of output: 6343


🏁 Script executed:

find apps/commerce-api -path "*/resources/*" -name "*.sql" -o -path "*/resources/*" -name "*.xml" | 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/application/UserFacadeTest.java

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

Length of output: 3545


🏁 Script executed:

find apps/commerce-api/src/test -name "*Integration*" -o -name "*Repository*Test*"

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

Length of output: 178


🏁 Script executed:

cat -n apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java | head -50

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

Length of output: 2006


🏁 Script executed:

rg "DataIntegrityViolation|ConstraintViolation|duplicate" apps/commerce-api/src/test --type java

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

Length of output: 5775


🏁 Script executed:

rg "existsByLoginId|findByLoginId" apps/commerce-api/src --type java -A 2 -B 2

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

Length of output: 2362


login_id/email에 데이터베이스 유니크 제약이 필수다

  • 현재 상태: UserFacade.signUp()에서 email만 existsByEmail()로 중복 확인하고, login_id는 중복 체크 없음. 두 필드 모두 데이터베이스 레벨 제약 부재.
  • 운영 관점: 동시 회원가입 시 서비스 레벨 중복 체크만으로는 race condition에 의해 중복 레코드가 생성될 수 있으며, 특히 login_id는 현재 중복 방지 로직이 전혀 없어 인증 및 조회가 모호해질 수 있다.
  • 수정안: users 테이블에 @UniqueConstraint를 추가하여 login_id와 email 모두에 데이터베이스 레벨 유니크 제약을 적용한다.
  • 추가 테스트: 동일 login_id/email 저장 시 DataIntegrityViolationException이 발생하는지 저장소 통합 테스트로 검증한다.
🔧 수정 예시
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.Id;
 import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
@@
-@Table(name = "users")
+@Table(
+    name = "users",
+    uniqueConstraints = {
+        `@UniqueConstraint`(name = "uk_users_login_id", columnNames = "login_id"),
+        `@UniqueConstraint`(name = "uk_users_email", columnNames = "email")
+    }
+)
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java`
around lines 26 - 44, Add database-level unique constraints for both loginId and
email in the UserModel entity: modify the class-level mapping (UserModel) to
declare unique constraints for the login_id and email columns (e.g., via
`@Table`(uniqueConstraints=...)) or mark each field as unique via `@Column`(unique =
true) on the loginId and email fields, and ensure column names match "login_id"
and "email". After adding constraints, add/adjust an integration test to attempt
saving duplicate login_id and duplicate email and assert that a
DataIntegrityViolationException (or the JPA persistence exception your stack
throws) is raised when violating each constraint. Ensure UserFacade.signUp
behavior remains compatible (it can keep existsByEmail() but rely on DB
constraint for safety).

Comment on lines +54 to +61
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldErrors().get(0);
String fieldName = fieldError.getField();
String message = fieldError.getDefaultMessage();
String errorMessage = String.format("필드 '%s': %s", fieldName, message);
return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
}
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

프로젝트 에러 핸들링 패턴 위반 및 런타임 예외 위험이 있다.

이 프로젝트는 MethodArgumentNotValidException 핸들러나 Bean Validation 처리를 도입하지 않고, CoreException을 통해 에러를 라우팅하여 일관된 응답 포맷을 유지하는 것이 원칙이다. DTO에서 Bean Validation 대신 도메인 계층(예: User 엔티티 생성자, PasswordPolicy)에서 검증을 수행하도록 설계되어 있다.

추가로, 다음 런타임 오류 위험이 존재한다:

  • Line 56: getFieldErrors()가 빈 리스트를 반환할 경우 get(0) 호출 시 IndexOutOfBoundsException 발생
  • Line 58: getDefaultMessage()가 null을 반환할 수 있어 "필드 'fieldName': null" 형태의 부적절한 메시지 노출 가능

운영 환경에서 예상치 못한 500 에러로 이어질 수 있으며, 일관성 없는 에러 핸들링은 유지보수와 디버깅을 어렵게 한다.

수정안: 핸들러 제거 또는 방어적 코드 적용

권장안 1: 프로젝트 패턴에 맞게 핸들러 제거

Bean Validation 대신 도메인 계층에서 검증하고 CoreException을 throw하는 방식으로 전환한다.

권장안 2: 핸들러 유지 시 방어적 코드 적용

 `@ExceptionHandler`
 public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
-    FieldError fieldError = e.getBindingResult().getFieldErrors().get(0);
-    String fieldName = fieldError.getField();
-    String message = fieldError.getDefaultMessage();
-    String errorMessage = String.format("필드 '%s': %s", fieldName, message);
+    List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
+    if (fieldErrors.isEmpty()) {
+        return failureResponse(ErrorType.BAD_REQUEST, "요청 값이 올바르지 않습니다.");
+    }
+    FieldError fieldError = fieldErrors.get(0);
+    String fieldName = fieldError.getField();
+    String message = fieldError.getDefaultMessage();
+    String errorMessage = String.format("필드 '%s': %s", fieldName, 
+        message != null ? message : "유효하지 않은 값입니다.");
     return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
 }

추가 테스트: FieldError가 없는 MethodArgumentNotValidException 케이스에 대한 단위 테스트 추가 필요하다.

Based on learnings: "Do not introduce MethodArgumentNotValidException handlers or Bean Validation handling, as that would create inconsistent error handling patterns."

📝 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
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldErrors().get(0);
String fieldName = fieldError.getField();
String message = fieldError.getDefaultMessage();
String errorMessage = String.format("필드 '%s': %s", fieldName, message);
return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
}
`@ExceptionHandler`
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
if (fieldErrors.isEmpty()) {
return failureResponse(ErrorType.BAD_REQUEST, "요청 값이 올바르지 않습니다.");
}
FieldError fieldError = fieldErrors.get(0);
String fieldName = fieldError.getField();
String message = fieldError.getDefaultMessage();
String errorMessage = String.format("필드 '%s': %s", fieldName,
message != null ? message : "유효하지 않은 값입니다.");
return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
}
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`
around lines 54 - 61, The handleBadRequest(MethodArgumentNotValidException)
handler in ApiControllerAdvice violates the project's error-handling pattern and
has runtime risks; remove the entire handleBadRequest method from
ApiControllerAdvice so validation errors are routed via the domain-layer
CoreException flow, or if you must keep it, make it defensive: check
e.getBindingResult().getFieldErrors() for emptiness before accessing get(0),
null-check fieldError.getDefaultMessage() and replace with a safe message, and
rethrow or wrap into the project's CoreException/ErrorType.BAD_REQUEST to
preserve consistent response formatting (reference:
ApiControllerAdvice.handleBadRequest, MethodArgumentNotValidException,
CoreException, ErrorType.BAD_REQUEST).

Comment on lines +36 to +39
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new IllegalStateException("HttpServletRequest not available");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

HttpServletRequest 누락 시 IllegalStateException은 응답 포맷을 깨뜨린다

운영 관점: CoreException 흐름을 건너뛰면 표준 에러 포맷과 모니터링 지표가 깨져 장애 분석이 어려워진다.
수정안: CoreException(적절한 ErrorType, 예: INTERNAL_SERVER_ERROR)으로 변환해 ApiControllerAdvice 흐름을 유지하라.
추가 테스트: Resolver 단위 테스트나 MVC 슬라이스 테스트로 request=null 상황에서 표준 에러 포맷이 내려오는지 검증하라.
Based on learnings: In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice라는 지침을 따랐다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java`
around lines 36 - 39, Replace the direct throw of IllegalStateException in
AuthUserArgumentResolver (where request =
webRequest.getNativeRequest(HttpServletRequest.class) is null) with throwing a
CoreException constructed with an appropriate ErrorType (e.g.,
INTERNAL_SERVER_ERROR) so the error flows through ApiControllerAdvice and
preserves the standard API error format and monitoring; update
AuthUserArgumentResolver's resolveArgument path to create and throw that
CoreException when request == null, and add a unit test or MVC-slice test to
verify that when request is null the controller advice returns the unified error
response format.

Comment on lines +20 to +46
private final Validator validator;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType() == CredentialsHeaders.class;
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new IllegalStateException("HttpServletRequest not available");
}

String loginId = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID);
String loginPw = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW);

CredentialsHeaders headers = new CredentialsHeaders(loginId, loginPw);
Set<ConstraintViolation<CredentialsHeaders>> violations = validator.validate(headers);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
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:

# CredentialsHeaders 클래스 확인
find apps/commerce-api -type f -name "CredentialsHeaders.java" | head -5

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

Length of output: 167


🏁 Script executed:

# CredentialsHeadersArgumentResolver 전체 코드 확인
find apps/commerce-api -type f -name "CredentialsHeadersArgumentResolver.java" -exec wc -l {} \;

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

Length of output: 186


🏁 Script executed:

# ApiControllerAdvice의 예외 핸들러 확인
find apps/commerce-api -type f -name "ApiControllerAdvice.java" | head -3

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

Length of output: 168


🏁 Script executed:

# Validator 사용 패턴 전역 검색
rg -l "Validator|@HandlerMethodArgumentResolver" apps/commerce-api/src/main/java --type java

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

Length of output: 183


🏁 Script executed:

cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java

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

Length of output: 863


🏁 Script executed:

cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java

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

Length of output: 1981


🏁 Script executed:

wc -l apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

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

Length of output: 172


🏁 Script executed:

cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

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

Length of output: 7363


CredentialsHeaders DTO의 Bean Validation 주석과 ArgumentResolver의 Validator 사용은 프로젝트 아키텍처 규칙을 위반한다

프로젝트는 헥사고날 아키텍처 원칙에 따라 검증을 도메인 계층에서 수행하고 모든 오류를 CoreException으로 처리하도록 설계되었다. 현재 코드는 이 패턴을 위반한다.

구체적으로:

  • CredentialsHeaders DTO에 @NotBlank, @Pattern, @Size 주석이 있으므로 제거해야 한다.
  • ArgumentResolver에서 Validator를 직접 호출하여 ConstraintViolationException을 던지는 패턴은 도메인 계층 검증을 우회한다.

ApiControllerAdvice가 ConstraintViolationException 핸들러를 제공하므로 현재 코드가 500 오류로 승격되지는 않지만, 이는 아키텍처 위반을 정당화하지 않는다. 검증은 인증 흐름 확장 시 중복·누락이 발생할 수 있는 기술별 계층(ArgumentResolver)이 아닌 도메인 또는 애플리케이션 계층에서 수행되어야 한다.

수정안:

  • CredentialsHeaders의 모든 Bean Validation 주석 제거
  • 로그인 ID/비밀번호 검증 로직을 도메인 계층(User 엔티티 생성자 또는 별도 검증 클래스)으로 이동
  • ArgumentResolver에서 검증 실패 시 CoreException 발생 (또는 도메인 계층 호출 후 발생한 CoreException 전파)
  • request가 null인 경우도 IllegalStateException 대신 CoreException으로 처리하여 일관된 오류 포맷 유지

테스트:

  • 헤더 누락, 형식 오류, null 요청에 대해 ApiControllerAdvice가 표준 오류 포맷과 적절한 오류 코드를 반환하는지 통합 테스트로 검증해야 한다.
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java`
around lines 20 - 46, Remove all Bean Validation annotations from the
CredentialsHeaders DTO and stop calling the injected Validator from
CredentialsHeadersArgumentResolver.resolveArgument; instead construct the
CredentialsHeaders (or pass raw headers) and delegate validation to the
domain/application layer (e.g., User constructor or a new CredentialsValidator
in the domain package) which should throw CoreException on invalid input;
replace the current throws of ConstraintViolationException and
IllegalStateException in resolveArgument with propagation of CoreException (or
throw CoreException when request is null) so the error format is consistent with
ApiControllerAdvice; keep LoopersHeaders.X_LOOPERS_LOGIN_ID and
X_LOOPERS_LOGIN_PW usage for header extraction and ensure integration tests
assert ApiControllerAdvice handles missing/invalid headers and null request
cases.

Comment on lines +3 to +18
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record ChangePasswordRequest(

@NotBlank(message = "기존 비밀번호는 필수입니다.")
String currentPassword,

@NotBlank(message = "새 비밀번호는 필수입니다.")
@Size(min = 8, max = 16, message = "비밀번호는 8~16자로 입력해주세요.")
@Pattern(
regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$",
message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다."
)
String newPassword
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:

# First, let's locate and examine the ChangePasswordRequest file
find . -name "ChangePasswordRequest.java" -type f

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

Length of output: 173


🏁 Script executed:

# Search for PasswordPolicy to understand validation rules
find . -name "*PasswordPolicy*" -type f

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

Length of output: 67


🏁 Script executed:

# Look for CoreException and error handling
find . -name "CoreException.java" -type f
find . -name "*ControllerAdvice*" -type f

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

Length of output: 249


🏁 Script executed:

# Check other DTOs in the interfaces directory to see the pattern
find . -path "*/interfaces/*" -name "*Request.java" -type f | head -20

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

Length of output: 173


🏁 Script executed:

# Read the ChangePasswordRequest file
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java

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

Length of output: 754


🏁 Script executed:

# Search for PasswordPolicy more broadly
find . -name "*.java" -type f | xargs grep -l "PasswordPolicy" | head -5

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

Length of output: 67


🏁 Script executed:

# Read CoreException
cat -n ./apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java

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

Length of output: 741


🏁 Script executed:

# Read ApiControllerAdvice
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java | head -80

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

Length of output: 4463


🏁 Script executed:

# Search more broadly for validation logic
find . -name "*.java" -type f | xargs grep -l "PasswordPolicy\|password" | grep -E "(service|domain)" | head -10

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

Length of output: 388


🏁 Script executed:

# Check if UserService exists and how it handles password validation
find . -name "UserService.java" -o -name "ChangePasswordService.java" -type f

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

Length of output: 159


🏁 Script executed:

# Look at other DTOs to see if they follow the no-validation pattern
find . -path "*/interfaces/*" -name "*.java" -type f | xargs grep -l "@NotBlank\|@Email\|@Valid" | head -5

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

Length of output: 430


🏁 Script executed:

# Check the User domain entity to understand where validation should happen
find . -name "User.java" -type f | grep -E "domain|entity" | head -5

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

Length of output: 67


🏁 Script executed:

# Read UserService to see password validation logic
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

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

Length of output: 3136


🏁 Script executed:

# Read UserModel to see domain validation
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java | head -100

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

Length of output: 3051


🏁 Script executed:

# Check UsersSignUpRequestDto - another DTO with validation annotations
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java

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

Length of output: 1336


🏁 Script executed:

# Check UsersController to see how validation is currently handled
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java

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

Length of output: 2599


🏁 Script executed:

# Check UserServiceTest to see what validation is expected
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java | head -150

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

Length of output: 6343


🏁 Script executed:

# Search for any test files related to password validation
find . -name "*Test.java" -type f | xargs grep -l "changePassword\|password.*validation" | head -5

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

Length of output: 248


DTO에서 Bean Validation 애노테이션을 제거하고 도메인 레이어에서 통일된 검증 처리가 필요하다

현재 ChangePasswordRequest는 Bean Validation 애노테이션을 사용하고 있어서, 요청 검증 실패 시 MethodArgumentNotValidException으로 분기되어 CoreException 기반의 통일된 응답 포맷과 모니터링 경로가 분리된다. 또한 HTTP 진입점이 아닌 다른 방식의 접근에서는 이 검증이 누락되는 문제가 발생한다.

수정안으로 다음을 수행하라:

  • ChangePasswordRequest에서 @NotBlank, @Size, @Pattern 애노테이션 제거
  • UserService.changePassword()에서 형식 검증 추가 (길이 8~16자, 문자 규칙)
  • 모든 검증 오류를 CoreException으로 처리하여 ApiControllerAdvice를 통해 통일된 응답 제공

추가 테스트로 UserServiceTest에서 형식 위반 케이스를 테스트하고 (길이 초과, 허용되지 않는 문자), UsersController 슬라이스 테스트에서 동일한 오류 응답 포맷이 유지되는지 검증하라.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java`
around lines 3 - 18, Remove the Bean Validation annotations from the
ChangePasswordRequest record (drop `@NotBlank`, `@Size`, `@Pattern`) and implement
equivalent format checks inside UserService.changePassword(): validate
currentPassword/newPassword length (8–16) and allowed characters
(^[A-Za-z0-9\p{P}\p{S}]+$) and on any violation throw the appropriate
CoreException so ApiControllerAdvice will produce the unified error
response/monitoring path; add unit tests in UserServiceTest for invalid-length
and disallowed-character cases and a UsersController slice test asserting the
same CoreException-backed error response format is returned through the
controller.

Comment on lines +23 to +25
@NotBlank(message = "이름은 필수입니다.")
@Size(min = 2, max = 10, message = "이름은 2자 이상 30자 이하여야 합니다.")
@Pattern(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java | head -40

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

Length of output: 1336


🏁 Script executed:

# 전체 파일 구조 확인
wc -l apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java

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

Length of output: 174


🏁 Script executed:

# 관련 테스트 파일 검색
find . -type f -name "*SignUp*Test*.java" -o -name "*UsersSignUp*Test*.java"

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

Length of output: 67


🏁 Script executed:

# 사용자 이름 길이 제약에 대한 다른 참조 검색
rg "이름.*자|name.*length|30" --type java -B 2 -A 2 apps/commerce-api/src/main/java/com/loopers/interfaces/user/

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

Length of output: 676


🏁 Script executed:

# 테스트 파일 더 광범위하게 검색
find . -type f -name "*Test*.java" | grep -i user | head -20

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

Length of output: 491


🏁 Script executed:

# User 도메인 클래스 찾기
fd -e java "User" apps/commerce-api/src/main/java | grep -E "(domain|entity|model)" | head -10

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

Length of output: 304


🏁 Script executed:

# 사용자 관련 테스트 폴더 전체 검색
find . -path "*test*" -name "*User*.java" -o -path "*test*" -name "*SignUp*.java" | head -20

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

Length of output: 491


🏁 Script executed:

# application.yml이나 설정 파일에서 정책 확인
find . -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | xargs grep -l "name\|user" 2>/dev/null | head -10

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

Length of output: 514


🏁 Script executed:

# UserDtoValidationTest 파일 확인
cat -n ./apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java

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

Length of output: 16205


🏁 Script executed:

# UserModel 도메인 클래스 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java

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

Length of output: 3051


🏁 Script executed:

# UserService 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java | head -60

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

Length of output: 2780


🏁 Script executed:

# 데이터베이스 스키마 또는 entity 정의 확인
find . -name "User*.java" -path "*/entity/*" -o -name "User*.java" -path "*/persistence/*" | head -10

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

Length of output: 67


이름 길이 제한값과 에러 메시지가 불일치한다

@Size 어노테이션에서 max=10으로 설정되어 있지만, 메시지는 "30자 이하"라고 명시되어 있다. 클라이언트는 메시지를 보고 최대 30자까지 입력 가능하다고 인식하여 불필요한 재시도와 문의가 발생할 수 있다.

메시지를 "이름은 2자 이상 10자 이하여야 합니다."로 수정하거나, 실제 정책이 30자라면 max 값을 30으로 변경한다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java`
around lines 23 - 25, In UsersSignUpRequestDto update the `@Size` annotation on
the name field so the constraint and message match: either set max=30 to reflect
the existing message or change the message to "이름은 2자 이상 10자 이하여야 합니다." to
reflect max=10; locate the `@Size`(...) on the name property in
UsersSignUpRequestDto and make the chosen change so validation rule and error
text are consistent.

Comment on lines +71 to +407
@DisplayName("회원가입 API 실패 테스트")
@Nested
class UserSignupFailureTest {

@Test
@DisplayName("이메일 형식이 잘못되면 400 Bad Request를 반환한다")
void fail_when_email_format_invalid() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"invalid-email"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이메일이 null이면 400 Bad Request를 반환한다")
void fail_when_email_null() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
null
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름이 null이면 400 Bad Request를 반환한다")
void fail_when_name_null() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
null,
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름이 빈 문자열이면 400 Bad Request를 반환한다")
void fail_when_name_empty() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름이 1자이면 400 Bad Request를 반환한다")
void fail_when_name_too_short() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름이 11자 이상이면 400 Bad Request를 반환한다")
void fail_when_name_too_long() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"가나다라마바사아자차카",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름에 숫자가 포함되면 400 Bad Request를 반환한다")
void fail_when_name_contains_number() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권1",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("이름에 특수문자가 포함되면 400 Bad Request를 반환한다")
void fail_when_name_contains_special_character() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권!",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("생년월일이 null이면 400 Bad Request를 반환한다")
void fail_when_birthDate_null() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
null,
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("생년월일이 미래 날짜이면 400 Bad Request를 반환한다")
void fail_when_birthDate_future() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.now().plusDays(1),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("비밀번호가 7자 이하면 400 Bad Request를 반환한다")
void fail_when_password_too_short() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Abc12!")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("비밀번호가 17자 이상이면 400 Bad Request를 반환한다")
void fail_when_password_too_long() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Abcd123!@#efgh456")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("비밀번호에 한글이 포함되면 400 Bad Request를 반환한다")
void fail_when_password_contains_korean() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1가")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("비밀번호에 공백이 포함되면 400 Bad Request를 반환한다")
void fail_when_password_contains_space() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password 1!")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("X-Loopers-LoginId 헤더가 없으면 400 Bad Request를 반환한다")
void fail_when_loginId_header_missing() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);
String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("로그인 ID에 특수문자가 포함되면 400 Bad Request를 반환한다")
void fail_when_loginId_contains_special_character() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);

String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim!")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1!")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("X-Loopers-LoginPw 헤더가 없으면 400 Bad Request를 반환한다")
void fail_when_loginPw_header_missing() throws Exception {
UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto(
LocalDate.of(1991, 12, 3),
"김용권",
"yk@google.com"
);
String json = objectMapper.writeValueAsString(requestBody);

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.content(json))
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("잘못된 JSON 형식이면 400 Bad Request를 반환한다")
void userSignupApiJsonInvalidTest() throws Exception {
String invalidJson = "{ invalid json }";

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim")
.header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1")
.content(invalidJson))
.andExpect(status().isBadRequest());
}
}

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:

#!/bin/bash
# Check for Bean Validation annotations in DTO
find apps/commerce-api/src/main/java -name "*SignUp*Dto.java" -o -name "*UsersSignUp*Dto.java" | head -5

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

Length of output: 171


🏁 Script executed:

# Check CoreException and ApiControllerAdvice existence
rg -l "class CoreException|class ApiControllerAdvice" apps/commerce-api/src/main/java

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

Length of output: 245


🏁 Script executed:

# Check UserFacade signUp method
fd -e java "UserFacade" apps/commerce-api/src/main/java --exec grep -l "signUp" {}

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

Length of output: 161


🏁 Script executed:

# Search for signUp method signature
rg -A 5 "public.*signUp\(" apps/commerce-api/src/main/java/com/loopers/application

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

Length of output: 769


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java

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

Length of output: 1336


🏁 Script executed:

rg -B 5 -A 20 "@PostMapping.*users|public.*signUp" apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java

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

Length of output: 208


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

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

Length of output: 8455


🏁 Script executed:

find apps/commerce-api/src/main/java -name "*UserController*" -o -name "*UserV1Controller*"

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

Length of output: 67


🏁 Script executed:

rg -B 5 -A 20 "@PostMapping.*users|public.*signUp" apps/commerce-api/src/main/java --type java

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

Length of output: 5899


DTO의 Bean Validation 어노테이션과 테스트가 아키텍처 원칙과 충돌한다

UsersSignUpRequestDto는 @NotNull, @NotBlank, @Email, @Pattern, @Size, @Past 어노테이션을 가지고 있으며, UsersController에서 @Valid로 검증한다. 그러나 프로젝트 아키텍처 원칙은 DTO에서 Bean Validation을 제거하고 도메인 레이어(User 엔티티 생성자, PasswordPolicy)에서 모든 검증을 수행하도록 규정한다.

현재 테스트는 MethodArgumentNotValidException이 400을 반환한다고 가정하므로, DTO 어노테이션이 제거되면 모두 실패한다. 또한 ApiControllerAdvice가 MethodArgumentNotValidException 핸들러를 가지고 있는데, 이는 아키텍처가 CoreException으로 통일된 에러 처리를 요구하는 것과 충돌한다.

수정 방향:

  1. UsersSignUpRequestDto에서 모든 Bean Validation 어노테이션 제거
  2. UsersController에서 @Valid 제거
  3. 필드 검증은 User 엔티티 생성자나 SignUpCommand 생성 시점에서 수행하도록 이동
  4. UserFacade.signUp이 검증 실패 시 CoreException을 던지도록 보장
  5. ApiControllerAdvice에서 MethodArgumentNotValidException 핸들러 제거

테스트 재구성:

  • 입력 검증 실패 시 UserFacade를 목킹하여 CoreException(ErrorType.BAD_REQUEST)을 던지도록 구성
  • ApiControllerAdvice가 표준 에러 포맷을 반환하는지 검증하도록 변경
  • 중복 이메일, 비밀번호 정책 위반 등 주요 오류 코드별로 상태 코드와 에러 바디 형식이 일관되는지 확인하는 케이스 추가
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java`
around lines 71 - 407, Remove all Bean Validation annotations from
UsersSignUpRequestDto and remove `@Valid` from UsersController method signature so
DTOs are plain; move validation logic into the domain (User constructor or
SignUpCommand and PasswordPolicy) and ensure UserFacade.signUp throws
CoreException with ErrorType.BAD_REQUEST on validation failures; update tests in
UserControllerTest to mock UserFacade.signUp to throw
CoreException(ErrorType.BAD_REQUEST) for invalid inputs and assert
ApiControllerAdvice’s unified error response; finally remove the
MethodArgumentNotValidException handler from ApiControllerAdvice so only
CoreException handling remains.

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.

1 participant