-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-1] 회원가입, 내 정보 조회, 비밀번호 변경 기능 구현 #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0dcfd8
83c0fa6
62db8d0
c45bbb5
79683a6
a5392aa
f1897e8
f030d42
adad0bd
f4dbb64
081a4e4
a3e884a
cf65c84
3c36e5e
5eec4e9
ad2ee7f
dc7df5c
b63e546
74dd37b
0f7fa42
6834d10
91c9966
60aca5f
b4031b7
7e7e913
503a581
cf55e49
7fe825e
8182abb
f3b968c
56c0a6b
5febf4a
37e4574
b44396c
b468aa9
b566e9f
8b1dca9
24511a7
9f4c1dd
7c681b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "SignUpCommand.java" -type fRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 818 Record의 Java record는 자동으로 생성되는 수정안: Record 내부에 추가 테스트: 수정 예시 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 |
||
| ) { | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "UpdatePasswordCommand.java" -type fRepository: 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.javaRepository: 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 -100Repository: 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의 기본 현재 운영 관점에서는 Spring 프레임워크의 예외 처리, 디버그 로깅, 또는 모니터링 시스템이 자동으로 수정안: 수정 예시 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());
}
}추가로 🤖 Prompt for AI Agents |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛡️ 수정 제안 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 |
||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "SignUpService.java" -type fRepository: 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 userRepository: 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.javaRepository: 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.javaRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 314 🏁 Script executed: find . -name "SignUpValidator.java" -type fRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 447 🏁 Script executed: find . -name "*.sql" -o -name "*Migration*.java" | head -20Repository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1143 🏁 Script executed: find . -path "*/resources/db/migration*" -o -path "*/schema*" | head -10Repository: 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.javaRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 741 🏁 Script executed: grep -r "@UniqueConstraint\|@Column.*unique" --include="*.java" | head -20Repository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 741 🏁 Script executed: find . -name "*ExceptionHandler*" -o -name "*ControllerAdvice*" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 170 🏁 Script executed: rg "DataIntegrityViolationException|ConstraintViolationException" --type javaRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 7333 🏁 Script executed: find . -path "*/resources/db/migration*" -type f | head -20Repository: 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 -20Repository: 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 -20Repository: 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 -10Repository: 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 -10Repository: 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.javaRepository: 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.javaRepository: 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 -150Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 5919 동시 요청 시 중복 loginId 저장을 방지하기 위해 데이터베이스 제약과 예외 처리가 필수다 현재 코드는 SignUpValidator에서 중복을 검증하지만, 검증과 저장 사이의 Race Condition으로 인해 동시 요청 시 동일 loginId가 중복 저장될 수 있다. 또한 데이터베이스 제약이 없으면 검증을 우회하는 다른 저장 경로에서도 중복이 발생할 수 있으며, 제약 위반 시 DataIntegrityViolationException이 발생하여 500 에러로 노출된다. 수정안:
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필수 값 null 입력 시 NPE로 500이 발생할 수 있다 수정 예시 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: 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| 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; | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # User.java 파일의 전체 내용 확인
find . -path "*domain/user/User.java" -type f | head -5Repository: 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 -50Repository: 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 userRepository: 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.javaRepository: 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.javaRepository: 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.javaRepository: 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.javaRepository: 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 -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 151 🏁 Script executed: # UserRepository 구현체 찾기
find . -name "*UserRepository*.java" -type f | xargs cat -nRepository: 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 -10Repository: 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 -5Repository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 3226 🏁 Script executed: # UserJpaRepository 찾기
find . -name "UserJpaRepository.java" -type f | xargs cat -nRepository: 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 -nRepository: 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/nullRepository: 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.ymlRepository: 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 -nRepository: 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 -nRepository: 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 -10Repository: 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 -15Repository: 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 -nRepository: 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 -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 170 🏁 Script executed: # SignUpService 확인
find . -name "SignUpService.java" -type f | xargs cat -nRepository: 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.javaRepository: 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.javaRepository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2074 loginId에 DB 유니크 제약을 추가하고 예외 처리를 정의해야 한다 현재 SignUpValidator의 애플리케이션 레벨 중복 체크만으로는 동시 가입 요청 시 레이스 컨디션을 방지할 수 없다. T1이 유효성을 통과하고 저장하기 전에 T2도 유효성 검사를 통과하면 중복 계정이 생성된다. 운영 관점에서 이는 인증 오류, 계정 혼동 등 심각한 문제를 야기한다. 다음을 수정해야 한다:
🤖 Prompt for AI Agents |
||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/User.java | head -80Repository: 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.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1906 🏁 Script executed: rg -l "SignUpCommand" --type java | head -5Repository: 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.javaRepository: 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.javaRepository: 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1238
🏁 Script executed:
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-cryptoversions (2025–2026)Spring Security CVEs relevant to
spring-security-crypto(2025)CVE-2025-22228 (HIGH) –
BCryptPasswordEncoder.matchesmay incorrectly returntruefor passwords >72 chars when the first 72 chars match.CVE-2025-22234 (MEDIUM) – Fix for CVE-2025-22228 inadvertently broke
DaoAuthenticationProvidertiming-attack mitigation (can leak info via response-time differences).Related (not crypto-specific, but in Spring Security)
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-cryptoversions (shows 7.0.2 and 7.1.0-M1 dates)[2] Spring blog: Spring Security
7.0.1/7.0.2releases[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:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1238
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 396
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1335
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 747
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 119
🏁 Script executed:
cat -n build.gradle.kts | head -80Repository: 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.ktsRepository: 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.4BOM) manages Spring Security6.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-bomversion6.4.4[1]. Sincespring-security-cryptois part of that BOM, the managedorg.springframework.security:spring-security-cryptoversion is6.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 의존만으로는 이 취약점이 해결되지 않으므로, 다음 중 하나를 수행해야 한다:
추가적으로 CI에서 의존성 리졸브 결과를 검증하고 보안 스캔을 통해 CVE 해결 여부를 확인해야 한다.
🤖 Prompt for AI Agents