Skip to content

Conversation

@leeminkyu-kr96
Copy link
Collaborator

@leeminkyu-kr96 leeminkyu-kr96 commented Nov 3, 2025

📌 Summary

  • 회원 (가입, 내 정보 조회) 및 포인트 (조회, 충전) 도메인에 대해 명시된 단위, 통합, E2E 테스트 케이스를 모두 구현하고 통과시켰습니다.

💬 리뷰 포인트

  • E2E 테스트 구현 방식: E2E 테스트 항목(HTTP 요청 검증)을 구현하는 것이 가장 어려웠습니다. (특히 TestRestTemplate), AI의 도움을 일부 받아 작성했는데, 이 접근 방식이 올바른지 중점적으로 리뷰 부탁드립니다.(아직 구조를 제대로 이해하지 못했습니다.)
  • Spring 아키텍처 관점: TDD를 진행하며, TDD 개념 자체보다 Spring 레이어드 아키텍처의 흐름에 대한 이해 부족이 더 큰 장벽이었다고 느꼈습니다. 혹시 계층 간 책임을 잘못 분배했거나 아키텍처 관점에서 어색한 부분이 있을까요?
  • 테스트 범위의 적절성: '어디까지 테스트해야 할까'가 가장 큰 고민이었습니다. 현재 작성한 단위/통합 테스트의 범위가 과제의 의도에 맞게 적절하게 작성되었는지 궁금합니다.

✅ Checklist

  • 테스트 코드 포함 (Quest의 모든 케이스 구현)
  • 불필요한 코드 제거 (디버깅 로그, 불필요한 주석 등)
  • README or 주석 보강 (필요 시)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 사용자 회원가입 API 엔드포인트 추가
    • 사용자 정보 조회 API 엔드포인트 추가
    • 포인트 조회 기능 추가
    • 포인트 충전 기능 추가
    • 사용자 ID, 이메일, 생년월일 유효성 검증 강화
    • 포인트 입력값 검증 추가 (최소 1 포인트 이상)
    • API 요청 헤더 및 본문 검증 오류 처리 개선

 - 개인 프로젝트에서 루퍼스프로젝트로 이전
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

개요

사용자 및 포인트 관리를 위한 도메인 계층, 서비스 계층, 애플리케이션 계층, 인프라 계층, API 계층의 완전한 아키텍처를 구현합니다. 사용자 회원가입/조회, 포인트 충전/조회 기능을 포함한 REST API 엔드포인트를 추가하고, 포괄적인 유닛/통합/E2E 테스트를 작성합니다.

변경 사항

응집도 / 파일(들) 변경 요약
도메인 엔티티
com/loopers/domain/user/UserModel.java, com/loopers/domain/point/PointModel.java
UserModel과 PointModel JPA 엔티티 추가. UserModel은 userId, email, birthDate 필드에 정규식 기반 검증 로직 포함. PointModel은 UserModel과의 다대일 관계, 포인트 충전/사용 메서드, 음수 검증 포함.
도메인 저장소 인터페이스
com/loopers/domain/user/UserRepository.java, com/loopers/domain/point/PointRepository.java
UserRepository (find, findById, save), PointRepository (findPoint, save) 인터페이스 정의.
도메인 서비스
com/loopers/domain/user/UserService.java, com/loopers/domain/point/PointService.java
UserService (getUser, signUp with CONFLICT 검증), PointService (findPoint, charge with 트랜잭션) 스프링 컴포넌트 추가.
인프라 저장소 구현
com/loopers/infrastructure/user/UserJpaRepository.java, com/loopers/infrastructure/user/UserRepositoryImpl.java, com/loopers/infrastructure/point/PointJpaRepository.java, com/loopers/infrastructure/point/PointRepositoryImpl.java
Spring Data JPA 저장소 및 구현체로 도메인 저장소 인터페이스 구현.
애플리케이션 파사드
com/loopers/application/user/UserFacade.java, com/loopers/application/point/PointFacade.java
UserFacade (signup, getUser), PointFacade (getPoint, chargePoint) 스프링 컴포넌트로 서비스 조율.
데이터 전송 객체
com/loopers/application/user/UserInfo.java, com/loopers/application/point/PointInfo.java
도메인 모델 변환용 UserInfo, PointInfo 레코드 및 from() 팩토리 메서드 추가.
API 명세 및 컨트롤러
com/loopers/interfaces/api/user/UserV1ApiSpec.java, com/loopers/interfaces/api/user/UserV1Controller.java, com/loopers/interfaces/api/point/PointV1ApiSpec.java, com/loopers/interfaces/api/point/PointV1Controller.java
Swagger 주석 포함한 API 명세 인터페이스 및 REST 컨트롤러 (POST /api/v1/users/signup, GET /api/v1/users/{userId}, GET /api/v1/points, POST /api/v1/points/charge).
API DTO
com/loopers/interfaces/api/user/UserV1Dto.java, com/loopers/interfaces/api/point/PointV1Dto.java
UserV1Dto (UserResponse, SignupRequest), PointV1Dto (PointResponse, ChargeRequest) 레코드 및 검증 주석 추가.
예외 처리
com/loopers/interfaces/api/ApiControllerAdvice.java
MissingRequestHeaderException, MethodArgumentNotValidException 핸들러 메서드 추가.
유닛 테스트
src/test/java/com/loopers/domain/user/UserModelTest.java, src/test/java/com/loopers/domain/point/PointModelTest.java
UserModel 및 PointModel 생성자 검증 테스트 (빈 입력, 형식 오류, 음수 등).
통합 테스트
src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java, src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java
UserService (회원가입, 중복 검증, 사용자 조회) 및 PointService (포인트 조회, 충전) 통합 테스트.
E2E 테스트
src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java, src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java
REST API 엔드포인트 E2E 테스트 (성공 경로, 실패 경로, 상태 코드 검증).

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant PointV1Controller
    participant PointFacade
    participant PointService
    participant UserService
    participant PointJpaRepository as PointRepository
    participant UserJpaRepository as UserRepository

    Client->>PointV1Controller: POST /api/v1/points/charge<br/>(X-USER-ID, amount)
    PointV1Controller->>PointFacade: chargePoint(userId, amount)
    
    PointFacade->>UserService: 사용자 존재 확인
    UserService->>UserRepository: find(userId)
    UserRepository-->>UserService: UserModel | null
    
    alt User Not Found
        UserService-->>PointFacade: throw NOT_FOUND
        PointFacade-->>PointV1Controller: CoreException
        PointV1Controller-->>Client: 404 Not Found
    else User Found
        PointFacade->>PointService: charge(PointModel)
        
        PointService->>UserRepository: find(user)
        UserRepository-->>PointService: UserModel
        
        PointService->>PointRepository: findByUser(user)
        PointRepository-->>PointService: PointModel | empty
        
        alt Point Exists
            PointService->>PointService: 포인트 누적
        else Point Not Exists
            PointService->>PointService: 새 PointModel 생성
        end
        
        PointService->>PointRepository: save(pointModel)
        PointRepository-->>PointService: PointModel
        
        PointService-->>PointFacade: 완료
        PointFacade->>PointService: findPoint(user)
        PointService-->>PointFacade: PointModel
        PointFacade->>PointFacade: PointInfo로 변환
        PointFacade-->>PointV1Controller: PointInfo
        PointV1Controller->>PointV1Controller: PointV1Dto.PointResponse로 변환
        PointV1Controller-->>Client: 200 OK + response
    end
