Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .claude/skills/requirements-analysis/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: requirements-analysis
description:
제공된 요구사항을 분석하고, 개발자와의 질문/대답을 통해 애매한 요구사항을 명확히 하여 정리합니다.
모든 정리가 끝나면, 시퀀스 다이어그램, 클래스 다이어그램, ERD 등을 Mermaid 문법으로 작성한다.
요구사항이 제공되었을 때, 코드를 작성하기 전 이를 명확히 하는 데에 사용합니다.
---
요구사항을 분석할 때 반드시 다음 흐름을 따른다.
### 1️⃣ 요구사항을 그대로 믿지 말고, 문제 상황으로 다시 설명한다.
- 요구사항 문장을 정리하는 데서 끝내지 않는다.
- "무엇을 만들까?"가 아니라 "지금 어떤 문제가 있고, 그걸 왜 해결하려는가?" 로 재해석한다.
- 다음 관점을 분리해서 정리한다:
- 사용자 관점
- 비즈니스 관점
- 시스템 관점
> 예시
> "주문 실패 시 결제를 취소한다" → "결제 성공/실패와 주문 상태가 어긋나지 않도록 일관성을 유지하려는 문제"

### 2️⃣ 애매한 요구사항을 숨기지 말고 드러낸다
- 추측하거나 알아서 결정하지 않는다.
- 요구사항에서 결정되지 않은 부분을 명시적으로 나열한다.
**다음 유형의 질문을 반드시 포함한다:**
- 정책 질문: 기준 시점, 성공/실패 조건, 예외 처리 규칙
- 경계 질문: 어디까지가 한 책임인가, 어디서 분리되는가
- 확장 질문: 나중에 바뀔 가능성이 있는가

### 3️⃣ 요구사항 명확화를 위한 질문을 개발자 답변이 쉬운 형태로 제시한다
- 질문은 우선순위를 가진다 (중요한 것부터).
- 선택지가 있는 경우, 옵션 + 영향도를 함께 제시한다.
> 형식 예시:
- 선택지 A: 하나의 트랜잭션으로 처리 → 구현 단순, 확장성 낮음
- 선택지 B: 단계별 분리 → 구조 복잡, 확장/보상 처리 유리

### 4️⃣ 합의된 내용을 바탕으로 개념 모델부터 잡는다
- 바로 코드나 기술 얘기로 들어가지 않는다.
- 먼저 다음을 정의한다:
- 액터 (사용자, 외부 시스템)
- 핵심 도메인
- 보조/외부 시스템
- 이 단계는 “구현”이 아니라 설계 사고 정렬이 목적이다.

### 5️⃣ 다이어그램은 항상 이유 → 다이어그램 → 해석 순서로 제시한다
**다이어그램을 그리기 전에 반드시 설명한다**
- 왜 이 다이어그램이 필요한지
- 이 다이어그램으로 무엇을 검증하려는지

**다이어그램은 Mermaid 문법으로 작성한다**
사용 기준:
- **시퀀스 다이어그램**
- 책임 분리
- 호출 순서
- 트랜잭션 경계 확인
- **클래스 다이어그램**
- 도메인 책임
- 의존 방향
- 응집도 확인
- **ERD**
- 영속성 구조
- 관계의 주인
- 정규화 여부

### 6️⃣ 다이어그램을 던지고 끝내지 말고 읽는 법을 짚어준다
- "이 구조에서 특히 봐야 할 포인트"를 2~3줄로 설명한다.
- 설계 의도가 드러나도록 해석을 붙인다.

### 7️⃣ 설계의 잠재 리스크를 반드시 언급한다
- 현재 설계가 가질 수 있는 위험을 숨기지 않는다.
- 트랜잭션 비대화
- 도메인 간 결합도 증가
- 정책 변경 시 영향 범위 확대
- 해결책은 정답처럼 말하지 않고 선택지로 제시한다.

