Skip to content

Conversation

@minor7295
Copy link
Collaborator

@minor7295 minor7295 commented Oct 31, 2025

📌 Summary

  • 회원 도메인 및 API 구현
    • User 도메인/리포지토리/서비스/컨트롤러 구현
    • 회원가입/회원정보 조회 API + 통합/E2E 테스트 추가
    • 회원가입 처리 로직을 SignUpFacade로 리팩토링
  • 포인트 도메인 및 API 구현
    • Point 도메인/리포지토리/서비스/컨트롤러 구현
    • 포인트 조회/충전 API + 단위/통합/E2E 테스트 추가

💬 Review Points

  • User Id 네이밍 일관성
    현재 사용자 PK(Long)와 비즈니스 식별자(String)가 모두 “id”로 혼용될 가능성이 있어 보입니다. 현재 PR은 User에서는 비즈니스 식별자를 userId로, Point에서는 PK를 userId로 사용하는 방식으로 구성되어있는데요, 혼란을 방지하기 위해 userName같은 다른 이름으로 구분하는 게 나을까요? 아니면 과제에 명시된 것을 따라 지금 방식을 유지하는 것이 더 나을지 의견 여쭙습니다.

  • User와 Point의 외래키 설정
    Point가 User모델은 참조하되, 외래키로 설정은 하지 않도록 했습니다. 데이터가 많이 쌓이고 DB에 파티셔닝을 해야하는 경우 외래키를 설정할 수 없는 것으로 알고 있어서 이렇게 처리했는데요, 실무 환경에서 보통 어떻게 설정하시는지 궁금합니다.

  • 픽스처와 테스트 가독성
    현재 사용자 생성 로직이 반복되어 UserTestFixture로 공통 데이터를 사용하고 있습니다. 그런데 단위/통합/E2E 전반에서 유사 데이터로만 검증하다 보니 경계·예외 케이스(미존재 사용자, 성별 누락/유효값, 최대 길이, 중복 가입 등)를 충분히 커버하지 못할 우려가 있습니다. 이번 과제 관점에서 어느 수준까지 케이스를 분기(정상/경계/에러)하는 것이 적정한지, 레벨별(단위/통합/E2E)로 픽스처를 구분해 최소 공용화하는 전략에 대한 피드백 부탁드립니다.

✅ Checklist

  • 테스트는 모두 통과해야 하며, @Test 기반으로 명시적으로 작성
  • 각 테스트는 테스트 명/설명/입력/예상 결과가 분명해야 함
  • E2E 테스트는 실제 HTTP 요청을 통해 흐름을 검증할 것

📎 References

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 사용자 회원가입 기능 추가
    • 포인트 시스템 구현 (포인트 조회 및 충전 기능)
    • 사용자 정보 조회 API 추가
    • 성별 정보 관리 기능 추가
  • 개선사항

    • 요청 헤더 검증 오류 처리 강화
    • 입력 데이터 유효성 검증 강화
    • API 응답 구조 표준화
  • 테스트

    • 통합 테스트 및 엔드-투-엔드 테스트 추가
    • 도메인 로직 단위 테스트 추가

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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.

워크스루

사용자 가입, 개인정보 조회, 포인트 충전 기능을 갖춘 전체 도메인 계층을 추가합니다. 사용자 검증, 포인트 관리, API 엔드포인트, 저장소 구현, 예외 처리를 포함하며 통합 및 E2E 테스트로 커버합니다.

변경 사항

코호트 / 파일 변경 요약
사용자 도메인
apps/commerce-api/src/main/java/com/loopers/domain/user/User.java, Gender.java, UserRepository.java, UserService.java
사용자 ID, 이메일, 생년월일, 성별 필드를 가진 JPA 엔드포인트를 생성합니다. Gender 열거형 추가, 유효성 검사 로직 포함(ID 길이, 이메일 형식, 날짜 형식), UserRepository 인터페이스 정의, UserService 비즈니스 로직 구현
포인트 도메인
apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java, PointRepository.java, PointService.java
Point JPA 엔드포인트(User와 1:1 연관), 잔액 필드, charge() 메서드(양수 검증 포함), PointRepository 인터페이스, PointService로 조회 및 충전 기능 구현
가입 애플리케이션
apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java, SignUpInfo.java
사용자 생성 및 포인트 초기화를 조율하는 SignUpFacade 컴포넌트 추가, SignUpInfo 응답 레코드
사용자 저장소 구현
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java, UserRepositoryImpl.java
Spring Data JPA 저장소 및 구현체 추가, userId로 검색 기능
포인트 저장소 구현
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java, PointRepositoryImpl.java
Spring Data JPA 저장소(JPQL fetch join 포함) 및 구현체 추가, userId로 검색 기능
API 예외 처리
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
MissingRequestHeaderException 및 MethodArgumentNotValidException 핸들러 추가
가입 API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Controller.java, SignUpV1Dto.java
POST /api/v1/signup 엔드포인트, 요청/응답 DTO 정의, Gender 변환 로직
포인트 API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java, PointsV1Dto.java
GET /me/points, POST /me/points/charge 엔드포인트, 요청/응답 DTO 정의, X-USER-ID 헤더 읽기
사용자 정보 API
apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Controller.java, UserInfoV1Dto.java
GET /api/v1/me 엔드포인트, 사용자 정보 응답 DTO
도메인 테스트
apps/commerce-api/src/test/java/com/loopers/domain/.../UserTest.java, UserServiceIntegrationTest.java, UserTestFixture.java, PointTest.java, PointServiceIntegrationTest.java
사용자 유효성 검사, 가입 로직, 포인트 충전 로직의 단위 및 통합 테스트
API E2E 테스트
apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java, UserInfoV1ApiE2ETest.java, PointsV1ApiE2ETest.java
가입, 사용자 정보 조회, 포인트 조회/충전 API의 엔드투엔드 테스트

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant SignUpV1Controller
    participant SignUpFacade
    participant UserService
    participant PointService
    participant UserRepository
    participant PointRepository

    User->>SignUpV1Controller: POST /api/v1/signup<br/>(userId, email, birthDate, gender)
    SignUpV1Controller->>SignUpFacade: signUp(userId, email, birthDate, gender)
    SignUpFacade->>UserService: create(userId, email, birthDate, gender)
    UserService->>UserService: User.of(...) 유효성 검사
    UserService->>UserRepository: save(user)
    UserRepository-->>UserService: User 반환
    SignUpFacade->>PointService: create(user, 0)
    PointService->>PointService: Point.of(user, 0)
    PointService->>PointRepository: save(point)
    PointRepository-->>PointService: Point 반환
    SignUpFacade->>SignUpFacade: SignUpInfo.from(user)
    SignUpFacade-->>SignUpV1Controller: SignUpInfo 반환
    SignUpV1Controller-->>User: 200 ApiResponse<SignupResponse>

    rect rgb(220, 240, 255)
    Note over SignUpV1Controller,PointRepository: 포인트 충전 흐름
    end

    User->>PointsV1Controller: POST /me/points/charge<br/>(X-USER-ID, amount)
    PointsV1Controller->>PointService: charge(userId, amount)
    PointService->>PointRepository: findByUserId(userId)
    PointRepository-->>PointService: Point 또는 null
    alt Point 존재
        PointService->>PointService: point.charge(amount) 유효성 검사
        PointService->>PointRepository: save(point)
        PointRepository-->>PointService: Point 반환
        PointService-->>PointsV1Controller: Point 반환
    else Point 없음
        PointService-->>PointsV1Controller: CoreException(NOT_FOUND)
    end
    PointsV1Controller-->>User: 200/404 ApiResponse