Loading

예상 코드 리뷰 노력

🎯 3 (중간) | ⏱️ ~25 분

특히 주의가 필요한 영역:

  • UserModel, PointModel 검증 로직: 정규식 기반 입력 검증 규칙 및 예외 처리 방식이 일관성 있게 구현되었는지 확인 필요
  • PointService.charge() 트랜잭션 경계: 포인트 생성 vs 업데이트 로직이 올바르게 처리되는지 검증 필요
  • UserService 중복 사용자 검증: CONFLICT 에러 처리 및 signup 흐름이 정확한지 확인
  • API 계층 E2E 테스트: 모든 엔드포인트와 에러 케이스가 적절히 커버되는지 검토 필요

관련 가능성 있는 PR

시 🐰

사용자와 포인트를 관리하는

아름다운 아키텍처 🏗️

도메인부터 API까지,

계층별로 쌓아올렸네! 🎯

테스트도 견고하게,

품질을 지킨다 ✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[volume-1] TDD 기반 1주차 과제 (회원, 포인트) 기능 구현'은 변경 사항의 핵심 내용을 명확하게 반영하고 있습니다. 회원과 포인트 도메인에 대한 TDD 기반 기능 구현이라는 주요 변경 내용을 간결하고 구체적으로 표현하고 있으며, 코드 리스트나 불필요한 표현이 없어 읽기 쉽습니다. 제목이 전체 변경 사항의 범위를 적절히 대표하고 있습니다.
Description check ✅ Passed PR 설명이 저장소의 템플릿 구조(Summary, Review Points, Checklist, References)를 잘 따르고 있습니다. Summary 섹션에서 구현한 기능을 명확히 설명하고, Review Points 섹션에서 E2E 테스트 구현 방식, Spring 아키텍처, 테스트 범위의 적절성에 대해 구체적인 검토 요청을 제시하고 있습니다. Checklist도 작성되어 있어 완성도 높은 설명입니다. References 섹션은 해당 작업의 특성상 필요하지 않아 작성되지 않았는데, 이는 템플릿 가이드라인을 따르고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

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: 6

🧹 Nitpick comments (18)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1)

59-65: 검증 에러 처리가 정상적으로 동작합니다.

MethodArgumentNotValidException 핸들러가 올바르게 구현되어 있으며, 필드 검증 실패 시 적절한 에러 메시지를 반환합니다.

다만, 다음 사항을 고려해볼 수 있습니다:

  • 여러 검증 오류가 발생할 경우 모든 메시지를 ", "로 연결하면 클라이언트가 개별 필드 오류를 구분하기 어려울 수 있습니다
  • 향후 프론트엔드 요구사항에 따라 필드별 구조화된 에러 응답을 고려할 수 있습니다

현재 구현은 기본적인 사용 사례에 충분하며, 필요시 추후 개선할 수 있습니다.

참고: null 안전성 향상을 위해 다음과 같이 개선할 수도 있습니다 (선택사항):

     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
         String errorMessage = e.getBindingResult().getFieldErrors().stream()