### 톤 & 스타일 가이드
- 강의처럼 설명하지 말고 설계 리뷰 톤을 유지한다
- 정답이라고 제시하기보다, 다른 선택지가 있다면 이를 제공하도록 한다.
- 코드보다 의도, 책임, 경계를 더 중요하게 다룬다
- 구현 전에 생각해야 할 것을 끌어내는 데 집중한다
45 changes: 0 additions & 45 deletions .codeguide/loopers-1-week.md

This file was deleted.

3 changes: 3 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

// security
implementation("org.springframework.security:spring-security-crypto")
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 1. build.gradle.kts 파일 위치 및 내용 확인
find . -name "build.gradle.kts" -type f | head -20

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

Length of output: 429


🏁 Script executed:

# 2. apps/commerce-api 디렉토리 구조 확인
ls -la apps/commerce-api/ 2>/dev/null || echo "Directory not found"

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

Length of output: 314


🏁 Script executed:

# 3. 프로젝트 루트 build.gradle.kts 확인
cat -n build.gradle.kts 2>/dev/null || echo "Not found in root"

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

Length of output: 4538


🏁 Script executed:

# 4. libs.versions.toml 파일 검색
find . -name "libs.versions.toml" -o -name "gradle.properties" -o -name "settings.gradle.kts" | head -10

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

Length of output: 126


🏁 Script executed:

# gradle.properties 확인
cat -n gradle.properties

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

Length of output: 747


🏁 Script executed:

# apps/commerce-api/build.gradle.kts 전체 내용 확인
cat -n apps/commerce-api/build.gradle.kts

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

Length of output: 1269


🏁 Script executed:

# settings.gradle.kts 확인
cat -n settings.gradle.kts

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

Length of output: 1230


🏁 Script executed:

# gradle.lockfile 확인
find . -name "gradle.lockfile" -o -name "*.lockfile" | head -5

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

Length of output: 67


🏁 Script executed:

# CI 설정 확인
find . -type f \( -name "*.yml" -o -name "*.yaml" \) -path "*/.github/*" | head -10

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

Length of output: 110


🏁 Script executed:

# 다른 apps의 보안 관련 의존성 버전 확인
rg -n "spring-security|spring-boot-starter-security" apps/ --type gradle

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

Length of output: 67


🏁 Script executed:

# 프로젝트 전체에서 명시적으로 버전을 지정한 의존성 패턴 확인
rg -n 'implementation\(.*:[^"]*:[^"]*"\)' root build.gradle.kts apps/ modules/ supports/ --type gradle | head -20

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

Length of output: 129


🏁 Script executed:

# Spring Boot 3.4.4 BOM에서 spring-security 버전 확인
curl -s https://repo.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/3.4.4/spring-boot-dependencies-3.4.4.pom 2>/dev/null | grep -A 2 "spring-security" | head -20

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

Length of output: 67


🏁 Script executed:

# 다른 모듈/앱에서 보안 관련 의존성이 어떻게 관리되는지 확인
find . -name "build.gradle.kts" -type f -exec grep -l "spring-security\|crypto" {} \;

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

Length of output: 121


🏁 Script executed:

# .github 디렉토리 전체 구조 확인
find .github -type f 2>/dev/null | head -20

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

Length of output: 141


명시적 버전 관리와 보안 검증 프로세스 추가

spring-security-crypto는 Spring Boot BOM을 통해 관리되는 표준 방식이나, 실제 해석되는 버전을 추적하고 보안 패치 적용을 보증하기 위해 다음을 수행해야 한다.

  1. Gradle 의존성 잠금 파일(gradle.lockfile) 도입으로 의존성 버전을 명시적으로 고정하고, 정기적으로 갱신 검증을 수행한다.
  2. CI 파이프라인에 의존성 취약점 스캔(SCA, 예: Snyk, OWASP Dependency-Check)을 추가하여 보안 취약점을 자동으로 감지한다.
  3. gradle.properties에서 Spring Boot 버전(3.4.4)을 명시하고, 별도 문서에 spring-security-crypto의 결정된 버전을 기록한다.