Loading

예상 코드 리뷰 시간

🎯 3 (중간 난이도) | ⏱️ ~30분

  • 특별히 주의할 영역:
    • User.java의 유효성 검사 로직(userId 길이, 이메일 패턴, 날짜 형식 검증) - 정규식 및 예외 메시지 확인 필요
    • Point.java의 charge() 메서드 - 포인트 양수 검증 로직 및 예외 처리 일관성 확인
    • PointsV1Controller.java의 ChargeRequest 검증 및 X-USER-ID 헤더 처리 - API 계약 적합성 검증
    • ApiControllerAdvice.java의 새로운 예외 핸들러 - 기존 핸들러와의 통일성 검토
    • PointJpaRepository.java의 JPQL fetch join 쿼리 - N+1 문제 해결 및 쿼리 정확성 확인
    • E2E 테스트들의 테스트 격리 및 데이터 정리 - DatabaseCleanUp 활용 적절성 검증

🐰 회원가입 대기실을 통과하고,
포인트라는 보물을 얻었네요!
유효성 검사의 성벽을 넘으며,
도메인은 튼튼해지고,
API는 활짝 열렸어요.
이젠 포인트를 충전하는 즐거움만 남았어요! 💰

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[volume-1] 회원가입, 내 정보 조회, 포인트 조회, 포인트 충전 기능 구현"은 변경사항의 주요 내용을 명확하고 정확하게 반영합니다. raw_summary에서 확인할 수 있듯이, SignUpFacade와 SignUpV1Controller를 통한 회원가입 기능, UserService와 UserInfoV1Controller를 통한 사용자 정보 조회 기능, 그리고 PointService와 PointsV1Controller를 통한 포인트 조회 및 충전 기능이 모두 구현되어 있습니다. 제목은 구체적이고 읽기 쉬우며, 파일 목록이나 불필요한 요소 없이 핵심 기능들을 효과적으로 요약하고 있습니다.
Description Check ✅ Passed PR 설명이 저장소의 템플릿 요구사항을 충족하고 있습니다. 📌 Summary 섹션에서 회원 및 포인트 도메인 구현의 핵심 내용을 명확하게 요약했으며, 💬 Review Points 섹션에서는 User ID 네이밍 일관성, 외래키 설정, 테스트 픽스처 전략 등 3가지 구체적인 검토 사항을 상세하게 제시하여 리뷰어가 효과적인 리뷰를 할 수 있도록 했습니다. ✅ Checklist 섹션에서는 테스트 작성, 테스트 명확성, E2E 검증 등 필수 항목들이 모두 체크되어 있으며, raw_summary를 통해 실제로 다양한 테스트가 포함되어 있음을 확인할 수 있습니다.

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

@looper88888
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

✅ Actions performed

Review triggered.

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (7)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Dto.java (1)

7-12: 입력 필드 형식 검증을 조금 더 엄격하게 해주세요

현재 email, birthDate 모두 @NotBlank만 지정되어 있어 공백만 거른 뒤 잘못된 형식이 그대로 비즈니스 레이어까지 전달됩니다. 실제 SignUpFacade에서 파싱/검증 중 예외가 나면 곧바로 500으로 번질 수 있어 API 단에서 사전에 거르는 편이 안전합니다. 최소한 email에는 @Email, birthDate에는 @Pattern(예: ^\d{4}-\d{2}-\d{2}$) 정도를 추가해 주시고, 필요한 jakarta.validation import도 함께 정리해 주시면 좋겠습니다.

apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)

23-25: null 반환 대신 Optional<User> 사용을 권장합니다.

findByUserIdnull을 반환하면 호출하는 코드에서 NPE가 발생할 수 있습니다. Optional<User>를 반환하거나 사용자를 찾을 수 없을 때 CoreException을 던지는 방식으로 변경하는 것이 더 안전합니다.

다음과 같이 수정할 수 있습니다:

-public User findByUserId(String userId) {
-    return userRepository.findByUserId(userId);
+public Optional<User> findByUserId(String userId) {
+    return Optional.ofNullable(userRepository.findByUserId(userId));
 }

또는 예외를 던지는 방식:

 public User findByUserId(String userId) {
-    return userRepository.findByUserId(userId);
+    User user = userRepository.findByUserId(userId);
+    if (user == null) {
+        throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다: " + userId);
+    }
+    return user;
 }
apps/commerce-api/src/main/java/com/loopers/domain/user/User.java (1)

77-77: 날짜 파싱에서 포맷터를 일관되게 사용하세요.

Line 64에서는 DATE_FORMATTER를 사용하여 검증하지만, Line 77에서는 포맷터 없이 파싱합니다. 일관성을 위해 동일한 포맷터를 사용하는 것이 좋습니다.

-this.birthDate = LocalDate.parse(birthDateStr);
+this.birthDate = LocalDate.parse(birthDateStr, DATE_FORMATTER);
apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java (1)

28-31: null 잔액을 자동으로 0으로 변환하는 것이 버그를 숨길 수 있습니다.

Line 30에서 balancenull일 때 자동으로 0으로 설정합니다. 이는 방어적이지만, 의도치 않게 null이 전달되는 버그를 숨길 수 있습니다.

명시적으로 검증하는 것을 고려하세요:

 public Point(User user, Long balance) {
+    if (balance == null) {
+        throw new IllegalArgumentException("잔액은 null일 수 없습니다.");
+    }
     this.user = user;
-    this.balance = balance != null ? balance : 0L;
+    this.balance = balance;
 }

또는 정적 팩토리 메서드에서 기본값을 명시적으로 처리:

 public static Point of(User user, Long balance) {
-    return new Point(user, balance);
+    return new Point(user, balance != null ? balance : 0L);
 }
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (1)

57-57: Mockito.reset() 사용을 재고하세요.

각 테스트에서 Mockito.reset()을 사용하는 것보다 @BeforeEach에서 새로운 spy를 생성하거나 테스트 격리를 위한 다른 방법을 사용하는 것이 좋습니다.

현재는 작동하지만, 테스트 격리가 더 깔끔하게 이루어질 수 있습니다.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java (1)

53-110: 중복 회원가입 시나리오 테스트 추가를 권장합니다.

현재 테스트는 성공 케이스와 성별 누락 케이스를 다루지만, 중복 ID로 회원가입을 시도하는 경우(409 Conflict 응답 예상)는 다루지 않습니다.

다음 테스트 추가를 고려하세요:

@DisplayName("이미 가입된 ID로 회원가입 시도 시, `409 Conflict` 응답을 반환한다.")
@Test
void returns409_whenDuplicateUserId() {
    // arrange - 첫 번째 가입
    SignUpV1Dto.SignUpRequest firstRequest = new SignUpV1Dto.SignUpRequest(
        UserTestFixture.ValidUser.USER_ID,
        UserTestFixture.ValidUser.EMAIL,
        UserTestFixture.ValidUser.BIRTH_DATE,
        Gender.MALE.name()
    );
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    testRestTemplate.exchange(ENDPOINT_SIGNUP, HttpMethod.POST, 
        new HttpEntity<>(firstRequest, headers), 
        new ParameterizedTypeReference<ApiResponse<SignUpV1Dto.SignupResponse>>() {});

    // act - 중복 가입 시도
    SignUpV1Dto.SignUpRequest duplicateRequest = new SignUpV1Dto.SignUpRequest(
        UserTestFixture.ValidUser.USER_ID,
        "different@email.com",
        UserTestFixture.ValidUser.BIRTH_DATE,
        Gender.FEMALE.name()
    );
    HttpEntity<SignUpV1Dto.SignUpRequest> httpEntity = new HttpEntity<>(duplicateRequest, headers);
    ResponseEntity<ApiResponse<Object>> response =
        testRestTemplate.exchange(ENDPOINT_SIGNUP, HttpMethod.POST, httpEntity, 
            new ParameterizedTypeReference<>() {});

    // assert
    assertAll(
        () -> assertThat(response.getStatusCode().value()).isEqualTo(409),
        () -> assertThat(userJpaRepository.count()).isEqualTo(1L)
    );
}
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2ETest.java (1)

135-200: 유효하지 않은 충전 금액에 대한 테스트 케이스 추가를 권장합니다.

현재 ChargePoints 테스트는 성공 케이스와 존재하지 않는 사용자 케이스를 다루지만, 음수 또는 0 금액과 같은 잘못된 입력은 테스트하지 않습니다. Point.validateChargeAmount(Line 42-46)는 이러한 케이스를 검증하므로, E2E 수준에서도 테스트되어야 합니다.

다음 테스트들을 추가하는 것을 고려하세요:

@DisplayName("0 이하의 금액으로 충전 시도 시, `400 Bad Request` 응답을 반환한다.")
@ParameterizedTest
@ValueSource(longs = {0L, -100L, -1L})
void returns400_whenInvalidChargeAmount(Long invalidAmount) {
    // arrange
    String userId = UserTestFixture.ValidUser.USER_ID;
    signUpFacade.signUp(userId, UserTestFixture.ValidUser.EMAIL, 
        UserTestFixture.ValidUser.BIRTH_DATE, Gender.MALE);
    
    PointsV1Dto.ChargeRequest requestBody = new PointsV1Dto.ChargeRequest(invalidAmount);
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.add("X-USER-ID", userId);
    HttpEntity<PointsV1Dto.ChargeRequest> httpEntity = new HttpEntity<>(requestBody, headers);

    // act
    ParameterizedTypeReference<ApiResponse<Object>> responseType = new ParameterizedTypeReference<>() {};
    ResponseEntity<ApiResponse<Object>> response =
        testRestTemplate.exchange(ENDPOINT_CHARGE, HttpMethod.POST, httpEntity, responseType);

    // assert
    assertThat(response.getStatusCode().value()).isEqualTo(400);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5ec397 and f6337e5.

📒 Files selected for processing (28)
  • apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/point/Point.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/Gender.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/user/User.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/PointsV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Dto.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Controller.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Dto.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/point/PointTest.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/domain/user/UserTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserTestFixture.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2ETest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserInfoV1ApiE2ETest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/main/java/com/loopers/domain/user/User.java
  • apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java
🔇 Additional comments (18)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (2)

13-16: 정상적인 위임 패턴입니다.

JPA 레포지토리에 대한 간단한 위임으로 구현이 명확합니다.


18-21: 문제 없음 - null 처리가 모든 호출 지점에서 올바르게 구현되어 있습니다.

검증 결과, findByUserId의 모든 호출 지점에서 null 반환값을 적절히 처리하고 있습니다:

  • 컨트롤러 계층: UserInfoV1ControllerPointsV1Controller에서 null 체크 후 CoreException 발생
  • 서비스 계층: PointService.charge()에서 null 체크 후 CoreException 발생
  • 테스트: UserServiceIntegrationTestPointServiceIntegrationTest에서 null 시나리오 명시적 검증

코드는 의도된 대로 작동하며 null 안전성이 보장됩니다.

apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)

3-6: 명확한 enum 정의입니다.

간단하고 명확한 Gender enum 구현입니다.

apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java (1)

3-6: 깔끔한 레포지토리 인터페이스입니다.

도메인 레이어의 레포지토리 추상화가 명확하게 정의되어 있습니다.

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

14-60: 잘 구조화된 도메인 검증 테스트입니다.

파라미터화된 테스트를 활용하여 다양한 입력 검증 시나리오를 효과적으로 커버하고 있습니다. AAA 패턴도 명확하게 적용되어 있습니다.

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

3-6: 명확한 레포지토리 인터페이스입니다.

Point 도메인을 위한 레포지토리 추상화가 잘 정의되어 있습니다.

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

7-9: 표준 Spring Data JPA 레포지토리입니다.

Spring Data JPA 규칙을 올바르게 따르고 있으며, findByUserId 메서드 시그니처가 적절합니다.

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

15-37: 효과적인 검증 로직 테스트입니다.

파라미터화된 테스트로 경계값(0 이하)을 포함한 다양한 시나리오를 테스트하고 있으며, Mockito를 적절히 활용하고 있습니다.

apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java (1)

8-18: 깔끔한 DTO 레코드 구현입니다.

Record 타입을 활용한 불변 DTO와 팩토리 메서드를 통한 도메인-애플리케이션 계층 간 변환이 명확합니다.

apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java (1)

19-22: 트랜잭션 경계 설정이 깔끔합니다
Line 19에서 사용자 생성과 포인트 초기화를 한 트랜잭션으로 묶어 race condition이나 잔여 데이터 걱정 없이 처리가 가능해진 점이 좋습니다.

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

10-11: 페치 조인으로 Lazy 문제 예방 👍
Line 10의 JOIN FETCH 덕분에 포인트 조회 뒤 곧바로 point.getUser() 접근해도 Lazy 예외가 나지 않는 점이 인상적입니다.

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

37-91: 실제 시나리오를 담은 통합 테스트 좋습니다
Line 37 이후의 시나리오가 존재/부재/충전 실패 등 핵심 흐름을 모두 커버해 회귀 위험을 크게 줄여줘서 안심이 되네요.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Dto.java (1)

6-19: 응답 DTO 매핑이 깔끔합니다

도메인 User의 필드를 DTO로 일관성 있게 옮겨 응답 스펙을 명확히 해주신 부분 좋습니다.

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

10-21: 테스트 픽스처 구성이 명확합니다

유효/무효 데이터를 한 곳에서 공유하도록 정리해 두어 테스트 가독성이 좋아졌습니다.

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

8-18: 포인트 응답/요청 DTO 구조가 명료합니다

Point 도메인에서 필요한 정보만 추려내고, 충전 요청에 대한 @NotNull/@Positive 검증까지 붙여둔 구성 좋아 보입니다.

apps/commerce-api/src/main/java/com/loopers/domain/user/User.java (1)

70-79: 생성자 유효성 검사 패턴 확인.

생성자에서 유효성 검사를 수행하면 JPA가 데이터베이스에서 엔티티를 재구성할 때도 검증이 실행됩니다. 이는 레거시 데이터나 검증 규칙이 변경된 경우 문제를 일으킬 수 있습니다.

Based on learnings

프로젝트에서 의도한 패턴인지 확인하세요. 만약 스키마 진화를 고려한다면, 생성자는 최소한의 null 체크만 하고 정적 팩토리 메서드(of)에서 전체 검증을 수행하는 패턴을 고려할 수 있습니다:

// 생성자는 JPA용 - 최소 검증
public User(String userId, String email, String birthDateStr, Gender gender) {
    if (userId == null || email == null || birthDateStr == null || gender == null) {
        throw new IllegalArgumentException("필수 필드는 null일 수 없습니다.");
    }
    this.userId = userId;
    this.email = email;
    this.birthDate = LocalDate.parse(birthDateStr, DATE_FORMATTER);
    this.gender = gender;
}

// 정적 팩토리는 애플리케이션용 - 전체 검증
public static User of(String userId, String email, String birthDate, Gender gender) {
    validateUserId(userId);
    validateEmail(email);
    validateBirthDate(birthDate);
    return new User(userId, email, birthDate, gender);
}
apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java (1)

21-21: 외래 키 제약 조건이 비활성화되어 있습니다.

NO_CONSTRAINT는 데이터베이스 수준의 참조 무결성을 비활성화합니다. 이는 성능이나 특정 아키텍처 결정일 수 있지만, 애플리케이션 코드에서 참조 무결성을 보장해야 합니다.

이것이 의도된 설계인지 확인하고, 고아 레코드를 방지하기 위한 애플리케이션 수준의 보호 장치가 있는지 검증하세요.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2ETest.java (1)

53-133: LGTM!

GetMyPoints 테스트 그룹은 성공 케이스, 404, 그리고 누락된 헤더 케이스를 잘 다루고 있습니다. 파라미터화된 테스트를 사용하여 모든 성별에 대해 검증하는 것도 훌륭합니다.

Comment on lines +24 to +34
@GetMapping("/me/points")
public ApiResponse<PointsV1Dto.PointsResponse> getMyPoints(
@RequestHeader("X-USER-ID") String userId
) {
Point point = pointService.findByUserId(userId);
if (point == null) {
throw new CoreException(ErrorType.NOT_FOUND, null);
}

return ApiResponse.success(PointsV1Dto.PointsResponse.from(point));
}
Copy link

@coderabbitai coderabbitai bot Oct 31, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

치명적인 인증/인가 취약점이 있습니다.

클라이언트가 제공한 X-USER-ID 헤더를 아무런 검증 없이 신뢰하고 있습니다. 악의적인 사용자가 임의의 userId를 헤더에 설정하여 다른 사용자의 포인트 정보를 조회할 수 있습니다. 이는 IDOR(Insecure Direct Object Reference) 취약점입니다.

인증된 사용자 정보는 반드시 서버 측 세션이나 JWT 토큰 등 신뢰할 수 있는 소스에서 가져와야 합니다. Spring Security의 @AuthenticationPrincipal 또는 SecurityContextHolder를 사용하여 인증된 사용자 정보를 가져오도록 수정해야 합니다.

-    public ApiResponse<PointsV1Dto.PointsResponse> getMyPoints(
-        @RequestHeader("X-USER-ID") String userId
-    ) {
+    public ApiResponse<PointsV1Dto.PointsResponse> getMyPoints(
+        @AuthenticationPrincipal UserDetails userDetails
+    ) {
+        String userId = userDetails.getUsername(); // 또는 인증된 사용자 ID를 가져오는 적절한 방법
         Point point = pointService.findByUserId(userId);

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

🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java
around lines 24–34, the controller currently trusts the client-provided
X-USER-ID header causing an IDOR; replace header-based user identification with
the authenticated principal from Spring Security (e.g., add an
@AuthenticationPrincipal parameter or read
SecurityContextHolder.getContext().getAuthentication()), use that principal's
userId to call pointService.findByUserId, and remove or ignore the X-USER-ID
header; if you must accept the header for backward compatibility, validate it
against the authenticated principal and return a 403 when they do not match.
Ensure null/absent principal handling returns an appropriate 401/403 and keep
the existing NOT_FOUND behavior for missing point records.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@coderabbitai 아마 인증 처리 로직은 다음 pr에서 추가될 것 같습니다. 이번 pr은 테스트 코드 작성과 구현하는 과정을 이해하는 것이 목적이라 인증 처리는 요건에 포함하지않고 헤더의 X-USER-ID 로 처리하라고 가이드 주신 것으로 이해해서요, 이 부분은 이번 pr에서는 피드백 주시지않아도 될 것 같습니다

Copy link

Choose a reason for hiding this comment

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

@minor7295, 이해했습니다! 이번 PR이 테스트 코드 작성과 구현 과정을 학습하는 것이 목적이고, 인증 처리는 다음 PR에서 추가될 예정이라는 점 확인했습니다.

현재 X-USER-ID 헤더 방식은 학습 목적으로 의도된 것이므로, 이 부분에 대한 피드백은 보류하겠습니다.

혹시 필요하시다면, 다음 PR을 위해 인증 구현 관련 이슈를 생성해드릴 수 있습니다. 원하시면 말씀해주세요!


✏️ Learnings added
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +36 to +43
@PostMapping("/me/points/charge")
public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
@RequestHeader("X-USER-ID") String userId,
@Valid @RequestBody PointsV1Dto.ChargeRequest request
) {
Point point = pointService.charge(userId, request.amount());
return ApiResponse.success(PointsV1Dto.PointsResponse.from(point));
}
Copy link

@coderabbitai coderabbitai bot Oct 31, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

치명적인 인증/인가 취약점이 있습니다.

GET 엔드포인트와 동일하게, 클라이언트가 제공한 X-USER-ID 헤더를 신뢰하여 임의의 사용자 계정에 포인트를 충전할 수 있습니다. 이는 심각한 보안 취약점이며 금전적 손실로 이어질 수 있습니다.

반드시 인증된 사용자 정보를 서버 측에서 안전하게 검증하고 가져와야 합니다.

-    public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
-        @RequestHeader("X-USER-ID") String userId,
+    public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
+        @AuthenticationPrincipal UserDetails userDetails,
         @Valid @RequestBody PointsV1Dto.ChargeRequest request
     ) {
+        String userId = userDetails.getUsername(); // 또는 인증된 사용자 ID를 가져오는 적절한 방법
         Point point = pointService.charge(userId, request.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
@PostMapping("/me/points/charge")
public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
@RequestHeader("X-USER-ID") String userId,
@Valid @RequestBody PointsV1Dto.ChargeRequest request
) {
Point point = pointService.charge(userId, request.amount());
return ApiResponse.success(PointsV1Dto.PointsResponse.from(point));
}
@PostMapping("/me/points/charge")
public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
@AuthenticationPrincipal UserDetails userDetails,
@Valid @RequestBody PointsV1Dto.ChargeRequest request
) {
String userId = userDetails.getUsername(); // 또는 인증된 사용자 ID를 가져오는 적절한 방법
Point point = pointService.charge(userId, request.amount());
return ApiResponse.success(PointsV1Dto.PointsResponse.from(point));
}

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

포인트 충전 작업에 멱등성 보장이 필요합니다.

현재 구현은 클라이언트 재시도 시 포인트가 중복으로 충전될 수 있습니다. 네트워크 타임아웃이나 클라이언트 오류로 인한 재시도 시 사용자에게 의도하지 않은 중복 충전이 발생할 수 있습니다.

멱등성 키(idempotency key)를 요청에 포함시키고, 이를 검증하여 동일한 요청이 여러 번 처리되지 않도록 해야 합니다. 또는 충전 작업을 트랜잭션으로 묶고 중복 요청을 감지하는 메커니즘을 추가해야 합니다.

멱등성 보장 로직이 PointService.charge 메서드 내부에 구현되어 있는지 확인이 필요합니다:


🏁 Script executed:

#!/bin/bash
# Description: PointService.charge 메서드의 구현을 확인하여 멱등성 처리 여부를 검증

# PointService 클래스 파일 찾기 및 charge 메서드 구현 확인
ast-grep --pattern $'class PointService {
  $$$
  charge($$$) {
    $$$
  }
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find PointService file
echo "=== Finding PointService files ==="
fd -t f "PointService" --extension java

# Search for the charge method definition
echo -e "\n=== Searching for charge method ==="
rg -nP --type=java "def\s+charge|void\s+charge|Point\s+charge|[a-zA-Z<>]+\s+charge\s*\(" -A 20

# Search for idempotency-related keywords
echo -e "\n=== Searching for idempotency patterns ==="
rg -nP --type=java "idempotent|idempotency|idempotent.*key|requestId|request.*id|duplicate.*check|duplicate.*key" -C 3

# Check the request DTO
echo -e "\n=== Finding ChargeRequest DTO ==="
fd -t f "ChargeRequest" --extension java

Length of output: 3676


🏁 Script executed:

#!/bin/bash
# Find PointsV1Dto to check ChargeRequest structure
echo "=== Finding PointsV1Dto ==="
fd -t f "PointsV1Dto" --extension java

# Check for database-level constraints
echo -e "\n=== Searching for database constraints ==="
rg -nP --type=java "@Table|@UniqueConstraint|@Column.*unique|uniqueConstraint" -C 2

# Search for idempotency interceptors or AOP
echo -e "\n=== Searching for interceptors/filters/AOP ==="
rg -nP --type=java "Interceptor|Filter|Advice|@Around|@Before|@After" -C 2

# Check Point entity for uniqueness
echo -e "\n=== Finding Point entity ==="
fd -t f "Point\.java" --extension java

Length of output: 7576


PointService.charge() 메서드에서 멱등성 처리가 필수적으로 필요합니다.

검증 결과, 현재 구현(apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java 25-32줄)에는 멱등성 보장 메커니즘이 전혀 없습니다. 단순한 조회-수정-저장 패턴이며, 데이터베이스 수준의 유일성 제약이나 중복 감지 로직이 없습니다.

클라이언트 재시도나 네트워크 타임아웃 발생 시 포인트가 중복으로 충전되는 문제가 발생할 수 있습니다. 금융 거래와 관련된 중요한 작업이므로 다음 중 하나를 구현해야 합니다:

  • 멱등성 키(idempotency key)를 요청에 포함시키고, 데이터베이스에서 중복 요청을 감지하는 메커니즘 추가
  • 충전 작업 기록을 별도 테이블에서 관리하여 중복 처리 방지
  • 데이터베이스 유일성 제약 추가로 동일 요청의 재시도 감지

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@coderabbitai 위의 코멘트와 마찬가지로 피드백 반영 생략하겠습니다.

Copy link

Choose a reason for hiding this comment

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

@minor7295, 알겠습니다! 말씀하신 대로 해당 피드백은 이번 PR에서 반영하지 않으시는 것으로 확인했습니다.

기존 학습 내용에 따르면 X-USER-ID 헤더 기반 사용자 식별은 학습 목적으로 임시 사용 중이며, 향후 PR에서 Spring Security를 통한 적절한 인증으로 교체될 예정이므로 현재 단계에서는 문제없습니다.

추가로 궁금하신 사항이 있으시면 언제든지 말씀해 주세요! 😊


🧠 Learnings used
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.

@minor7295 minor7295 changed the title Base pr round1 [volume-1] 회원가입, 내 정보 조회, 포인트 조회, 포인트 충전 기능 구현 Oct 31, 2025
@minor7295 minor7295 marked this pull request as ready for review October 31, 2025 09:01
@minor7295 minor7295 force-pushed the base-pr-round1 branch 4 times, most recently from d238f1d to 56b5b1c Compare October 31, 2025 14:10
* test: User 단위테스트 추가

* feat: User 도메인 구현

* test: 회원 가입 통합테스트 추가

* feat: 회원가입 서비스 로직 구현

* test: 회원가입 E2E 테스트 추가

* feat: 회원가입 API 구현

* test: gender필드를 저장할 수 있도록 테스트 코드 수정

* refactor: User도메인에 성별 필드 추가

* test: 회원 정보 조회 통합 테스트 작성

* feat: 회원 정보 조회 서비스 로직 구현

* test: 회원 정보 조회 E2E 테스트 작성

* feat: 회원 정보 조회 API 추가
* test: 회원가입 관련 테스트 코드가 SignUpFacade를 참조하도록 수정

* refactor: 회원가입을 처리하는 SignUpFacade 구현

* test: 포인트 조회 통합테스트 추가

* feat: 포인트 조회 서비스 로직 구현

* test: 포인트 조회 E2E 테스트 코드 추가

* feat: 포인트 조회 API 로직 추가

* test: 포인트 충전 단위 테스트 추가

* feat: 포인트 충전 도메인 로직 추가

* test: 포인트 충전 테스트 코드 추가

* feat: 포인트 충전 서비스 로직 추가

* test: 포인트 충전 E2E 테스트 코드 추가

* feat: 포인트 충전 API 추가
@minor7295
Copy link
Collaborator Author

@coderabbitai generate docstring

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all main production Java files (non-test) from the PR to generate docstrings
echo "=== Domain Layer ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
echo -e "\n\n=== Domain Layer - Gender ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java
echo -e "\n\n=== Domain Layer - UserRepository ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
echo -e "\n\n=== Domain Layer - UserService ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
echo -e "\n\n=== Domain Layer - Point ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java
echo -e "\n\n=== Domain Layer - PointRepository ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java
echo -e "\n\n=== Domain Layer - PointService ==="
cat apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java

Length of output: 6824


🏁 Script executed:

#!/bin/bash
# Fetch application, infrastructure, and interface layer files
echo "=== Application Layer - SignUpFacade ==="
cat apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java
echo -e "\n\n=== Application Layer - SignUpInfo ==="
cat apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java
echo -e "\n\n=== Infrastructure Layer - UserRepositoryImpl ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
echo -e "\n\n=== Infrastructure Layer - UserJpaRepository ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
echo -e "\n\n=== Infrastructure Layer - PointRepositoryImpl ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java
echo -e "\n\n=== Infrastructure Layer - PointJpaRepository ==="
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java

Length of output: 3591


🏁 Script executed:

#!/bin/bash
# Fetch interface layer files
echo "=== Interface Layer - SignUpV1Controller ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Controller.java
echo -e "\n\n=== Interface Layer - SignUpV1Dto ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/signup/SignUpV1Dto.java
echo -e "\n\n=== Interface Layer - UserInfoV1Controller ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Controller.java
echo -e "\n\n=== Interface Layer - UserInfoV1Dto ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/userinfo/UserInfoV1Dto.java
echo -e "\n\n=== Interface Layer - PointsV1Controller ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java
echo -e "\n\n=== Interface Layer - PointsV1Dto ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Dto.java
echo -e "\n\n=== Interface Layer - ApiControllerAdvice ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

Length of output: 13642


@minor7295, 전체 코드에 대한 Javadoc 형식의 docstring을 생성했습니다. 아래는 각 레이어별로 정리된 문서입니다.


📚 Domain Layer

User.java

/**
 * 사용자 도메인 엔티티.
 * <p>
 * 사용자의 기본 정보(ID, 이메일, 생년월일, 성별)를 관리하며,
 * 각 필드에 대한 유효성 검증을 수행합니다.
 * </p>
 * 
 * <h3>검증 규칙</h3>
 * <ul>
 *   <li>userId: 영문 및 숫자 조합, 최대 10자</li>
 *   <li>email: 유효한 이메일 형식</li>
 *   <li>birthDate: yyyy-MM-dd 형식</li>
 * </ul>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 사용자를 생성합니다.
 * 
 * @param userId 사용자 ID (영문 및 숫자, 최대 10자)
 * @param email 이메일 주소
 * @param birthDateStr 생년월일 (yyyy-MM-dd 형식)
 * @param gender 성별
 * @throws CoreException userId, email, birthDate가 유효하지 않을 경우
 */
public User (String userId, String email, String birthDateStr, Gender gender)

/**
 * User 인스턴스를 생성하는 정적 팩토리 메서드.
 * 
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDate 생년월일 문자열
 * @param gender 성별
 * @return 생성된 User 인스턴스
 * @throws CoreException 유효성 검증 실패 시
 */
public static User of(String userId, String email, String birthDate, Gender gender)

/**
 * 사용자 ID의 유효성을 검증합니다.
 * 
 * @param userId 검증할 사용자 ID
 * @throws CoreException userId가 null, 공백이거나 형식에 맞지 않을 경우
 */
private void validateUserId(String userId)

/**
 * 이메일의 유효성을 검증합니다.
 * 
 * @param email 검증할 이메일 주소
 * @throws CoreException email이 null, 공백이거나 형식에 맞지 않을 경우
 */
private void validateEmail(String email)

/**
 * 생년월일의 유효성을 검증합니다.
 * 
 * @param birthDate 검증할 생년월일 문자열
 * @throws CoreException birthDate가 null, 공백이거나 yyyy-MM-dd 형식이 아닐 경우
 */
private static void validateBirthDate(String birthDate)

Gender.java

/**
 * 사용자의 성별을 나타내는 열거형.
 * 
 * @author Loopers
 * @version 1.0
 */

UserRepository.java

/**
 * User 엔티티에 대한 저장소 인터페이스.
 * <p>
 * 사용자 정보의 영속성 계층과의 상호작용을 정의합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 사용자를 저장합니다.
 * 
 * @param user 저장할 사용자
 * @return 저장된 사용자
 */
User save(User user);

/**
 * 사용자 ID로 사용자를 조회합니다.
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 사용자, 없으면 null
 */
User findByUserId(String userId);

UserService.java

/**
 * 사용자 도메인 서비스.
 * <p>
 * 사용자 생성 및 조회 등의 도메인 로직을 처리합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 새로운 사용자를 생성합니다.
 * 
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDateStr 생년월일 (yyyy-MM-dd)
 * @param gender 성별
 * @return 생성된 사용자
 * @throws CoreException 중복된 사용자 ID가 존재하거나 유효성 검증 실패 시
 */
public User create(String userId, String email, String birthDateStr, Gender gender)

/**
 * 사용자 ID로 사용자를 조회합니다.
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 사용자, 없으면 null
 */
public User findByUserId(String userId)

Point.java

/**
 * 포인트 도메인 엔티티.
 * <p>
 * 사용자의 포인트 잔액을 관리하며, 포인트 충전 기능을 제공합니다.
 * User와 일대일 관계를 맺고 있습니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * Point 인스턴스를 생성합니다.
 * 
 * @param user 포인트 소유자
 * @param balance 초기 잔액 (null인 경우 0으로 초기화)
 */
public Point(User user, Long balance)

/**
 * Point 인스턴스를 생성하는 정적 팩토리 메서드.
 * 
 * @param user 포인트 소유자
 * @param balance 초기 잔액
 * @return 생성된 Point 인스턴스
 */
public static Point of(User user, Long balance)

/**
 * 포인트를 충전합니다.
 * 
 * @param amount 충전할 포인트 금액 (0보다 커야 함)
 * @throws CoreException amount가 null이거나 0 이하일 경우
 */
public void charge(Long amount)

/**
 * 충전 금액의 유효성을 검증합니다.
 * 
 * @param amount 검증할 충전 금액
 * @throws CoreException amount가 null이거나 0 이하일 경우
 */
private void validateChargeAmount(Long amount)

PointRepository.java

/**
 * Point 엔티티에 대한 저장소 인터페이스.
 * <p>
 * 포인트 정보의 영속성 계층과의 상호작용을 정의합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 포인트를 저장합니다.
 * 
 * @param point 저장할 포인트
 * @return 저장된 포인트
 */
Point save(Point point);

/**
 * 사용자 ID로 포인트를 조회합니다.
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 포인트, 없으면 null
 */
Point findByUserId(String userId);

PointService.java

/**
 * 포인트 도메인 서비스.
 * <p>
 * 포인트 생성, 조회, 충전 등의 도메인 로직을 처리합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 새로운 포인트를 생성합니다.
 * 
 * @param user 포인트 소유자
 * @param balance 초기 잔액
 * @return 생성된 포인트
 */
public Point create(User user, Long balance)

/**
 * 사용자 ID로 포인트를 조회합니다.
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 포인트, 없으면 null
 */
public Point findByUserId(String userId)

/**
 * 사용자의 포인트를 충전합니다.
 * <p>
 * 트랜잭션 내에서 실행되어 데이터 일관성을 보장합니다.
 * </p>
 * 
 * @param userId 사용자 ID
 * @param amount 충전할 금액 (0보다 커야 함)
 * @return 충전된 포인트
 * @throws CoreException 포인트를 찾을 수 없거나 충전 금액이 유효하지 않을 경우
 */
@Transactional
public Point charge(String userId, Long amount)

🎯 Application Layer

SignUpFacade.java

/**
 * 회원가입 파사드.
 * <p>
 * 회원가입 시 사용자 생성과 포인트 초기화를 조율하는 
 * 애플리케이션 서비스입니다.
 * 트랜잭션 경계를 관리하여 데이터 일관성을 보장합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 회원가입을 처리합니다.
 * <p>
 * 사용자를 생성하고 초기 포인트(0)를 부여합니다.
 * 전체 과정이 하나의 트랜잭션으로 처리됩니다.
 * </p>
 * 
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDateStr 생년월일 (yyyy-MM-dd)
 * @param gender 성별
 * @return 생성된 사용자 정보
 * @throws CoreException 유효성 검증 실패 또는 중복 ID 존재 시
 */
@Transactional
public SignUpInfo signUp(String userId, String email, String birthDateStr, Gender gender)

SignUpInfo.java

/**
 * 회원가입 결과 정보를 담는 레코드.
 * <p>
 * User 도메인 엔티티로부터 생성된 불변 데이터 전송 객체입니다.
 * </p>
 * 
 * @param id 사용자 엔티티 ID
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDate 생년월일
 * @param gender 성별
 * @author Loopers
 * @version 1.0
 */
public record SignUpInfo(Long id, String userId, String email, LocalDate birthDate, Gender gender)

/**
 * User 엔티티로부터 SignUpInfo를 생성합니다.
 * 
 * @param user 변환할 사용자 엔티티
 * @return 생성된 SignUpInfo
 */
public static SignUpInfo from(User user)

🏗️ Infrastructure Layer

UserRepositoryImpl.java

/**
 * UserRepository의 JPA 구현체.
 * <p>
 * Spring Data JPA를 활용하여 User 엔티티의 
 * 영속성 작업을 처리합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * {@inheritDoc}
 */
@Override
public User save(User user)

/**
 * {@inheritDoc}
 */
@Override
public User findByUserId(String userId)

UserJpaRepository.java

/**
 * User 엔티티를 위한 Spring Data JPA 리포지토리.
 * <p>
 * JpaRepository를 확장하여 기본 CRUD 기능과 
 * 사용자 ID 기반 조회 기능을 제공합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 사용자 ID로 사용자를 조회합니다.
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 사용자를 담은 Optional
 */
Optional<User> findByUserId(String userId);

PointRepositoryImpl.java

/**
 * PointRepository의 JPA 구현체.
 * <p>
 * Spring Data JPA를 활용하여 Point 엔티티의 
 * 영속성 작업을 처리합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * {@inheritDoc}
 */
@Override
public Point save(Point point)

/**
 * {@inheritDoc}
 */
@Override
public Point findByUserId(String userId)

PointJpaRepository.java

/**
 * Point 엔티티를 위한 Spring Data JPA 리포지토리.
 * <p>
 * JpaRepository를 확장하여 기본 CRUD 기능과 
 * 사용자 ID 기반 조회 기능을 제공합니다.
 * N+1 문제 방지를 위해 Fetch Join을 사용합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 사용자 ID로 포인트를 조회합니다.
 * <p>
 * JOIN FETCH를 사용하여 연관된 User 엔티티를 함께 로드합니다.
 * </p>
 * 
 * @param userId 조회할 사용자 ID
 * @return 조회된 포인트를 담은 Optional
 */
@Query("SELECT p FROM Point p JOIN FETCH p.user WHERE p.user.userId = :userId")
Optional<Point> findByUserId(@Param("userId") String userId);

🌐 Interface Layer

SignUpV1Controller.java

/**
 * 회원가입 API v1 컨트롤러.
 * <p>
 * 사용자 회원가입 요청을 처리하는 REST API를 제공합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 회원가입을 처리합니다.
 * 
 * @param request 회원가입 요청 데이터 (userId, email, birthDate, gender)
 * @return 생성된 사용자 정보를 담은 API 응답
 * @throws CoreException gender 값이 유효하지 않거나, 유효성 검증 실패 또는 중복 ID 존재 시
 */
@PostMapping
public ApiResponse<SignUpV1Dto.SignupResponse> signUp(
    @Valid @RequestBody SignUpV1Dto.SignUpRequest request
)

SignUpV1Dto.java

/**
 * 회원가입 API v1의 데이터 전송 객체(DTO) 컨테이너.
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 회원가입 요청 데이터.
 * 
 * @param userId 사용자 ID (필수)
 * @param email 이메일 주소 (필수)
 * @param birthDate 생년월일 (필수, yyyy-MM-dd)
 * @param gender 성별 (필수, MALE 또는 FEMALE)
 */
public record SignUpRequest(
    @NotBlank String userId,
    @NotBlank String email,
    @NotBlank String birthDate,
    @NotBlank String gender
) {}

/**
 * 회원가입 응답 데이터.
 * 
 * @param id 사용자 엔티티 ID
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDate 생년월일
 * @param gender 성별
 */
public record SignupResponse(Long id, String userId, String email, String birthDate, String gender)

/**
 * SignUpInfo로부터 SignupResponse를 생성합니다.
 * 
 * @param info 회원가입 정보
 * @return 생성된 응답 객체
 */
public static SignupResponse from(SignUpInfo info)

UserInfoV1Controller.java

/**
 * 사용자 정보 API v1 컨트롤러.
 * <p>
 * 인증된 사용자의 정보 조회 기능을 제공합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 현재 사용자의 정보를 조회합니다.
 * 
 * @param userId X-USER-ID 헤더로 전달된 사용자 ID
 * @return 사용자 정보를 담은 API 응답
 * @throws CoreException 사용자를 찾을 수 없는 경우
 */
@GetMapping("/me")
public ApiResponse<UserInfoV1Dto.UserInfoResponse> getMyInfo(
    @RequestHeader("X-USER-ID") String userId
)

UserInfoV1Dto.java

/**
 * 사용자 정보 API v1의 데이터 전송 객체(DTO) 컨테이너.
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 사용자 정보 응답 데이터.
 * 
 * @param userId 사용자 ID
 * @param email 이메일 주소
 * @param birthDate 생년월일 (문자열)
 * @param gender 성별
 */
public record UserInfoResponse(
    String userId,
    String email,
    String birthDate,
    String gender
)

/**
 * User 엔티티로부터 UserInfoResponse를 생성합니다.
 * 
 * @param user 사용자 엔티티
 * @return 생성된 응답 객체
 */
public static UserInfoResponse from(User user)

PointsV1Controller.java

/**
 * 포인트 API v1 컨트롤러.
 * <p>
 * 사용자의 포인트 조회 및 충전 기능을 제공합니다.
 * </p>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 현재 사용자의 포인트를 조회합니다.
 * 
 * @param userId X-USER-ID 헤더로 전달된 사용자 ID
 * @return 포인트 정보를 담은 API 응답
 * @throws CoreException 포인트를 찾을 수 없는 경우
 */
@GetMapping("/me/points")
public ApiResponse<PointsV1Dto.PointsResponse> getMyPoints(
    @RequestHeader("X-USER-ID") String userId
)

/**
 * 현재 사용자의 포인트를 충전합니다.
 * 
 * @param userId X-USER-ID 헤더로 전달된 사용자 ID
 * @param request 충전 요청 데이터 (amount)
 * @return 충전된 포인트 정보를 담은 API 응답
 * @throws CoreException 포인트를 찾을 수 없거나 충전 금액이 유효하지 않은 경우
 */
@PostMapping("/me/points/charge")
public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
    @RequestHeader("X-USER-ID") String userId,
    @Valid @RequestBody PointsV1Dto.ChargeRequest request
)