-            .map(FieldError::getDefaultMessage)
+            .map(error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "검증 오류")
             .collect(Collectors.joining(", "));
         return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
     }
apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1)

6-9: 애플리케이션 DTO에서 엔티티를 직접 노출하지 않는 편이 좋겠습니다.
현재 PointInfo가 UserModel을 그대로 들고 있어 애플리케이션 레이어에서 영속 엔티티가 노출되고 있습니다. 상위 계층(API DTO, 테스트 등)에서 실수로 엔티티를 재사용하면 지연 로딩 문제나 의도치 않은 상태 변경이 발생해 레이어 경계가 흐려집니다. 필요한 값(userId 등)만 복사하거나 UserInfo 같은 얇은 뷰 모델을 별도로 두는 방향을 검토해 주세요.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java (1)

15-18: Swagger 요청 본문 정의가 누락되어 문서가 어색해질 수 있습니다.
인터페이스 파라미터에 @RequestBody(또는 @parameter) 없이 @Schema만 사용하면 springdoc-openapi가 요청 본문으로 인식하지 못해 Swagger UI에 query parameter처럼 표시될 수 있습니다. 문서를 인터페이스에서 관리하려면 다음과 같이 명시해 주세요.

-import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
 ...
-    ApiResponse<UserV1Dto.UserResponse> signup(
-        @Schema(name = "회원가입 요청", description = "회원가입할 유저의 정보")
-        UserV1Dto.SignupRequest request
-    );
+    ApiResponse<UserV1Dto.UserResponse> signup(
+        @RequestBody(
+            description = "회원가입할 유저의 정보"
+        )
+        UserV1Dto.SignupRequest request
+    );
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (3)

13-13: 미사용 import를 제거하세요.

java.time.LocalDate가 import되어 있지만 사용되지 않습니다.


16-16: DisplayName이 불완전합니다.

"포인트 " 다음에 설명이 누락되어 있습니다. 예: "포인트 생성" 또는 "포인트 검증"과 같이 명확한 이름을 지정하세요.


34-35: 미완성 코드를 정리하세요.

주석 처리된 "//포인트 사용하기"는 향후 구현 예정인 것으로 보입니다. TODO 주석으로 변경하거나 제거하는 것이 좋습니다.

apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)

30-31: 파일 끝의 빈 줄을 정리하세요.

파일 끝에 불필요한 빈 줄이 있습니다.

apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (3)

7-7: 미사용 import를 제거하세요.

org.apache.catalina.User가 import되어 있지만 사용되지 않습니다.


12-12: 미사용 import를 제거하세요.

org.springdoc.api.ErrorMessage가 import되어 있지만 사용되지 않습니다.


17-19: 미사용 static import를 정리하세요.

Mockito의 any, verify, times import는 사용되지만, 파일에서 실제로 필요한 것만 남기고 정리하는 것이 좋습니다. Line 67에서만 사용되고 있습니다.

apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)

26-27: 미사용 필드를 제거하세요.

PointJpaRepository 필드가 선언되어 있지만 테스트에서 사용되지 않습니다.


83-97: 포인트 충전 테스트 커버리지를 확장하는 것을 권장합니다.

현재는 존재하지 않는 유저에 대한 실패 케이스만 테스트하고 있습니다. 다음 케이스를 추가하면 더 완전한 테스트가 됩니다:

  • 새로운 유저(포인트 레코드 없음)에게 충전하는 경우
  • 기존 포인트가 있는 유저에게 추가 충전하는 경우 (누적 검증)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (1)

78-80: 반복되는 ParameterizedTypeReference 생성을 리팩토링하세요.

동일한 ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> 생성 코드가 Lines 78-80, 99-101, 128-130, 152-154에서 반복됩니다.

테스트 클래스의 상수로 추출하면 가독성이 향상됩니다:

+    private static final ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> POINT_RESPONSE_TYPE = 
+        new ParameterizedTypeReference<>() {};
+
     @DisplayName("포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.")
     @Test
     void returnsPoint_whenValidUserIdHeaderIsProvided() {
         // ...
         
-        ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
         ResponseEntity<ApiResponse<PointV1Dto.PointResponse>> response =
-            testRestTemplate.exchange(ENDPOINT_GET, HttpMethod.GET, new HttpEntity<>(headers), responseType);
+            testRestTemplate.exchange(ENDPOINT_GET, HttpMethod.GET, new HttpEntity<>(headers), POINT_RESPONSE_TYPE);

동일한 패턴을 다른 테스트 메서드에도 적용하세요.

apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (2)

18-31: 레이어 간 책임 분리를 개선할 수 있습니다.

현재 PointFacadePointService 모두에서 사용자 존재 여부를 검증하고 있어 중복이 발생합니다:

  • Facade의 Line 19-22: 사용자 검증
  • Service의 findPoint (PointService.java Line 22-25): 동일한 검증 수행

또한 Line 23에서 조회를 위해 new PointModel(user, 0)으로 더미 값을 전달하는 것은 API 설계상 어색합니다. PointModel 생성자는 포인트 충전/사용을 위한 금액을 받도록 설계되었는데, 조회 시 0을 전달하는 것은 의미상 맞지 않습니다.

다음 중 하나의 방식으로 개선을 고려해보세요:

방안 1 (권장): PointService.findPoint의 시그니처를 변경하여 userId만 받도록 수정

-public PointModel findPoint(PointModel point) {
-    UserModel requestUser = point.getUser();
-    var foundUser = userRepository.find(requestUser.getUserId());
+public PointModel findPoint(String userId) {
+    var foundUser = userRepository.find(userId);
     if (foundUser.isEmpty()) {
         return null;
     }
     return pointRepository.findPoint(foundUser.get()).orElse(null);
}

그리고 Facade에서:

 public PointInfo getPoint(String userId) {
-    UserModel user = userService.getUser(userId);
-    if (user == null) {
-        throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
-    }
-    PointModel pointModel = new PointModel(user, 0);
-    PointModel point = pointService.findPoint(pointModel);
+    PointModel point = pointService.findPoint(userId);
     
     if (point == null) {
         throw new CoreException(ErrorType.NOT_FOUND, "포인트 정보가 없습니다.");
     }
     
     return PointInfo.from(point);
}

방안 2: UserService의 검증 로직을 신뢰하고 PointService에서는 사용자 검증을 제거하여 단일 책임 원칙을 준수


33-43: 중복된 사용자 검증 로직을 제거하고 개선할 수 있습니다.

Lines 34-37의 사용자 검증 로직이 getPoint 메서드(Lines 19-22)와 완전히 동일하며, PointService.charge(PointService.java Line 32-33)에서도 동일한 검증을 수행합니다.

또한 Line 41에서 충전 후 재조회를 위해 다시 new PointModel(user, 0)을 생성하는데, 이는 불필요한 객체 생성입니다.

개선 방안:

 public PointInfo chargePoint(String userId, int amount) {
-    UserModel user = userService.getUser(userId);
-    if (user == null) {
-        throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다.");
-    }
-    PointModel pointModel = new PointModel(user, amount);
-    pointService.charge(pointModel);
+    pointService.charge(userId, amount);
     
-    PointModel charged = pointService.findPoint(new PointModel(user, 0));
+    PointModel charged = pointService.findPoint(userId);
     return PointInfo.from(charged);
}

이렇게 하려면 PointService.charge의 시그니처도 변경이 필요합니다:

-public void charge(PointModel point) {
-    UserModel user = point.getUser();
-    var foundUser = userRepository.find(user.getUserId())
+public void charge(String userId, int amount) {
+    var foundUser = userRepository.find(userId)
         .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저가 존재하지 않습니다."));
   
     var existing = pointRepository.findPoint(foundUser);
     if (existing.isPresent()) {
-        existing.get().charge(point.getPoint());
+        existing.get().charge(amount);
         pointRepository.save(existing.get());
         return;
     }
-    pointRepository.save(new PointModel(foundUser, point.getPoint()));
+    pointRepository.save(new PointModel(foundUser, amount));
}

이렇게 하면 Facade 레이어에서 불필요한 UserModel 조회와 PointModel 생성을 제거할 수 있습니다.

apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (3)

18-22: TODO 주석을 제거하거나 업데이트하세요.

Lines 18-22의 체크리스트 항목들은 이미 아래 테스트들로 구현되었으므로 주석을 제거하거나 완료 표시로 업데이트하는 것이 좋습니다.


24-24: 주석의 오타를 수정하세요.

"입력한 아이디가 빈칸칸이거나" → "입력한 아이디가 빈칸이거나"


14-122: 검증 실패 케이스는 잘 커버되었으나, 정상 케이스 테스트가 누락되었습니다.

현재 테스트는 모두 검증 실패 케이스만 다루고 있습니다. 올바른 입력값으로 UserModel 객체가 성공적으로 생성되는 정상 케이스 테스트를 추가하는 것을 권장합니다.

예시:

@DisplayName("유효한 입력값으로 User 객체 생성에 성공한다.")
@Test
void createsUserModel_whenAllInputsAreValid() {
    // arrange & act
    UserModel userModel = new UserModel("user123", "user123@example.com", "1999-01-01");
    
    // assert
    assertAll(
        () -> assertThat(userModel.getUserId()).isEqualTo("user123"),
        () -> assertThat(userModel.getEmail()).isEqualTo("user123@example.com"),
        () -> assertThat(userModel.getBirthDate()).isEqualTo("1999-01-01")
    );
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9ae7ad and 369c10d.

📒 Files selected for processing (27)
  • apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-31T14:57:08.821Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 22
File: apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java:24-34
Timestamp: 2025-10-31T14:57:08.821Z
Learning: In the loopers-spring-java-template repository, the X-USER-ID header-based user identification is temporarily used for learning purposes and will be replaced with proper authentication (Spring Security) in a future PR. Do not flag this as a security issue until proper authentication is implemented.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
📚 Learning: 2025-10-31T02:20:33.770Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 15
File: core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java:16-27
Timestamp: 2025-10-31T02:20:33.770Z
Learning: In UserIdentifier and similar value objects, when the constructor performs only null-checking while the static create() method performs full validation (regex, length, etc.), this is an intentional pattern for schema evolution. The constructor is used by the persistence layer to reconstruct domain objects from the database (no validation needed for already-validated legacy data), while create() is used by the application layer to create new domain objects (with validation for new data). This allows backward compatibility when validation rules change in production without requiring migration of all existing database records.

Applied to files:

  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java
🧬 Code graph analysis (16)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
  • Entity (12-57)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
  • DisplayName (43-81)
  • DisplayName (83-97)
  • SpringBootTest (21-98)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
  • DisplayName (14-122)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
  • PointV1Dto (6-20)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
  • SpringBootTest (26-161)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
  • RequiredArgsConstructor (11-38)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (10-28)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (16-36)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (3)
  • DisplayName (52-93)
  • DisplayName (95-129)
  • SpringBootTest (25-130)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (5)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
  • RequiredArgsConstructor (10-37)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
  • Entity (9-59)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
  • RequiredArgsConstructor (11-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
  • RequiredArgsConstructor (10-32)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
  • UserV1Dto (6-27)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (3)
  • DisplayName (62-109)
  • DisplayName (111-162)
  • SpringBootTest (23-163)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
  • DisplayName (16-36)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
  • DisplayName (43-81)
  • DisplayName (83-97)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
  • DisplayName (52-93)
  • DisplayName (95-129)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
  • DisplayName (14-122)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
  • RequiredArgsConstructor (10-28)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
  • UserV1Dto (6-27)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
  • RequiredArgsConstructor (12-44)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
  • RequiredArgsConstructor (12-43)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
  • RequiredArgsConstructor (11-31)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
  • PointV1Dto (6-20)
🔇 Additional comments (21)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2)

11-13: LGTM! Import 문이 적절하게 추가되었습니다.

새로운 예외 핸들러에서 사용하는 클래스들이 올바르게 import되었습니다.


52-57: LGTM! 헤더 누락 예외 처리가 적절합니다.

MissingRequestHeaderException 핸들러가 기존 핸들러들과 동일한 패턴으로 구현되어 일관성이 있으며, 명확한 에러 메시지를 제공합니다.

apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java (1)

5-12: 단순하면서도 명확한 DTO 변환입니다

레코드로 DTO를 구성하고, 팩토리 메서드에서 도메인 엔티티를 그대로 매핑한 구조가 계층 간 책임을 잘 분리해 줍니다.

apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java (1)

9-11: 필요한 조회 메서드를 정확히 노출했습니다

사용자 단위 포인트 조회가 도메인 요구사항과 정확히 맞물려 있고, Spring Data JPA 메서드 시그니처도 적절하게 정의되어 있습니다.

apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (1)

56-92: 회원가입 통합 테스트가 잘 작성되었습니다.

회원가입 시나리오가 포괄적으로 테스트되고 있습니다:

  • 저장 동작 spy 검증
  • 중복 ID 검증

테스트 커버리지가 적절합니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (2)

16-19: 충전 금액 검증 정책을 확인하세요.

ChargeRequest@Min(value = 1)로 최소 1 이상을 요구하지만, PointModel의 생성자와 charge 메서드는 0 이상을 허용합니다.

  • API 레벨에서 0원 충전을 막는 것이 의도라면 일관성이 있습니다.
  • 그러나 도메인 모델과 API 검증 정책이 다르므로, 이것이 의도된 설계인지 확인이 필요합니다.

비즈니스 요구사항에 따라 0원 충전이 허용되어야 한다면 @Min(value = 0)으로 변경하거나, 도메인 모델에서도 1 이상만 허용하도록 통일하세요.


7-14: DTO 구조가 잘 설계되었습니다.

Record와 static factory 메서드를 사용한 깔끔한 DTO 설계입니다.

apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)

23-30: 생성자 검증이 올바르게 구현되었습니다.

음수 포인트를 차단하는 검증 로직이 적절합니다.

apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (1)

43-80: 포인트 조회 테스트가 잘 작성되었습니다.

존재하는 유저와 존재하지 않는 유저에 대한 시나리오를 모두 커버하고 있습니다.

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

62-109: E2E 테스트 구현이 올바르게 작성되었습니다.

PR 설명에서 언급하신 E2E 테스트 구현 방식에 대한 피드백:

잘된 점:

  • TestRestTemplateParameterizedTypeReference를 사용한 접근법이 정확합니다.
  • 헤더 설정, 요청/응답 매핑이 적절합니다.
  • 2xx, 4xx 상태 코드 검증이 올바릅니다.
  • @SpringBootTest(webEnvironment = RANDOM_PORT)를 사용한 실제 HTTP 요청 테스트가 적합합니다.

이것이 Spring Boot에서 권장하는 E2E 테스트 방식입니다.


111-162: 포인트 충전 E2E 테스트가 포괄적입니다.

성공 케이스와 실패 케이스(존재하지 않는 유저)를 모두 검증하고 있습니다.

apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (2)

20-27: 포인트 조회 로직이 올바르게 구현되었습니다.

사용자 검증 후 포인트를 조회하는 흐름이 적절합니다. 다만 findPoint가 사용자가 없는 경우와 포인트가 없는 경우 모두 null을 반환하여 구분할 수 없는데, 현재 요구사항에서는 문제가 되지 않습니다. 향후 세밀한 에러 처리가 필요하다면 별도 예외를 던지는 것을 고려할 수 있습니다.


30-42: 충전 로직이 올바르게 구현되었습니다.

기존 포인트가 있으면 업데이트하고, 없으면 새로 생성하는 로직이 적절합니다. Line 37의 charge() 메서드를 통한 도메인 로직 캡슐화도 좋은 설계입니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)

29-37: 사용자 조회 엔드포인트가 올바르게 구현되었습니다.

경로 변수로 userId를 받아 처리하는 구조가 RESTful 설계 원칙에 부합합니다.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (4)

26-50: E2E 테스트 설정이 올바르게 구현되었습니다.

TestRestTemplate 사용 방식과 DatabaseCleanUp을 통한 테스트 격리가 적절합니다. PR 설명에서 E2E 테스트 구현이 어려웠다고 하셨는데, 실제 구현은 매우 잘 되어 있습니다. RANDOM_PORT 설정과 @AfterEach에서의 데이터 정리는 E2E 테스트의 모범 사례입니다.


67-89: 회원가입 성공 테스트가 잘 작성되었습니다.

ParameterizedTypeReference를 사용한 제네릭 타입 처리와 assertAll을 통한 다중 검증이 적절합니다.


93-112: 필수 필드 누락 테스트가 의도와 다르게 동작할 수 있습니다.

이 테스트는 gender 필드가 없을 때 400 응답을 기대하는데, UserV1Controller에서 gender가 실제로 사용되지 않는 문제와 연관됩니다. gender 필드의 처리 방향이 결정되면 이 테스트도 함께 검토가 필요합니다.

UserV1Controller.java의 gender 필드 처리 방향에 따라 이 테스트를 업데이트하세요:

  • gender를 저장하기로 했다면: 테스트 유지
  • gender를 제거하기로 했다면: 다른 필수 필드(예: userId, email)로 테스트 변경

120-159: 사용자 조회 관련 E2E 테스트가 성공/실패 케이스를 모두 커버하고 있습니다.

존재하는 사용자 조회와 존재하지 않는 사용자 조회 모두 테스트하여 엔드포인트의 동작을 충분히 검증하고 있습니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1)

9-31: API 명세가 명확하고 잘 문서화되었습니다.

Swagger 애노테이션을 통한 API 문서화가 적절하며, X-USER-ID 헤더 파라미터에 대한 설명도 명확합니다. 인터페이스를 통한 API 계약 정의는 좋은 설계 패턴입니다.

apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)