🤖 Prompt for AI Agents
In `@apps/commerce-api/build.gradle.kts` around lines 14 - 15, The build currently
depends on "org.springframework.security:spring-security-crypto" without an
explicit resolved version or SCA in CI; enable Gradle dependency locking by
generating and committing gradle.lockfile, add a springBootVersion property in
gradle.properties (e.g. 3.4.4) so BOM resolution is explicit, record the
resolved version of spring-security-crypto in your project docs or a
DEPENDENCIES.md, and add an SCA step (Snyk or OWASP Dependency-Check) to the CI
pipeline to fail or warn on known vulnerabilities so the dependency and its
versions are regularly scanned and validated.


// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.loopers.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.loopers.user.controller;

import com.loopers.user.domain.User;
import com.loopers.user.dto.ChangePasswordRequest;
import com.loopers.user.dto.CreateUserRequest;
import com.loopers.user.dto.CreateUserResponse;
import com.loopers.user.dto.GetMyInfoResponse;
import com.loopers.user.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

public static final String LOGIN_ID_HEADER = "X-Loopers-LoginId";
public static final String LOGIN_PW_HEADER = "X-Loopers-LoginPw";

private final UserService userService;

@PostMapping
public ResponseEntity<CreateUserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 164


🏁 Script executed:

find apps/commerce-api -name "*ApiControllerAdvice*" -o -name "*ControllerAdvice*" | head -20

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

Length of output: 168


🏁 Script executed:

find apps/commerce-api -path "*/config/*" -name "*.java" | grep -i advice

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

Length of output: 67


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java

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

Length of output: 2195


🏁 Script executed:

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

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

Length of output: 6820


🏁 Script executed:

find apps/commerce-api -name "CreateUserRequest.java" -o -name "ChangePasswordRequest.java"

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

Length of output: 240


🏁 Script executed:

find apps/commerce-api -path "*/test/*" -name "*UserController*Test*.java"

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

Length of output: 168


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java

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

Length of output: 765


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java

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

Length of output: 315


🏁 Script executed:

cat -n apps/commerce-api/src/test/java/com/loopers/user/controller/UserControllerTest.java

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

Length of output: 5536


🏁 Script executed:

find apps/commerce-api -name "GlobalExceptionHandler.java"

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

Length of output: 171


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/user/exception/GlobalExceptionHandler.java

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

Length of output: 2606


🏁 Script executed:

find apps/commerce-api -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" | head -1 | xargs cat | grep -A 5 -B 5 "spring-boot\|spring-webmvc" | head -40

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

Length of output: 606


🏁 Script executed:

rg "MethodArgumentNotValidException" apps/commerce-api

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

Length of output: 576


@Valid 제거하고 응답 포맷 일관성 확보 필요

@Valid 사용으로 MethodArgumentNotValidException이 GlobalExceptionHandler를 통해 Map 형식으로 응답하나, 다른 에러는 ApiControllerAdvice의 ApiResponse 형식으로 반환되어 클라이언트 파싱 일관성이 깨진다. 응답 포맷을 ApiResponse로 통일하거나, 요청값 검증을 서비스/도메인 계층(User 생성자, PasswordPolicy)으로 이동하여 모든 에러가 CoreException을 거쳐 ApiControllerAdvice로 처리되도록 개선해야 한다.

또한 비밀번호를 X-Loopers-LoginPw 헤더로 전달하면 로그, 프록시, APM에서 민감정보가 노출될 수 있으므로 요청 본문으로 이동하는 것이 필요하다.