PointsV1Dto.java

/**
 * 포인트 API v1의 데이터 전송 객체(DTO) 컨테이너.
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * 포인트 정보 응답 데이터.
 * 
 * @param userId 사용자 ID
 * @param balance 포인트 잔액
 */
public record PointsResponse(String userId, Long balance)

/**
 * Point 엔티티로부터 PointsResponse를 생성합니다.
 * 
 * @param point 포인트 엔티티
 * @return 생성된 응답 객체
 */
public static PointsResponse from(Point point)

/**
 * 포인트 충전 요청 데이터.
 * 
 * @param amount 충전할 포인트 금액 (필수, 0보다 커야 함)
 */
public record ChargeRequest(
    @NotNull(message = "포인트는 필수입니다.")
    @Positive(message = "포인트는 0보다 큰 값이어야 합니다.")
    Long amount
) {}

ApiControllerAdvice.java

/**
 * 전역 API 예외 처리 핸들러.
 * <p>
 * 애플리케이션 전역에서 발생하는 예외를 가로채어
 * 일관된 형식의 에러 응답을 생성합니다.
 * </p>
 * 
 * <h3>처리하는 예외 유형</h3>
 * <ul>
 *   <li>CoreException: 도메인 비즈니스 로직 예외</li>
 *   <li>Validation 예외: 요청 데이터 검증 실패</li>
 *   <li>HTTP 메시지 변환 예외: JSON 파싱 오류</li>
 *   <li>기타 예상치 못한 예외</li>
 * </ul>
 * 
 * @author Loopers
 * @version 1.0
 */