13-25: 리포지토리 구현이 올바른 레이어 분리 패턴을 따르고 있습니다.

도메인 인터페이스(PointRepository)를 인프라스트럭처 레이어에서 구현하고, JPA 리포지토리에 위임하는 구조가 적절합니다. 이는 도메인 레이어가 인프라에 의존하지 않도록 하는 클린 아키텍처 원칙에 부합합니다.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)

17-36: 컨트롤러 구현이 적절한 레이어 책임을 따르고 있습니다.

컨트롤러는 HTTP 요청/응답 처리만 담당하고, 비즈니스 로직은 PointFacade에 위임하는 구조가 올바릅니다. X-USER-ID 헤더를 통한 사용자 식별과 @Valid를 통한 요청 검증도 적절합니다.

Comment on lines +47 to +56
public void use(int amount) {

if (amount < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0보다 커야 합니다.");
}
if (point < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "포인트는 0 이상이어야 합니다.");
}
this.point -= amount;
}
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

use 메서드에 잔액 부족 검증이 누락되었습니다.

현재 구현의 문제점:

  1. Critical: Line 55에서 this.point -= amount를 실행하기 전에 point >= amount 검증이 없습니다. 잔액이 부족해도 차감되어 음수가 될 수 있습니다.
  2. Minor: Line 49-51의 에러 메시지가 "사용 금액은 0보다 커야 합니다."인데, 실제 검증은 amount < 0으로 0 이상을 허용합니다. 메시지를 "사용 금액은 0 이상이어야 합니다."로 수정하세요.
  3. Optional: Line 52-54의 point < 0 검증은 불필요합니다. 생성자와 charge에서 이미 음수를 차단하므로 point는 항상 0 이상입니다.

