Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c0dcfd8
remove: deprecated codeguide
hanyoung-kurly Feb 1, 2026
83c0fa6
feat: 회원가입 시 비밀번호 암호화 로직 추가
hey-sion Feb 2, 2026
62db8d0
feat: 회원가입 로그인 ID 영문/숫자 검증 추가
hey-sion Feb 2, 2026
c45bbb5
refactor: UserServiceTest를 통합 테스트 구조로 변경하기 위한 수정
hey-sion Feb 2, 2026
79683a6
feat: User 엔티티 회원가입 필드 검증 로직 추가
hey-sion Feb 3, 2026
a5392aa
feat: 회원가입 통합테스트 및 비밀번호 검증 로직 구현
hey-sion Feb 3, 2026
f1897e8
test: UserService 테스트 성격에 맞게 클래스명 변경
hey-sion Feb 3, 2026
f030d42
fix: TestContainers 설정에 @Profile("test") 추가
hey-sion Feb 3, 2026
adad0bd
feat: BCrypt 비밀번호 암호화 구현체 추가
hey-sion Feb 3, 2026
f4dbb64
feat: 회원가입 DB 저장 통합테스트 추가
hey-sion Feb 3, 2026
081a4e4
refactor: 서비스 레이어 DTO를 RequestV1에서 Command로 변경
hey-sion Feb 4, 2026
a3e884a
chore: 누락된 파일 추가
hey-sion Feb 4, 2026
cf65c84
refactor: birthDate 타입을 LocalDate로 변경
hey-sion Feb 4, 2026
3c36e5e
feat: Bean Validation 및 예외 핸들러 추가
hey-sion Feb 4, 2026
5eec4e9
refactor: 요청 dto 빈값체크 어노테이션 변경 @NotNull -> @NotBlank
hey-sion Feb 4, 2026
ad2ee7f
refactor: 검증 로직을 SignUpValidator로 분리하고 User 팩토리 메서드 적용
hey-sion Feb 4, 2026
dc7df5c
refactor: 기존 UserServiceTest의 일부 테스트 메서드 위치 SignUpValidatorTest로 이동
hey-sion Feb 4, 2026
b63e546
refactor: SignUpCommand에 from 팩토리 메서드 추가
hey-sion Feb 4, 2026
74dd37b
refactor: birthDate 검증을 NotBlank에서 NotNull로 수정
hey-sion Feb 4, 2026
0f7fa42
feat: 회원가입 컨트롤러 및 E2E 테스트 추가
hey-sion Feb 4, 2026
6834d10
feat: command와 암호화된 비밀번호를 가지고 User를 생성하는 팩토리 메서드 추가
hey-sion Feb 4, 2026
91c9966
refactor: UserService -> SignUpService로 클래스명 변경
hey-sion Feb 4, 2026
60aca5f
refactor: UserFixture 내에 비밀번호는 암호화된 비밀번호여야 하므로 상수명 변경
hey-sion Feb 4, 2026
b4031b7
feat: 내 정보 조회 기능 추가 및 단위테스트 구현
hey-sion Feb 4, 2026
7e7e913
feat: 회원가입 서비스 클래스 레벨에 @Transactional 추가
hey-sion Feb 4, 2026
503a581
refactor: 테스트 클래스 내 불필요한 프로필 설정 제거 및 DisplayName 개선
hey-sion Feb 5, 2026
cf55e49
feat: 비밀번호 변경 기능 추가 및 PasswordPolicyValidator 분리
hey-sion Feb 5, 2026
7fe825e
chore: 코드 라인 정리
hey-sion Feb 5, 2026
8182abb
feat: 내 정보 조회 controller 구현 및 테스트 코드 추가
hey-sion Feb 6, 2026
f3b968c
feat: 내 정보 조회 controller 구현 및 테스트 코드 추가
hey-sion Feb 6, 2026
56c0a6b
feat: 비밀번호 변경 구현 및 테스트 코드 추가
hey-sion Feb 6, 2026
5febf4a
refactor: User 조회 메서드 분리
hey-sion Feb 6, 2026
37e4574
chore: 코드 라인 정리
hey-sion Feb 6, 2026
b44396c
refactor: 메서드명 및 위치 변경
hey-sion Feb 6, 2026
b468aa9
refactor: 클래스위치 변경
hey-sion Feb 6, 2026
b566e9f
chore: 코드 라인 정리
hey-sion Feb 6, 2026
8b1dca9
refactor: 불필요한 profile 값 제거
hey-sion Feb 6, 2026
24511a7
refactor: 비밀번호 변경 로직 개선 및 테스트 코드 수정
hey-sion Feb 10, 2026
9f4c1dd
refactor: User 엔티티 loginId unique 제약 추가 및 중복 필드 제거
hey-sion Feb 10, 2026
7c681b9
refactor: User 테이블명 변경(users) 및 불필요한 import 제거
hey-sion Feb 10, 2026
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
45 changes: 0 additions & 45 deletions .codeguide/loopers-1-week.md