/**
 * CoreException을 처리합니다.
 * 
 * @param e 발생한 CoreException
 * @return 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handle(CoreException e)

/**
 * 요청 파라미터 타입 불일치 예외를 처리합니다.
 * 
 * @param e 발생한 MethodArgumentTypeMismatchException
 * @return BAD_REQUEST 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatchException e)

/**
 * 필수 요청 파라미터 누락 예외를 처리합니다.
 * 
 * @param e 발생한 MissingServletRequestParameterException
 * @return BAD_REQUEST 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e)

/**
 * 필수 요청 헤더 누락 예외를 처리합니다.
 * 
 * @param e 발생한 MissingRequestHeaderException
 * @return BAD_REQUEST 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingRequestHeaderException e)

/**
 * 요청 데이터 유효성 검증 실패 예외를 처리합니다.
 * 
 * @param e 발생한 MethodArgumentNotValidException
 * @return BAD_REQUEST 에러 응답 (검증 실패 필드 정보 포함)
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e)

/**
 * HTTP 메시지 읽기 실패 예외를 처리합니다.
 * JSON 파싱 오류, 타입 불일치 등을 처리합니다.
 * 
 * @param e 발생한 HttpMessageNotReadableException
 * @return BAD_REQUEST 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e)

/**
 * 서버 웹 입력 예외를 처리합니다.
 * 
 * @param e 발생한 ServerWebInputException
 * @return BAD_REQUEST 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e)

/**
 * 리소스를 찾을 수 없는 예외를 처리합니다.
 * 
 * @param e 발생한 NoResourceFoundException
 * @return NOT_FOUND 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e)

/**
 * 예상치 못한 모든 예외를 처리합니다.
 * 
 * @param e 발생한 Throwable
 * @return INTERNAL_ERROR 에러 응답
 */
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handle(Throwable e)