다음 diff를 적용하세요:

 public void use(int amount) {
 
     if (amount < 0) {
-        throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0보다 커야 합니다.");
+        throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0 이상이어야 합니다.");
     }
-    if (point < 0) {
-        throw new CoreException(ErrorType.BAD_REQUEST, "포인트는 0 이상이어야 합니다.");
+    if (this.point < amount) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "포인트 잔액이 부족합니다.");
     }
     this.point -= amount;
 }
📝 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
public void use(int amount) {
if (amount < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0보다 커야 합니다.");
}
if (point < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "포인트는 0 이상이어야 합니다.");
}
this.point -= amount;
}
public void use(int amount) {
if (amount < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액은 0 이상이어야 합니다.");
}
if (this.point < amount) {
throw new CoreException(ErrorType.BAD_REQUEST, "포인트 잔액이 부족합니다.");
}
this.point -= amount;
}
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java
around lines 47 to 56, add a balance-check before subtracting so the method
validates that amount is non-negative and that point >= amount (throw
CoreException with an appropriate ErrorType and message on failure), change the
error message for the amount check from "사용 금액은 0보다 커야 합니다." to "사용 금액은 0 이상이어야
합니다.", and remove the redundant point < 0 check (since constructor/charge
already prevent negative balances); then perform this.point -= amount.

Comment on lines +9 to +10
@Entity
@Table(name = "user")
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

예약어 테이블명으로 인한 SQL 오류 가능성