This file was deleted.

1 change: 1 addition & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {

// web
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.security:spring-security-crypto")
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:

cd apps/commerce-api && cat -n build.gradle.kts | head -30

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

Length of output: 1238


🏁 Script executed:

cd apps/commerce-api && grep -i "spring\|bom\|version" build.gradle.kts | head -20

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

Length of output: 426


🌐 Web query:

spring-security-crypto latest version 2025 2026 CVE vulnerability

💡 Result:

Latest org.springframework.security:spring-security-crypto versions (2025–2026)

  • Latest stable: 7.0.2 (released to Maven Central 2025-12-15) [1][2]
  • Latest pre-release (milestone): 7.1.0-M1 (published 2026-01-19) [1]

Spring Security CVEs relevant to spring-security-crypto (2025)

  • CVE-2025-22228 (HIGH)BCryptPasswordEncoder.matches may incorrectly return true for passwords >72 chars when the first 72 chars match.

    • Affects Spring Security 6.4.0–6.4.3, 6.3.0–6.3.7, etc.
    • Fixed in 6.4.4 and 6.3.8 (OSS lines) [3]
  • CVE-2025-22234 (MEDIUM) – Fix for CVE-2025-22228 inadvertently broke DaoAuthenticationProvider timing-attack mitigation (can leak info via response-time differences).

    • Affects specific versions: 6.4.4 only, 6.3.8 only, etc.
    • Fixed in 6.4.5 and 6.3.9 (OSS lines) [4][5]

Related (not crypto-specific, but in Spring Security)

  • CVE-2025-41248 (MEDIUM) – method-security annotation resolution bug can cause authorization bypass in some generic type hierarchies; fixed in 6.4.11 / 6.5.5 [6][7]

If you’re on 7.0.2, you’re already beyond the vulnerable ranges for CVE-2025-22228 and the “bad” intermediate versions for CVE-2025-22234. [1][3][4]

Sources

[1] Maven Central directory listing for spring-security-crypto versions (shows 7.0.2 and 7.1.0-M1 dates)
[2] Spring blog: Spring Security 7.0.1 / 7.0.2 releases
[3] Spring advisory: CVE-2025-22228
[4] Spring advisory: CVE-2025-22234
[5] Snyk advisory for CVE-2025-22234 (fix versions)
[6] Spring advisory: CVE-2025-41248
[7] Spring blog: Fix releases for CVE-2025-41248 / CVE-2025-41249


🏁 Script executed:

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

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

Length of output: 1238


🏁 Script executed:

find . -maxdepth 3 -name "build.gradle.kts" -o -name "gradle.properties" -o -name "settings.gradle.kts" | head -10

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

Length of output: 396


🏁 Script executed:

cat -n build.gradle.kts 2>/dev/null | head -40

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

Length of output: 1335


🏁 Script executed:

cat -n gradle.properties 2>/dev/null | head -30

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

Length of output: 747


🏁 Script executed:

rg "spring.*boot.*version|springBootVersion" --type kotlin --type properties -i | head -20

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

Length of output: 119


🏁 Script executed:

cat -n build.gradle.kts | head -80

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

Length of output: 3363


🏁 Script executed:

rg "spring-security|dependencyManagement" -A 5 -B 5 build.gradle.kts

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

Length of output: 436


🌐 Web query:

Spring Boot 3.4.4 spring-security version managed by BOM

💡 Result:

Spring Boot 3.4.4 (via the org.springframework.boot:spring-boot-dependencies:3.4.4 BOM) manages Spring Security 6.4.4 (i.e., spring-security-bom / spring-security-core, etc.). [1]

If you need to override it, set the BOM property spring-security.version. [2]

Sources:
[1] Spring Boot v3.4.4 release notes (“Upgrade to Spring Security 6.4.4”) (github.com)
[2] Spring Security reference docs (“override … with <spring-security.version>”) (docs.spring.io)

Citations:


🌐 Web query:

Spring Boot 3.4.4 release notes spring-security-crypto version

💡 Result:

Spring Boot 3.4.4 manages Spring Security via org.springframework.security:spring-security-bom version 6.4.4 [1]. Since spring-security-crypto is part of that BOM, the managed org.springframework.security:spring-security-crypto version is 6.4.4 [1]. (Spring Security 6.4.4 was released on 2025-03-19) [2].

Sources: [1] Spring Boot 3.4.4 BOM managed dependencies (MvnRepository), [2] Spring Security 6.4.4 release announcement.


Spring Security 6.4.4의 CVE-2025-22234 취약점으로 인한 명시적 버전 관리 필요

현재 Spring Boot 3.4.4 BOM은 spring-security-crypto 6.4.4를 관리하고 있으며, 이 버전은 타이밍 공격 취약점 CVE-2025-22234(MEDIUM)에 노출되어 있다. BOM 의존만으로는 이 취약점이 해결되지 않으므로, 다음 중 하나를 수행해야 한다:

  • spring-security-crypto를 6.4.5 이상으로 명시적으로 고정하거나
  • Spring Boot를 3.4.5 이상으로 업그레이드하여 BOM에서 관리하는 spring-security-crypto 버전을 상향한다.

추가적으로 CI에서 의존성 리졸브 결과를 검증하고 보안 스캔을 통해 CVE 해결 여부를 확인해야 한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/build.gradle.kts` at line 11, The spring-security-crypto
dependency is managed by the Spring Boot 3.4.4 BOM which pins it to 6.4.4
(CVE-2025-22234); fix it by explicitly overriding the dependency version or
bumping the BOM: either change the dependency
implementation("org.springframework.security:spring-security-crypto") to include
a safe version (e.g.,
implementation("org.springframework.security:spring-security-crypto:6.4.5")) or
update the Spring Boot BOM/platform coordinate to 3.4.5+ so the BOM supplies a
fixed spring-security-crypto, and add a CI step to validate the resolved
dependency tree and run your security scanner to confirm the CVE is no longer
present.

implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.user;

import com.loopers.interfaces.api.user.dto.UserV1Dto;

import java.time.LocalDate;

public record SignUpCommand(
String loginId,
String password,
String name,
LocalDate birthDate,
String email
Comment on lines +7 to +12
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 166


🏁 Script executed:

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

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

Length of output: 818


Record의 toString() 메서드에서 평문 비밀번호가 노출될 수 있다

Java record는 자동으로 생성되는 toString() 메서드가 모든 필드를 포함하므로, 예외 발생 시 스택 트레이스나 로깅 시스템에 이 객체가 출력되면 password가 평문으로 남는다. 이는 보안 인사고(Credential Exposure), 규제 위반, 감시 로그 분석 시 민감정보 노출로 이어질 수 있다.

수정안: Record 내부에 toString() 메서드를 오버라이드하여 password를 마스킹한다.

추가 테스트: toString() 메서드의 결과에 실제 비밀번호가 포함되지 않음을 검증하는 단위 테스트를 작성한다.

수정 예시
 public record SignUpCommand(
         String loginId,
         String password,
         String name,
         LocalDate birthDate,
         String email
 ) {
+    `@Override`
+    public String toString() {
+        return "SignUpCommand[loginId=%s,password=***,name=%s,birthDate=%s,email=%s]"
+                .formatted(loginId, name, birthDate, email);
+    }
     public static SignUpCommand from(UserV1Dto.CreateRequest request) {
         return new SignUpCommand(
                 request.loginId(),
                 request.password(),
                 request.name(),
                 request.birthDate(),
                 request.email()
         );
     }
 }
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java`
around lines 7 - 12, SignUpCommand 레코드의 자동 생성된 toString()가 평문 password를 노출하므로
SignUpCommand에 toString()를 오버라이드해 password 값을 마스킹(예: "****" 또는 부분 마스킹)하도록 변경하고
나머지 필드만 출력하도록 구현하세요; 대상 식별자는 record SignUpCommand(...)와 필드 password 및 메서드
toString()입니다. 또한 SignUpCommandToStringMasking 같은 단위 테스트를 추가해 toString() 반환값에 실제
비밀번호가 포함되지 않는지(assertFalse(result.contains(actualPassword))) 검증하도록 작성하세요.

) {
public static SignUpCommand from(UserV1Dto.CreateRequest request) {
return new SignUpCommand(
request.loginId(),
request.password(),
request.name(),
request.birthDate(),
request.email()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.loopers.application.user;

import com.loopers.interfaces.api.user.dto.UserV1Dto;

public record UpdatePasswordCommand(
String loginId,
String currentPassword,
String newPassword
) {
Comment on lines 5 to 9
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 174


🏁 Script executed:

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

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

Length of output: 532


🏁 Script executed:

# 이 record가 실제로 로그 또는 예외 처리에서 사용되는지 확인
rg "UpdatePasswordCommand" --type java -B 2 -A 2 | head -100

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

Length of output: 5978


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java | grep -A 20 "updatePassword"

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

Length of output: 1223


record의 기본 toString()이 평문 비밀번호를 노출한다

현재 UpdatePasswordCommand record는 toString() 메서드를 오버라이드하지 않아 Java 기본 구현이 사용된다. 이 경우 예외 발생 시 스택트레이스나 로깅 시스템에서 객체를 문자열로 변환할 때 newPassword가 평문으로 노출되어 보안/컬플라이언스 사고로 이어질 수 있다.

운영 관점에서는 Spring 프레임워크의 예외 처리, 디버그 로깅, 또는 모니터링 시스템이 자동으로 toString()을 호출하므로 실제 위험이 존재한다.

수정안: toString() 메서드를 오버라이드해 비밀번호를 마스킹한다.

수정 예시
 public record UpdatePasswordCommand(
     String loginId,
     String newPassword
 ) {
+    `@Override`
+    public String toString() {
+        return "UpdatePasswordCommand[loginId=%s,newPassword=***]".formatted(loginId);
+    }
     public static UpdatePasswordCommand from(String loginId, UserV1Dto.UpdatePasswordRequest request) {
         return new UpdatePasswordCommand(loginId, request.newPassword());
     }
 }

추가로 toString() 결과에 비밀번호가 포함되지 않는지 검증하는 단위 테스트를 작성한다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java`
around lines 5 - 8, UpdatePasswordCommand 레코드의 기본 toString()가 평문 newPassword를
노출하므로 toString()을 오버라이드해서 비밀번호를 마스킹하거나 제외하도록 변경하세요 (예: 클래스
UpdatePasswordCommand의 toString()에서 loginId만 노출하거나 newPassword를 "****"로 대체). 변경할
위치는 UpdatePasswordCommand 레코드 선언부이며 toString() 오버라이드 구현과 함께 toString() 결과에 실제
newPassword가 포함되지 않음을 검증하는 단위 테스트를 추가하세요 (테스트는 new
UpdatePasswordCommand(...).toString()가 원본 newPassword 문구를 포함하지 않음을 assert).

public static UpdatePasswordCommand from(String loginId, String currentPassword, UserV1Dto.UpdatePasswordRequest request) {
return new UpdatePasswordCommand(loginId, currentPassword, request.newPassword());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.user;

import com.loopers.domain.user.User;

import java.time.LocalDate;

public record UserInfo(String loginId, String name, LocalDate birthDate, String email) {
public static UserInfo from(User user) {
return new UserInfo(
user.getLoginId(),
maskLastChar(user.getName()),
user.getBirthDate(),
user.getEmail()
);
}

private static String maskLastChar(String name) {
if (name == null || name.isBlank()) return name;
if (name.length() == 1) return "*";

return name.substring(0, name.length() - 1) + "*";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.domain.user;

public interface PasswordEncoder {
String encode(String rawPassword);
boolean matches(String rawPassword, String encodedPassword);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.loopers.domain.user;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class PasswordPolicyValidator {

private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$";

private PasswordPolicyValidator() {}

public static void validate(String password, LocalDate birthDate) {
if (!password.matches(PASSWORD_PATTERN)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자의 영문/숫자/특수문자만 가능합니다.");
}

String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK);
if (password.contains(birthDateString)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다.");
}
Comment on lines +15 to +23
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

passwordbirthDate가 null일 경우 NPE가 발생한다.

password.matches() 또는 birthDate.format() 호출 시 null 파라미터가 전달되면 NullPointerException이 발생하여 500 에러로 응답된다. 운영 환경에서 잘못된 요청에 대해 명확한 400 에러를 반환하려면 null 체크가 필요하다.

🛡️ 수정 제안
 public static void validate(String password, LocalDate birthDate) {
+    if (password == null || password.isBlank()) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수 입력 항목입니다.");
+    }
+    if (birthDate == null) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수 입력 항목입니다.");
+    }
+
     if (!password.matches(PASSWORD_PATTERN)) {
         throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자의 영문/숫자/특수문자만 가능합니다.");
     }
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java`
around lines 15 - 23, PasswordPolicyValidator.validate currently calls
password.matches(PASSWORD_PATTERN) and
birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK) without null checks which
can throw NPEs; add explicit null validation at the start of validate(String
password, LocalDate birthDate) to check both password and birthDate and if
either is null throw new CoreException(ErrorType.BAD_REQUEST, "<appropriate
message>") (e.g., "비밀번호는 필수입니다." / "생년월일은 필수입니다.") before using PASSWORD_PATTERN
or BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK so invalid requests return 400 instead
of a 500.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.domain.user;

import com.loopers.application.user.SignUpCommand;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@Transactional
@RequiredArgsConstructor
public class SignUpService {
private final SignUpValidator signUpValidator;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;

public void signUp(SignUpCommand command) {
signUpValidator.validate(command);
String encodedPassword = passwordEncoder.encode(command.password());
User user = User.create(command, encodedPassword);

userRepository.save(user);
Comment on lines +16 to +21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 161


🏁 Script executed:

find . -name "User.java" -type f | grep -E "domain|model"

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

Length of output: 152


🏁 Script executed:

find . -name "*Repository.java" | grep -i user

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

Length of output: 337


🏁 Script executed:

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

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

Length of output: 960


🏁 Script executed:

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

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

Length of output: 3782


🏁 Script executed:

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

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

Length of output: 314


🏁 Script executed:

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

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

Length of output: 163


🏁 Script executed:

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

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

Length of output: 447


🏁 Script executed:

find . -name "*.sql" -o -name "*Migration*.java" | head -20

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 1143


🏁 Script executed:

find . -path "*/resources/db/migration*" -o -path "*/schema*" | head -10

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

Length of output: 67


🏁 Script executed:

find . -name "CoreException.java" -o -name "ErrorType.java"

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

Length of output: 238


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java

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

Length of output: 949


🏁 Script executed:

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

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

Length of output: 741


🏁 Script executed:

grep -r "@UniqueConstraint\|@Column.*unique" --include="*.java" | head -20

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 741


🏁 Script executed:

find . -name "*ExceptionHandler*" -o -name "*ControllerAdvice*" | head -10

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

Length of output: 170


🏁 Script executed:

rg "DataIntegrityViolationException|ConstraintViolationException" --type java

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/interfaces/api/ApiControllerAdvice.java

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

Length of output: 7333


🏁 Script executed:

find . -path "*/resources/db/migration*" -type f | head -20

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

Length of output: 67


🏁 Script executed:

find . -type f \( -name "*.sql" -o -name "*schema*" -o -name "*ddl*" \) | head -20

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

Length of output: 67


🏁 Script executed:

rg "loginId|login_id" --type java -A 2 -B 2 | grep -E "@Column|@UniqueConstraint|@Index" | head -20

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

Length of output: 67


🏁 Script executed:

find . -type d -name "migration" -o -type d -name "flyway" -o -type d -name "liquibase"

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

Length of output: 67


🏁 Script executed:

find . -type f \( -name "*.java" -path "*test*" \) | xargs grep -l "signUp\|SignUp" | head -10

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

Length of output: 425


🏁 Script executed:

rg "concurrent|simultaneous|race|duplicate" --type java | grep -i test | head -10

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

Length of output: 544


🏁 Script executed:

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

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

Length of output: 2074


🏁 Script executed:

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

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

Length of output: 5507


🏁 Script executed:

cat -n ./apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java | head -150

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

Length of output: 5919


동시 요청 시 중복 loginId 저장을 방지하기 위해 데이터베이스 제약과 예외 처리가 필수다

현재 코드는 SignUpValidator에서 중복을 검증하지만, 검증과 저장 사이의 Race Condition으로 인해 동시 요청 시 동일 loginId가 중복 저장될 수 있다. 또한 데이터베이스 제약이 없으면 검증을 우회하는 다른 저장 경로에서도 중복이 발생할 수 있으며, 제약 위반 시 DataIntegrityViolationException이 발생하여 500 에러로 노출된다.

수정안:

  • User 엔티티의 loginId 필드에 @Column(unique = true) 추가하거나 데이터베이스 마이그레이션에서 UNIQUE 제약 설정
  • ApiControllerAdvice에 DataIntegrityViolationException 핸들러 추가하여 CoreException(CONFLICT)로 변환
  • 동일 loginId 동시 요청 시나리오를 테스트하는 통합 테스트 추가 (ExecutorService 또는 CountDownLatch 활용)
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java`
around lines 16 - 21, Add a UNIQUE constraint on the User.loginId (either add
`@Column`(unique = true) on the User entity or create a DB migration) to ensure
DB-level uniqueness, then catch DB constraint violations by adding a
DataIntegrityViolationException handler in ApiControllerAdvice that converts the
exception into a CoreException with CONFLICT; also update SignUpService.signUp
to remain as-is (keep signUpValidator.validate and userRepository.save) but rely
on the DB constraint for race conditions and add an integration test that
simulates concurrent signUp calls (use ExecutorService or CountDownLatch) to
assert only one user is created and others receive a conflict response.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.loopers.domain.user;

import com.loopers.application.user.SignUpCommand;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
@RequiredArgsConstructor
public class SignUpValidator {
private final UserRepository userRepository;

public void validate(SignUpCommand command) {
if (userRepository.findByLoginId(command.loginId()).isPresent()) {
throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.");
}

if (command.birthDate().isAfter(LocalDate.now())) {
throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 미래일 수 없습니다.");
}

PasswordPolicyValidator.validate(command.password(), command.birthDate());
Comment on lines +16 to +25
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

필수 값 null 입력 시 NPE로 500이 발생할 수 있다
운영 관점에서 loginId/password/birthDate가 누락되면 NPE로 일관된 CoreException 포맷을 깨고 500이 발생해 장애 분석과 클라이언트 처리에 문제가 된다.
수정안: validate 시작부에서 null/blank를 검사해 CoreException(BAD_REQUEST)로 변환한 뒤 나머지 검증을 수행한다.
추가 테스트: null/blank 입력에 대해 BAD_REQUEST를 기대하는 단위 테스트를 추가한다.

수정 예시
     public void validate(SignUpCommand command) {
+        if (command.loginId() == null || command.loginId().isBlank()) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 필수값입니다.");
+        }
+        if (command.password() == null || command.password().isBlank()) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수값입니다.");
+        }
+        if (command.birthDate() == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수값입니다.");
+        }
         if (userRepository.findByLoginId(command.loginId()).isPresent()) {
             throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.");
         }

         if (command.birthDate().isAfter(LocalDate.now())) {
             throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 미래일 수 없습니다.");
         }

         PasswordPolicyValidator.validate(command.password(), command.birthDate());
     }

As per coding guidelines: **/*.java: Java + Spring Boot 리뷰 기준이다. Optional/Stream 남용으로 가독성이 떨어지면 단순화하고, 예외 흐름이 명확한지 점검한다. null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다. 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다. 로깅 시 민감정보 노출 가능성을 점검한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java`
around lines 16 - 25, Add defensive null/blank checks at the start of
SignUpValidator.validate: verify command is non-null and its loginId and
password are non-null/non-blank and birthDate is non-null; if any check fails
throw new CoreException(ErrorType.BAD_REQUEST, "<field> is required") before
calling userRepository.findByLoginId, birthDate.isAfter or
PasswordPolicyValidator.validate so we avoid NPEs; update error messages to be
clear per field (loginId/password/birthDate) and add unit tests asserting
BAD_REQUEST is thrown for null/blank command.loginId(), command.password(),
command.birthDate() and for a null SignUpCommand.

}
}
95 changes: 95 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.loopers.domain.user;

import com.loopers.application.user.SignUpCommand;
import com.loopers.domain.BaseEntity;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;

import java.time.LocalDate;

@Getter
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Column(unique = true)
private String loginId;
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.java 파일의 전체 내용 확인
find . -path "*domain/user/User.java" -type f | head -5

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

Length of output: 152


🏁 Script executed:

# loginId 필드에 대한 `@Column` 애너테이션 확인
cd apps/commerce-api && find . -name "User.java" -path "*domain/user*" | xargs cat -n | head -50

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

Length of output: 1960


🏁 Script executed:

# 데이터베이스 마이그레이션/스키마 파일 검색
find . -type f \( -name "*migration*" -o -name "*schema*" -o -name "*.sql" \) | grep -i user

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

Length of output: 67


🏁 Script executed:

# 회원 가입 관련 서비스 로직 검색
find . -path "*domain/user*" -name "*.java" -type f | xargs grep -l "loginId"

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

Length of output: 869


🏁 Script executed:

# User.java 전체 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/User.java

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

Length of output: 3782


🏁 Script executed:

# UserService의 회원 가입 로직 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java

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

Length of output: 1906


🏁 Script executed:

# SignUpValidator 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java

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

Length of output: 1143


🏁 Script executed:

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

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

Length of output: 314


🏁 Script executed:

# 데이터베이스 마이그레이션/스키마 파일 검색 (모든 .sql, .xml 파일)
find . -type f \( -name "*.sql" -o -name "*.xml" \) | xargs grep -l "user" 2>/dev/null | head -10

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

Length of output: 151


🏁 Script executed:

# UserRepository 구현체 찾기
find . -name "*UserRepository*.java" -type f | xargs cat -n

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

Length of output: 1879


🏁 Script executed:

# 회원 가입 로직 찾기 (application layer)
find . -path "*application/user*" -name "*.java" -type f | head -10

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

Length of output: 333


🏁 Script executed:

# 데이터베이스 마이그레이션 또는 application.yml 파일 찾기
find . -type f \( -name "application*.yml" -o -name "application*.yaml" \)

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

Length of output: 256


🏁 Script executed:

# JPA Entity 설정 관련 파일 검색
find . -type f -name "*.java" | xargs grep -l "@Column(unique" 2>/dev/null | head -5

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

Length of output: 67


🏁 Script executed:

# 통합 테스트 코드 확인
cat -n apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java

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

Length of output: 3226


🏁 Script executed:

# UserJpaRepository 찾기
find . -name "UserJpaRepository.java" -type f | xargs cat -n

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

Length of output: 447


🏁 Script executed:

# 회원 가입 UseCase/Controller 찾기
find . -path "*application*" -name "*.java" -type f | grep -i signup | xargs cat -n

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

Length of output: 818


🏁 Script executed:

# 예외 처리 관련 코드 (DataIntegrityViolation 매핑)
find . -name "*.java" -type f | xargs grep -l "DataIntegrityViolation" 2>/dev/null

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

Length of output: 69


🏁 Script executed:

# 데이터베이스 스키마 설정 (application.yml)
cat -n apps/commerce-api/src/main/resources/application.yml

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

Length of output: 1450


🏁 Script executed:

# JPA 엔티티 관련 설정 확인 (BaseEntity)
find . -name "BaseEntity.java" -type f | xargs cat -n

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

Length of output: 2291


🏁 Script executed:

# jpa.yml 설정 확인
find . -name "jpa.yml" -type f | xargs cat -n

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

Length of output: 2903


🏁 Script executed:

# 회원 가입 UseCase 또는 Service 찾기
find . -path "*application*" -o -path "*interfaces*" | grep -i user | xargs grep -l "sign" 2>/dev/null | head -10

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

Length of output: 174


🏁 Script executed:

# 모든 Java 파일에서 signup 또는 SignUp 찾기
find . -name "*.java" -type f | xargs grep -l "SignUp" 2>/dev/null | head -15

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

Length of output: 821


🏁 Script executed:

# 예외 처리 관련 코드 (ErrorType, CoreException)
find . -name "ErrorType.java" -type f | xargs cat -n

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

Length of output: 949


🏁 Script executed:

# DataIntegrityViolationException 처리 관련
find . -name "*.java" -type f | xargs grep -l "Exception" 2>/dev/null | xargs grep -l "Handler\|handler\|advice" | head -5

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

Length of output: 170


🏁 Script executed:

# SignUpService 확인
find . -name "SignUpService.java" -type f | xargs cat -n

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

Length of output: 960


🏁 Script executed:

# SignUpV1Controller 확인
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java

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

Length of output: 1423


🏁 Script executed:

# ApiControllerAdvice 확인
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: 7333


🏁 Script executed:

# 통합 테스트 확인
cat -n apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java

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

Length of output: 2074


loginId에 DB 유니크 제약을 추가하고 예외 처리를 정의해야 한다

현재 SignUpValidator의 애플리케이션 레벨 중복 체크만으로는 동시 가입 요청 시 레이스 컨디션을 방지할 수 없다. T1이 유효성을 통과하고 저장하기 전에 T2도 유효성 검사를 통과하면 중복 계정이 생성된다. 운영 관점에서 이는 인증 오류, 계정 혼동 등 심각한 문제를 야기한다.

다음을 수정해야 한다:

  • loginId 필드에 @Column(unique = true) 또는 @Table(uniqueConstraints = @UniqueConstraint(columnNames = "loginId")) 추가
  • DB 인덱스 설정 (JPA 또는 마이그레이션 스크립트)
  • DataIntegrityViolationException을 CoreException으로 매핑하도록 ApiControllerAdvice에 핸들러 추가 (ErrorType.CONFLICT 반환)
  • 동시에 동일한 loginId로 2건의 가입 요청을 보냈을 때 하나만 성공하는지 검증하는 통합 테스트 추가
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java` at line
19, User.loginId에 DB 레벨 유니크 제약이 빠져 있어 동시 가입 레이스가 발생하므로 User 클래스의 loginId 필드에
`@Column`(unique = true) 또는 엔티티에 `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = "loginId")) 를 추가하고 DB 마이그레이션(또는 JPA 인덱스)으로 실제
유니크 인덱스를 생성하세요; 또한 ApiControllerAdvice에 DataIntegrityViolationException을 잡아
CoreException으로 변환해 ErrorType.CONFLICT를 반환하도록 핸들러를 추가(예: catch
DataIntegrityViolationException -> throw new CoreException(ErrorType.CONFLICT,
...))하고 SignUpValidator는 애플리케이션 레벨 체크로 유지하되 동시성 케이스를 검증하는 통합 테스트를 추가하여 동일한
loginId로 동시에 2건 요청시 하나만 저장되는지 확인하세요.

private String password; // encoded
private String name;
private LocalDate birthDate;
private String email;

protected User() {}

private User(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) {
validateLoginId(loginId);
validateEncodedPassword(encodedPassword);
validateName(name);
Comment on lines +27 to +30
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:

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

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

Length of output: 3189


🏁 Script executed:

find . -type f -name "*UserService*" | grep -E "\.java$"

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

Length of output: 328


🏁 Script executed:

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

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

Length of output: 1906


🏁 Script executed:

rg -l "SignUpCommand" --type java | head -5

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

Length of output: 461


🏁 Script executed:

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

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

Length of output: 960


🏁 Script executed:

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

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

Length of output: 1143


validateEncodedPassword() 메서드를 제거해야 한다

현재 구조에서 SignUpService는 PasswordPolicyValidator.validate() → passwordEncoder.encode() → User.create() 순서로 처리하므로, User 생성자에 전달되는 encodedPassword는 항상 유효한 BCrypt 해시다. 생성자에서 null/공백 검증은 서비스 계층의 책임 영역을 침범하며, 운영 중 암호화 정책 변경 시 도메인 엔티티까지 수정해야 하는 유지보수 리스크를 낳는다.

validateEncodedPassword() 메서드(60-64줄)를 제거하고, SignUpService와 UserService.updatePassword()에서 이미 준수 중인 정책 검증 → 인코딩 → 엔티티 생성 흐름을 명확히 유지해야 한다. 단위 테스트에서 서비스가 정책 검증 후 인코딩된 값만 User.create()로 전달하는지 확인하는 케이스를 추가로 보강해야 한다.

수정안
-        validateEncodedPassword(encodedPassword);
@@
-    private void validateEncodedPassword(String encodedPassword) {
-        if (encodedPassword == null || encodedPassword.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수값입니다.");
-        }
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private User(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) {
validateLoginId(loginId);
validateEncodedPassword(encodedPassword);
validateName(name);
private User(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) {
validateLoginId(loginId);
validateName(name);
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java` around
lines 27 - 30, 요약: User 생성자에서 validateEncodedPassword() 검증을 제거하세요; 수정: User 클래스의
private User(String loginId, String encodedPassword, ...) 생성자에서
validateEncodedPassword() 호출과 관련 메서드(예: validateEncodedPassword())를 삭제하고
encodedPassword에 대한 null/공백 검증을 제거해 도메인이 인코딩 정책에 의존하지 않게 만드세요; 또한 SignUpService와
UserService.updatePassword() 흐름(PasswordPolicyValidator.validate() →
passwordEncoder.encode() → User.create(...))이 정책 검증 후 인코딩된 값만 User.create()로
전달하도록 테스트를 보강해 주세요; 참고 심볼: validateEncodedPassword(), User.create(),
SignUpService, UserService.updatePassword().

validateBirthDate(birthDate);
validateEmail(email);

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

public static User create(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) {
return new User(loginId, encodedPassword, name, birthDate, email);
}

public static User create(SignUpCommand command, String encodedPassword) {
return new User(command.loginId(), encodedPassword, command.name(), command.birthDate(), command.email());

}

private void validateLoginId(String loginId) {
if (loginId == null || loginId.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 필수값입니다.");
}

if (!loginId.matches("^[a-zA-Z0-9]+$")) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 영문/숫자만 가능합니다.");
}
}

private void validateEncodedPassword(String encodedPassword) {
if (encodedPassword == null || encodedPassword.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수값입니다.");
}
}

private void validateName(String name) {
if (name == null || name.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "이름은 필수값입니다.");
}

if (name.length() < 2) {
throw new CoreException(ErrorType.BAD_REQUEST, "이름은 최소 두글자 이상이어야 합니다.");
}
}

private void validateBirthDate(LocalDate birthDate) {
if (birthDate == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수값입니다.");
}
}

private void validateEmail(String email) {
if (email == null || email.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 필수값입니다.");
}

if (!email.contains("@") || !email.matches("^[\\w\\.]+@[\\w\\.]+\\.[a-zA-Z]{2,}$")) {
throw new CoreException(ErrorType.BAD_REQUEST, "이메일 형식이 올바르지 않습니다.");
}
}

public void updatePassword(String newEncodedPassword) {
this.password = newEncodedPassword;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.loopers.domain.user;

import java.util.Optional;

public interface UserRepository {
void save(User user);
Optional<User> findByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.loopers.domain.user;

import com.loopers.application.user.UpdatePasswordCommand;
import com.loopers.application.user.UserInfo;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public void updatePassword(UpdatePasswordCommand command) {
User user = getUser(command.loginId());

if (!passwordEncoder.matches(command.currentPassword(), user.getPassword())) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
}

if (passwordEncoder.matches(command.newPassword(), user.getPassword())) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호와 동일한 비밀번호로 변경할 수 없습니다.");
}

PasswordPolicyValidator.validate(command.newPassword(), user.getBirthDate());
String encoded = passwordEncoder.encode(command.newPassword());
user.updatePassword(encoded);
}

@Transactional(readOnly = true)
public UserInfo getMyInfo(String loginId) {
User user = getUser(loginId);
return UserInfo.from(user);
}

private User getUser(String loginId) {
return userRepository.findByLoginId(loginId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
"[loginId = " + loginId + "] 를 찾을 수 없습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.infrastructure.user;

import com.loopers.domain.user.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class BcryptPasswordEncoder implements PasswordEncoder {
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

@Override
public String encode(String rawPassword) {
return encoder.encode(rawPassword);
}

@Override
public boolean matches(String rawPassword, String encodedPassword) {
return encoder.matches(rawPassword, encodedPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.loopers.infrastructure.user;

import com.loopers.domain.user.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserJpaRepository extends JpaRepository<User, Long> {
Optional<User> findByLoginId(String loginId);
}
Loading