수정 제안
-import jakarta.validation.Valid;
@@
-    public ResponseEntity<CreateUserResponse> createUser(`@Valid` `@RequestBody` CreateUserRequest request) {
+    public ResponseEntity<CreateUserResponse> createUser(`@RequestBody` CreateUserRequest request) {
@@
-    public ResponseEntity<Void> changePassword(
+    public ResponseEntity<Void> changePassword(
             `@RequestHeader`(LOGIN_ID_HEADER) String loginId,
-            `@RequestHeader`(LOGIN_PW_HEADER) String currentPassword,
-            `@Valid` `@RequestBody` ChangePasswordRequest request
+            `@RequestBody` ChangePasswordRequest request
     ) {
-        userService.changePassword(loginId, currentPassword, request.newPassword());
+        userService.changePassword(loginId, request.currentPassword(), request.newPassword());

User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(CreateUserResponse.from(user));
}

@GetMapping("/me")
public ResponseEntity<GetMyInfoResponse> getMyInfo(
@RequestHeader(LOGIN_ID_HEADER) String loginId
) {
GetMyInfoResponse response = userService.getMyInfo(loginId);
return ResponseEntity.ok(response);
}

@PatchMapping("/password")
public ResponseEntity<Void> changePassword(
@RequestHeader(LOGIN_ID_HEADER) String loginId,
@RequestHeader(LOGIN_PW_HEADER) String currentPassword,
@Valid @RequestBody ChangePasswordRequest request
Comment on lines +41 to +43
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

비밀번호를 헤더로 전달하면 노출 위험이 큼
운영 관점에서 요청 헤더는 게이트웨이/프록시/접근 로그/APM에 기본 수집되는 경우가 많아 비밀번호가 평문으로 남을 위험이 크다.
수정안으로 ChangePasswordRequest에 currentPassword를 추가해 바디로 전달하고 헤더 파라미터 및 상수를 제거하는 방식이 안전하다.
추가 테스트로 currentPassword가 바디에 있을 때만 성공하고 누락 시 4xx로 실패하는지 컨트롤러 테스트를 추가하는 것이 필요하다.

수정 제안
-    public ResponseEntity<Void> changePassword(
-            `@RequestHeader`(LOGIN_ID_HEADER) String loginId,
-            `@RequestHeader`(LOGIN_PW_HEADER) String currentPassword,
-            `@RequestBody` ChangePasswordRequest request
-    ) {
-        userService.changePassword(loginId, currentPassword, request.newPassword());
+    public ResponseEntity<Void> changePassword(
+            `@RequestHeader`(LOGIN_ID_HEADER) String loginId,
+            `@RequestBody` ChangePasswordRequest request
+    ) {
+        userService.changePassword(loginId, request.currentPassword(), request.newPassword());
         return ResponseEntity.ok().build();
     }

As per coding guidelines '로깅 시 민감정보 노출 가능성을 점검한다.'를 따랐다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java`
around lines 41 - 43, The controller currently accepts currentPassword via
request header (see UserController method signature using
`@RequestHeader`(LOGIN_PW_HEADER) and LOGIN_ID_HEADER) which risks leaking
credentials; change the API to require currentPassword inside
ChangePasswordRequest (add a validated field currentPassword to the
ChangePasswordRequest DTO), remove the `@RequestHeader` parameters and any LOGIN_*
header constants usage from the controller method (update UserController to read
currentPassword from the request body instead), add validation annotations so
missing currentPassword triggers 4xx, and add a controller test that posts a
ChangePasswordRequest with and without currentPassword verifying success and 4xx
failure respectively.

) {
userService.changePassword(loginId, currentPassword, request.newPassword());
return ResponseEntity.ok().build();
}
}
62 changes: 62 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/user/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.loopers.user.domain;

import com.loopers.domain.BaseEntity;
import jakarta.persistence.*;
import org.springframework.util.Assert;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {

@Column(nullable = false, length = 10, unique = true)
private String loginId;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private String birthDate;

@Column(nullable = false)
private String email;

@Builder
public User(String loginId, String password, String name, String birthDate, String email) {
validateRequired(loginId, password, name, birthDate, email);

this.loginId = loginId;
this.password = password;
this.name = name;
this.birthDate = birthDate;
this.email = email;
}

private void validateRequired(String loginId, String password, String name, String birthDate, String email) {
Assert.hasText(loginId, "loginId는 필수입니다");
Assert.hasText(password, "password는 필수입니다");
Assert.hasText(name, "name은 필수입니다");
Assert.hasText(birthDate, "birthDate는 필수입니다");
Assert.hasText(email, "email은 필수입니다");
}

public String getMaskedName() {
if (name.length() == 1) {
return "*";
}
return name.substring(0, name.length() - 1) + "*";
}

public void changePassword(String newPassword) {
Assert.hasText(newPassword, "새 비밀번호는 필수입니다");
this.password = newPassword;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.user.dto;

import jakarta.validation.constraints.NotBlank;

public record ChangePasswordRequest(
@NotBlank
String newPassword
Comment on lines +3 to +7
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. ChangePasswordRequest 파일 확인
find . -name "ChangePasswordRequest.java" -type f | head -5

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

Length of output: 166


🏁 Script executed:

#!/bin/bash
# 2. 프로젝트 전체 DTO의 Bean Validation 사용 패턴 확인
rg -n '@NotBlank|@Email|@Pattern|@Valid|jakarta\.validation' -g '*.java' -A 2 -B 2 | head -100

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

Length of output: 6330


🏁 Script executed:

#!/bin/bash
# 3. 프로젝트 구조와 User 관련 서비스/도메인 파일 확인
find . -path '*/user/*' -name '*.java' -type f | grep -E '(Service|Domain|Policy)' | head -20

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

Length of output: 331


🏁 Script executed:

#!/bin/bash
# 4. UserService의 changePassword 메서드 확인
cat -n ./apps/commerce-api/src/main/java/com/loopers/user/service/UserService.java

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

Length of output: 3194


🏁 Script executed:

#!/bin/bash
# 5. PasswordPolicy 파일 찾기 및 내용 확인
find . -name "*PasswordPolicy*" -type f

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

Length of output: 67


🏁 Script executed:

#!/bin/bash
# 6. 에러 핸들링 구조 확인 - CoreException과 ApiControllerAdvice 패턴
find . -name "*ControllerAdvice*" -o -name "*CoreException*" | head -10

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

Length of output: 332


🏁 Script executed:

#!/bin/bash
# 7. ApiControllerAdvice 확인 - MethodArgumentNotValidException 핸들링 여부
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

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

Length of output: 6820


🏁 Script executed:

#!/bin/bash
# 8. PasswordValidator 확인
find . -name "PasswordValidator.java" -type f | xargs cat -n

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

Length of output: 1039


ChangePasswordRequest의 @NotBlank를 제거하고, PasswordValidator 예외를 CoreException으로 매핑해야 한다

@NotBlank는 MethodArgumentNotValidException을 유발하는데, ApiControllerAdvice에 이를 처리하는 핸들러가 없어서 generic Throwable 핸들러에 의해 INTERNAL_ERROR로 처리된다. 또한 도메인 계층의 PasswordValidator.validate()도 IllegalArgumentException을 던져서 동일하게 INTERNAL_ERROR로 처리되므로, 검증 에러의 응답 상태 코드와 형식이 일관되지 않는다.

수정안으로 다음 항목이 필요하다:

  • DTO에서 @NotBlank 제거
  • PasswordValidator 예외를 CoreException(BadRequest, "비밀번호 검증 실패") 등으로 래핑하여 통일된 에러 핸들링 흐름으로 변경
  • UserController의 @Valid 제거

추가 테스트로 newPassword가 null/blank/정책 위배인 경우 각각 BAD_REQUEST 상태 코드와 통일된 ApiResponse 형식으로 응답되는지 검증해야 한다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java`
around lines 3 - 7, Change ChangePasswordRequest by removing the `@NotBlank`
annotation on newPassword and remove the `@Valid` usage from the relevant
UserController endpoint so DTO-level JSR validation is not used; update
PasswordValidator.validate (or the caller in UserController) to catch
IllegalArgumentException (or any validation exception) and rethrow a
CoreException.badRequest("비밀번호 검증 실패") (or construct CoreException with
BadRequest semantics) so all password validation failures are mapped to a
unified CoreException flow; ensure references to ChangePasswordRequest,
PasswordValidator.validate, CoreException and UserController/@Valid are updated
and add tests verifying null/blank/policy-violating newPassword each return
BAD_REQUEST with the unified ApiResponse format.

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.loopers.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record CreateUserRequest(
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9]+$")
String loginId,
@NotBlank
String password,
@NotBlank
String name,
@NotBlank
@Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}")
String birthDate,
@Email
@NotBlank
String email
Comment on lines +3 to +20
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# User 엔티티 찾기
fd -e java User | grep -E '(entity|domain)' | head -20

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

Length of output: 220


🏁 Script executed:

# CreateUserRequest 파일 확인
fd CreateUserRequest -e java

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

Length of output: 257


🏁 Script executed:

# UserService 찾기
fd UserService -e java

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

Length of output: 325


🏁 Script executed:

# 프로젝트의 Bean Validation 사용 현황 전체 파악
rg 'jakarta\.validation|@NotBlank|@Email|@Pattern' -g '*.java' -A 2 -B 2 | head -80

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

Length of output: 4839


🏁 Script executed:

# 다른 DTO들의 검증 패턴 확인
fd -e java -path '*/dto/*' | head -20

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

Length of output: 253


🏁 Script executed:

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

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

Length of output: 2253


🏁 Script executed:

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

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

Length of output: 3194


🏁 Script executed:

cat -n apps/commerce-api/src/test/java/com/loopers/user/controller/CreateUserRequestValidationTest.java

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

Length of output: 5154


🏁 Script executed:

fd PasswordValidator -e java

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

Length of output: 252


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/user/validator/PasswordValidator.java

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

Length of output: 1039


DTO의 Bean Validation 어노테이션을 제거하고 도메인 레이어 검증으로 일원화해야 한다

현재 @NotBlank, @Pattern, @Email은 @Valid 경로에서만 MethodArgumentNotValidException을 발생시키므로, 컨트롤러를 거치지 않고 UserService를 직접 호출하는 다른 진입점에서는 loginId·email·birthDate 형식 검증이 누락된다. 이는 CoreException 통일 경로를 벗어나 일관성 없는 에러 응답을 야기한다.

수정안으로 다음을 진행해야 한다:

  1. DTO에서 검증 어노테이션 제거
  2. PasswordValidator 패턴을 따라 LoginIdValidator, BirthDateValidator, EmailValidator를 domain/validator 패키지에 작성하고 UserService.createUser()에서 호출
  3. 형식 검증 실패 시 IllegalArgumentException 또는 도메인 예외 발생
  4. CreateUserRequestValidationTest는 제거하고, UserServiceTest에서 잘못된 형식 입력 시 예외 발생 여부를 검증
🛠️ 수정 예시
-import jakarta.validation.constraints.Email;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.Pattern;
-
 public record CreateUserRequest(
-        `@NotBlank`
-        `@Pattern`(regexp = "^[a-zA-Z0-9]+$")
         String loginId,
-        `@NotBlank`
         String password,
-        `@NotBlank`
         String name,
-        `@NotBlank`
-        `@Pattern`(regexp = "\\d{4}-\\d{2}-\\d{2}")
         String birthDate,
-        `@Email`
-        `@NotBlank`
         String email
 ) {
 }

UserService.createUser()에 검증 로직 추가:

public User createUser(CreateUserRequest request) {
    LoginIdValidator.validate(request.loginId());      // 추가
    BirthDateValidator.validate(request.birthDate());  // 추가
    EmailValidator.validate(request.email());          // 추가
    
    if(userRepository.existsByLoginId(request.loginId())){
        throw new DuplicateLoginIdException();
    }
    PasswordValidator.validate(request.password(), request.birthDate());
    // ...
}
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java`
around lines 3 - 20, Remove all Jakarta Bean Validation annotations from the
CreateUserRequest DTO and centralize format checks in the domain layer:
implement LoginIdValidator, BirthDateValidator, and EmailValidator in
domain/validator (modeled after the existing PasswordValidator) and call them
from UserService.createUser(...) before any repository operations; on validation
failure throw IllegalArgumentException or a domain-specific exception (e.g.,
InvalidFormatException). Also delete CreateUserRequestValidationTest and move
its assertions into UserServiceTest to assert that invalid loginId, birthDate,
email, and password inputs cause the expected exceptions. Ensure method names
referenced are LoginIdValidator.validate(...), BirthDateValidator.validate(...),
EmailValidator.validate(...), and PasswordValidator.validate(...), and that
UserService.createUser(...) invokes them in that order prior to checking
duplicates.

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.loopers.user.dto;

import com.loopers.user.domain.User;

public record CreateUserResponse(
Long id,
String loginId,
String name,
String email,
String birthDate
) {
public static CreateUserResponse from(User user) {
return new CreateUserResponse(
user.getId(),
user.getLoginId(),
user.getName(),
user.getEmail(),
user.getBirthDate()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.loopers.user.dto;

import com.loopers.user.domain.User;

public record GetMyInfoResponse(
String loginId,
String name,
String birthDate,
String email
) {
public static GetMyInfoResponse from(User user) {
return new GetMyInfoResponse(
user.getLoginId(),
user.getMaskedName(),
user.getBirthDate(),
user.getEmail()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers.user.exception;

public class DuplicateLoginIdException extends RuntimeException {

private static final String DEFAULT_MESSAGE = "이미 사용 중인 로그인 ID입니다.";

public DuplicateLoginIdException() {
super(DEFAULT_MESSAGE);
}

public DuplicateLoginIdException(String message) {
super(message);
}
Comment on lines +3 to +13
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

CoreException 경로로 통일해 예외 응답 포맷을 보장해야 한다
운영 관점에서 RuntimeException 직접 사용은 ApiControllerAdvice의 공통 응답 포맷을 우회하거나 500으로 매핑될 위험이 있어 모니터링과 클라이언트 오류 처리 일관성이 깨진다.
수정안으로 CoreException을 상속하고 에러 코드/HTTP 상태 매핑에 맞춘 생성자를 제공하며, 필요하면 cause를 보존하도록 추가 생성자를 두어야 한다.
추가 테스트로 중복 loginId 가입 요청 시 409와 공통 에러 바디 포맷이 유지되는지 통합 테스트로 검증해야 한다.

Based on learnings: In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format. Do not introduce MethodArgumentNotValidException handlers or Bean Validation handling, as that would create inconsistent error handling patterns. This guidance applies to all Java files under the commerce-api module (source code under apps/commerce-api/src/main/java/).

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/user/exception/DuplicateLoginIdException.java`
around lines 3 - 13, Replace DuplicateLoginIdException to extend CoreException
(not RuntimeException) and add constructors that accept (String errorCode,
HttpStatus httpStatus, String message) or at minimum preserve the
DEFAULT_MESSAGE while mapping the appropriate error code/HTTP status for
duplicate-login (409); add an additional constructor that accepts a Throwable
cause to preserve original exceptions. Ensure the class references CoreException
and ApiControllerAdvice handling conventions used elsewhere so the common error
body is returned; update/ensure any places that throw new
DuplicateLoginIdException(...) pass the required error code/status signature.
Finally, add an integration test that triggers a duplicate sign-up and asserts a
409 response and that the response body matches the ApiControllerAdvice common
error format.

}
Loading