user는 PostgreSQL, MySQL, H2 등에서 예약어라서 Hibernate가 select ... from user 같은 쿼리를 생성할 때 즉시 SQLSyntaxError 가 발생합니다. 테스트나 운영 DB 어느 쪽이든 영향을 받으니 테이블명을 users 같이 예약어가 아닌 이름으로 바꾸거나, 반드시 해당 이름을 써야 한다면 @Table(name = "\"user\"")처럼 ANSI 인용을 강제해야 합니다.

-@Table(name = "user")
+@Table(name = "users")
📝 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
@Entity
@Table(name = "user")
@Entity
@Table(name = "users")
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java around
lines 9-10, the entity is mapped to the reserved table name "user" which can
cause SQLSyntaxError on PostgreSQL/MySQL/H2; change the mapping to a
non-reserved name (e.g., @Table(name = "users")) or, if you must keep "user",
force ANSI quoting (e.g., @Table(name = "\"user\"")), and then update any
related JPQL/native queries, migrations, and tests to use the new table name.

Comment on lines +23 to +30
public UserModel signUp(UserModel userModel) {
Optional<UserModel> user = userRepository.find(userModel.getUserId());

if (user.isPresent()) {
throw new CoreException(ErrorType.CONFLICT, "[userId = " + userModel.getUserId() + "] 아이디가 중복되었습니다.");
}
return userRepository.save(userModel);
}
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

동시 가입 시 중복 체크-저장 사이 경쟁 상태가 발생합니다.
현재는 userRepository.find(...)로 중복을 확인한 뒤 save를 호출하지만, 두 요청이 동시에 들어오면 둘 다 Optional이 비어 있다고 판단하고 각각 save를 시도할 수 있습니다. DB에 userId 유니크 제약이 없다면 중복 레코드가 쌓이고, 제약이 있더라도 DataIntegrityViolationException이 발생해 500으로 튈 위험이 큽니다. 저장 구간을 예외 처리로 감싸 CONFLICT로 변환하고, userId에 유니크 인덱스를 두는 식으로 방어 로직을 추가해 주세요.

+import org.springframework.dao.DataIntegrityViolationException;
...
     public UserModel signUp(UserModel userModel) {
         Optional<UserModel> user = userRepository.find(userModel.getUserId());
 
         if (user.isPresent()) {
             throw new CoreException(ErrorType.CONFLICT, "[userId = " + userModel.getUserId() + "] 아이디가 중복되었습니다.");
         }
-        return userRepository.save(userModel);
+        try {
+            return userRepository.save(userModel);
+        } catch (DataIntegrityViolationException e) {
+            throw new CoreException(ErrorType.CONFLICT, "[userId = " + userModel.getUserId() + "] 아이디가 중복되었습니다.", e);
+        }
     }
🤖 Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
around lines 23 to 30, there is a race between the find() duplicate check and
save() that can produce duplicate users or a 500 on constraint violation; wrap
the save call in a try/catch that catches the persistence exception thrown on
unique-constraint violations (e.g., DataIntegrityViolationException /
PersistenceException) and translate it into a CoreException with
ErrorType.CONFLICT and a descriptive message including the userId, and also
ensure the userId has a unique index at the DB/schema level (add a unique
constraint in the JPA entity or DB migration) so the service is protected even
under concurrent requests.