/**
 * 에러 메시지에서 누락된 파라미터명을 추출합니다.
 * 
 * @param message 에러 메시지
 * @return 추출된 파라미터명
 */
private String extractMissingParameter(String message)

/**
 * 에러 타입과 메시지를 기반으로 실패 응답을 생성합니다.
 * 
 * @param errorType 에러 타입
 * @param errorMessage 에러 메시지
 * @return 에러 응답
 */
private ResponseEntity<ApiResponse<?>> failureResponse(ErrorType errorType, String errorMessage)

이 docstring들을 각 파일의 클래스와 메서드 위에 추가하시면 됩니다. 모든 주요 public 메서드와 클래스에 대한 설명이 포함되어 있습니다.

@minor7295 minor7295 merged commit 794d236 into Loopers-dev-lab:minor7295 Nov 2, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 13, 2025
@minor7295 minor7295 self-assigned this Nov 13, 2025
hyujikoh added a commit that referenced this pull request Nov 25, 2025
junoade added a commit that referenced this pull request Nov 25, 2025
[volume-4] 트랜잭션 및 동시성 구현
minor7295 added a commit that referenced this pull request Dec 2, 2025
* Feature/catalog caching (#20)

* feat: 상품 조회 결과를 캐시로 관리하는 서비스 로직 추가

* feat: 상품 조회하는 서비스 로직에 Cache Aside 패턴 적용

* Feature/cache eviction (#21)

* feature: 상품 좋아요 수에 로컬캐시 적용 (#21)

* test: LikeFacade 테스트 코드 수정

* feat: Product 엔티티에 인덱스 추가 (#22)
@coderabbitai coderabbitai bot mentioned this pull request Dec 11, 2025
10 tasks
yeonsu00 added a commit that referenced this pull request Dec 12, 2025
pg-simulator 모듈 추가 및 설정 추가
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