Comment on lines +20 to +23
@Override
public Optional<UserModel> findById(Long id) {
return Optional.empty();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

findById 구현이 플레이스홀더입니다.

findById 메서드가 항상 Optional.empty()를 반환합니다. 이 메서드가 실제로 사용되지 않는다면 문제없지만, 향후 사용 시 버그가 될 수 있습니다. 현재 과제 범위에서 필요하지 않다면 TODO 주석을 추가하거나, 사용할 계획이라면 올바른 구현을 제공하세요.


🏁 Script executed:

#!/bin/bash
# Description: findById 메서드가 실제로 호출되는지 확인

# UserRepository.findById 호출을 검색
rg -nP --type=java '\bfindById\s*\(' -A 2 -B 2

Length of output: 1726


Let me search the codebase more specifically for how UserRepository is being used.

Let me search for where UserRepository is injected and used in the codebase:

This will help identify which classes are using UserRepository and what methods they're calling.


🏁 Script executed:

#!/bin/bash
# Search for UserRepository bean injection and usage
rg -nP --type=java '(UserRepository|@Autowired.*User|userRepository\s*\.)' -A 2 -B 2 | head -100

Length of output: 10177


🏁 Script executed:

#!/bin/bash
# Search for UserRepository injection points and method calls
rg -nP --type=java 'private.*UserRepository|@Autowired' -A 3 | head -80

Length of output: 7245


🏁 Script executed:

#!/bin/bash
# Search for all Java files in the codebase
fd -e java --type f | head -20

Length of output: 1593


🏁 Script executed:

#!/bin/bash
# Search for Service or Controller classes that might use UserRepository
rg -nP --type=java '@Service|@Component|@Controller' -l | head -20

Length of output: 974


UserRepository.findById는 사용되지 않는 플레이스홀더입니다. 인터페이스에서 제거하거나, 구현하거나, TODO 주석을 추가하세요.

코드베이스를 검색한 결과, UserRepository.findById() 메서드는 어디에도 호출되지 않습니다. UserService에서는 find(userId)save() 메서드만 사용되며, 테스트 파일에서도 findById를 호출하는 곳이 없습니다. 현재 이 메서드는 항상 Optional.empty()를 반환하는 플레이스홀더 상태이므로, 다음 중 하나를 수행해야 합니다:

  • 인터페이스와 구현에서 제거
  • 필요한 경우 실제 구현 제공
  • 향후 사용 계획이 있으면 TODO 주석 추가
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
around lines 20-23, the findById(Long) method is a placeholder that always
returns Optional.empty() and is not used anywhere; either remove it from the
UserRepository interface and this implementation (also remove unused imports and
update any references), or if you intend to keep it, replace the body with a
proper implementation that queries the underlying data source and returns
Optional.ofNullable(result), or at minimum add a TODO comment explaining why
it’s a stub and when it will be implemented; pick one of these actions and apply
it consistently to both the interface and implementation so the codebase remains
clean and compiles.

Comment on lines +18 to +27
@PostMapping("/signup")
@ResponseStatus(HttpStatus.CREATED)
@Override
public ApiResponse<UserV1Dto.UserResponse> signup(
@Valid @RequestBody UserV1Dto.SignupRequest request
) {
UserInfo info = userFacade.signup(request.userId(), request.email(), request.birthDate());
UserV1Dto.UserResponse response = UserV1Dto.UserResponse.from(info);
return ApiResponse.success(response);
}
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

필수 입력 필드인 gender가 처리되지 않고 있습니다.

Line 24에서 request.userId(), request.email(), request.birthDate()만 전달하고 있으며, request.gender()는 검증은 되지만 사용되지 않습니다. UserV1Dto.SignupRequest에서 gender를 @NotBlank로 필수 검증하고 있지만(UserV1Dto.java Line 17), 실제로는 저장되지 않아 사용자에게 혼란을 줄 수 있습니다.

다음 중 하나를 선택하세요:

  1. gender를 실제로 저장한다면, UserFacade와 UserModel에 gender 파라미터 추가
-UserInfo info = userFacade.signup(request.userId(), request.email(), request.birthDate());
+UserInfo info = userFacade.signup(request.userId(), request.email(), request.gender(), request.birthDate());
  1. gender를 저장하지 않는다면, 요청 DTO에서 gender 필드와 검증 제거
 public record SignupRequest(
     @NotBlank(message = "userId는 필수입니다.")
     String userId,
     @NotBlank(message = "email은 필수입니다.")
     String email,
-    @NotBlank(message = "gender는 필수입니다.")
-    String gender,
     @NotBlank(message = "birthDate는 필수입니다.")
     String birthDate
 ) {}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +17 to +26
public record SignupRequest(
@NotBlank(message = "userId는 필수입니다.")
String userId,
@NotBlank(message = "email은 필수입니다.")
String email,
@NotBlank(message = "gender는 필수입니다.")
String gender,
@NotBlank(message = "birthDate는 필수입니다.")
String birthDate
) {}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

SignupRequest의 gender 필드가 사용되지 않습니다.

SignupRequestgender 필드가 있지만, UserModel 생성자는 userId, email, birthDate만 받습니다.

  • 향후 사용을 위해 수집하는 것이라면 주석으로 명시하세요.
  • 현재 과제에서 필요없다면 제거하는 것이 좋습니다.
  • 실제로 저장해야 한다면 UserModel에 gender 필드를 추가하세요.

🏁 Script executed:

#!/bin/bash
# Find UserModel class definition and its constructor
find . -type f -name "*.java" | head -20
rg -n "class UserModel" --type=java -A 20

Length of output: 6317


🏁 Script executed:

#!/bin/bash
# Search for SignupRequest usage and gender field references
rg -n "SignupRequest" --type=java -B 2 -A 5

Length of output: 4842


🏁 Script executed:

#!/bin/bash
# Search for mapper or service methods that might process gender
rg -n "gender" --type=java -B 3 -A 3

Length of output: 1819


🏁 Script executed:

#!/bin/bash
# Check the UserV1Dto file content
find . -path "*/user/UserV1Dto.java" -exec cat {} \;

Length of output: 784


SignupRequestgender 필드를 처리하세요.

검증 결과, gender 필드가 클라이언트로부터 수집되고 @notblank로 검증되지만, UserV1Controller에서 userFacade.signup()을 호출할 때 전달되지 않으며, UserModel에도 해당 필드가 없습니다.

다음 중 하나를 선택하세요:

  • gender 필드를 UserModel에 추가하고 저장 로직을 구현
  • SignupRequest에서 gender 필드 제거
  • 향후 사용 목적이라면 명확한 주석 추가
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
around lines 17-26, the SignupRequest includes a validated gender field that is
never passed into userFacade.signup() nor stored in UserModel; either add gender
to the domain and persistence layers and propagate it through the controller to
userFacade.signup(), updating UserModel, mapper/DTO conversions, repository save
logic and any database schema if needed, or remove the gender field from
SignupRequest (and its validation) if not used; if you intend to keep it for
future use, add a clear TODO comment explaining why it's not yet persisted and
link to the ticket that will implement storage.

hyujikoh added a commit that referenced this pull request Dec 14, 2025
refactor(order): 주문 및 결제 이벤트 핸들러 리팩토링
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