Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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.

3 changes: 3 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ dependencies {
// web
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

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

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

Length of output: 1271


🏁 Script executed:

# Find root build.gradle.kts or gradle configuration
fd -t f "build.gradle.kts" | head -5

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

Length of output: 242


🏁 Script executed:

cat -n build.gradle.kts

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

Length of output: 4538


🏁 Script executed:

cat -n gradle.properties

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

Length of output: 747


🌐 Web query:

Spring Boot 3.4.4 spring-boot-starter-security version CVE

💡 Result:

Spring Boot 3.4.4’s spring-boot-starter-security pulls in Spring Security 6.4.4 (via the Spring Boot BOM). [1]

CVE context for Spring Security 6.4.4

So, if you’re on Boot 3.4.4, you’re on Spring Security 6.4.4 (includes fixes for CVE-2025-22223/22228, but is impacted by CVE-2025-22234). [1][2][3]

What to do

  • Upgrade Spring Boot to a release that manages Spring Security ≥ 6.4.5, or override the managed version to 6.4.5+. [3]

References:
[1] (github.com)
[2] (spring.io)
[3] (spring.io)

Citations:


🏁 Script executed:

# Search for dependency scanning or security tasks in gradle files
rg -i "(dependenc.*scan|security.*scan|owasp|snyk|checkmarx)" --type gradle

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

Length of output: 233


🏁 Script executed:

# Search for CI configuration and security scanning
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".gradle" \) | grep -E "(github|gitlab|workflow|ci)" | head -10

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

Length of output: 110


🏁 Script executed:

# Search for GitHub workflows
find .github/workflows -type f 2>/dev/null | head -10

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

Length of output: 67


🏁 Script executed:

# Check if there are any security scanning plugins in any gradle file
rg "dependency.*check|owasp|snyk" --type-list

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

Length of output: 474


🏁 Script executed:

# Check build gradle files for any security-related configuration
find . -name "build.gradle.kts" -type f | xargs grep -l "security\|cve\|vulnerability" 2>/dev/null

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

Length of output: 121


Spring Security 6.4.4의 CVE-2025-22234 취약점 해결이 필수다.

Spring Boot 3.4.4 BOM에 의해 Spring Security 6.4.4가 관리되고 있으나, 해당 버전은 CVE-2025-22234에 영향을 받는다. 이 취약점은 인증/인가 처리 과정에서 발생하는 보안 결함이며, 상용 환경에서 미해결 시 인증 우회 등의 위험에 노출된다.

수정안으로 dependencyManagement 블록에서 Spring Security를 6.4.5 이상으로 명시적으로 오버라이드한다.

dependencyManagement {
    dependencies {
        dependency("org.springframework.security:spring-security-core:6.4.5")
    }
}

이후 의존성 취약점 스캔 태스크를 CI에 포함하고, 회원가입/로그인/비밀번호 변경 API의 인증/인가 관련 스모크 테스트를 추가한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/build.gradle.kts` around lines 12 - 14, Spring Security
6.4.4 (managed by the Spring Boot BOM) is vulnerable to CVE-2025-22234; override
the managed version by adding a dependencyManagement entry that forces
org.springframework.security:spring-security-core to 6.4.5 or later (use
dependencyManagement and specify the
org.springframework.security:spring-security-core:6.4.5 coordinate), then update
CI to run a dependency-vulnerability scan task and add smoke tests for signup,
login, and password-change authorization flows to validate the fix.

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

// querydsl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.application.member;

import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

@RequiredArgsConstructor
@Component
public class MemberFacade {

private final MemberService memberService;

public MemberInfo register(String loginId, String rawPassword, String name,
LocalDate birthDate, String email) {
Member member = memberService.register(loginId, rawPassword, name, birthDate, email);
return MemberInfo.from(member);
}

public MemberInfo getMe(Member authenticatedMember) {
return MemberInfo.fromWithMaskedName(authenticatedMember);
}

public void changePassword(Member authenticatedMember, String currentPassword, String newPassword) {
memberService.changePassword(authenticatedMember, currentPassword, newPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.loopers.application.member;

import com.loopers.domain.member.Member;

import java.time.LocalDate;

public record MemberInfo(String loginId, String name, LocalDate birthDate, String email) {
public static MemberInfo from(Member member) {
return new MemberInfo(
member.getLoginId(),
member.getName(),
member.getBirthDate(),
member.getEmail()
);
}

public static MemberInfo fromWithMaskedName(Member member) {
return new MemberInfo(
member.getLoginId(),
maskName(member.getName()),
member.getBirthDate(),
member.getEmail()
);
}

private static String maskName(String name) {
if (name == null || name.isEmpty()) {
return "";
}
if (name.length() == 1) {
return "*";
}
return name.substring(0, name.length() - 1) + "*";
}
}
142 changes: 142 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.loopers.domain.member;

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 java.time.LocalDate;

@Entity
@Table(name = "member")
public class Member extends BaseEntity {

@Column(unique = true, nullable = false)
private String loginId;
private String password;
private String name;
private LocalDate birthDate;
private String email;

protected Member() {}

private Member(String loginId, String password, String name,
LocalDate birthDate, String email) {
this.loginId = loginId;
this.password = password;
this.name = name;
this.birthDate = birthDate;
this.email = email;
}

public static Member create(String loginId, String rawPassword,
String name, LocalDate birthDate,
String email, PasswordEncoder encoder) {
validateNotBlank(loginId, "로그인ID는 필수입니다.");
validateNotBlank(rawPassword, "비밀번호는 필수입니다.");
validateNotBlank(name, "이름은 필수입니다.");
validateNotNull(birthDate, "생년월일은 필수입니다.");
validateNotBlank(email, "이메일은 필수입니다.");

validateLoginId(loginId);
validatePassword(rawPassword, birthDate);
String normalizedName = normalizeName(name);
validateName(normalizedName);
validateEmail(email);

String encodedPassword = encoder.encode(rawPassword);
return new Member(loginId, encodedPassword, normalizedName, birthDate, email);
}
Comment on lines 34 to 51
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 가드가 필요하다.
encoder 또는 currentPassword/newRawPassword가 null이면 NPE로 500이 발생해 운영에서 표준 오류 포맷이 깨지고 원인 분석이 어려워진다.
수정안으로 create와 changePassword 시작부에서 null을 검사해 CoreException(BAD_REQUEST)로 통일한다.

수정 예시
     public static Member create(String loginId, String rawPassword,
                                 String name, LocalDate birthDate,
                                 String email, PasswordEncoder encoder) {
+        if (encoder == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 인코더가 필요합니다.");
+        }
         validateLoginId(loginId);
         validatePassword(rawPassword, birthDate);
         String normalizedName = normalizeName(name);
         validateName(normalizedName);
         validateEmail(email);

         String encodedPassword = encoder.encode(rawPassword);
         return new Member(loginId, encodedPassword, normalizedName, birthDate, email);
     }

     public void changePassword(String currentPassword, String newRawPassword,
                                PasswordEncoder encoder) {
+        if (currentPassword == null || newRawPassword == null || encoder == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 입력값이 누락되었습니다.");
+        }
         // 현재 비밀번호 확인
         if (!encoder.matches(currentPassword, this.password)) {
             throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
         }

추가 테스트로 currentPassword/newRawPassword/encoder가 null일 때 BAD_REQUEST가 발생하는지 단위 테스트를 추가한다.
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/).'를 근거로 했다.
As per coding guidelines 'null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다'를 근거로 했다.

Also applies to: 83-97

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java` around
lines 32 - 43, Validate null inputs at the start of Member.create and
Member.changePassword: check that encoder is not null in create, and that
encoder, currentPassword, and newRawPassword are not null in changePassword; if
any are null, throw a CoreException with BAD_REQUEST to ensure unified error
handling via ApiControllerAdvice. Update Member.create (method name: create) and
Member.changePassword (method name: changePassword) to perform these null-guards
before any processing (e.g., before calling encoder.encode or password checks).
Add unit tests asserting that calling Member.create with a null encoder and
Member.changePassword with null encoder/currentPassword/newRawPassword each
result in a CoreException with BAD_REQUEST. Ensure messages in exceptions
provide minimal context for debugging while keeping the error type consistent.


private static void validateNotNull(Object value, String message) {
if (value == null) {
throw new CoreException(ErrorType.BAD_REQUEST, message);
}
}

private static void validateNotBlank(String value, String message) {
if (value == null || value.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, message);
}
}

private static void validatePassword(String password, LocalDate birthDate) {
if (password.length() < 8 || password.length() > 16) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자여야 합니다.");
}
if (!password.matches("^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]+$")) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 영문 대소문자, 숫자, 특수문자만 허용됩니다.");
}
String birthDateStr = birthDate.toString().replace("-", ""); // 19900115
if (password.contains(birthDateStr)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다.");
}
}

private static void validateLoginId(String loginId) {
if (!loginId.matches("^[a-zA-Z0-9]+$")) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 영문과 숫자만 허용됩니다.");
}
}

private static String normalizeName(String name) {
return name.trim().replaceAll("\\s+", " ");
}

private static void validateName(String name) {
boolean isKorean = name.matches("^[가-힣]+$");
boolean isEnglish = name.matches("^[a-zA-Z]+( [a-zA-Z]+)*$");

if (!isKorean && !isEnglish) {
throw new CoreException(ErrorType.BAD_REQUEST, "이름은 한글만 또는 영문만 허용됩니다.");
}
}

private static void validateEmail(String email) {
if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
throw new CoreException(ErrorType.BAD_REQUEST, "올바른 이메일 형식이 아닙니다.");
}
}
Comment on lines 34 to 101
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 위험
운영 관점에서 null 입력이 들어오면 NPE로 500이 발생해 표준 오류 포맷과 모니터링이 깨질 수 있다.
수정안: create/검증 단계에서 loginId/password/name/birthDate/email null을 먼저 검사해 CoreException(BAD_REQUEST)으로 처리하고, validatePassword에서 birthDate null도 차단한다.
추가 테스트: 필수 값이 null 또는 공백인 요청에 대해 400과 메시지가 반환되는지 테스트한다.

🔧 수정 예시
     public static Member create(String loginId, String rawPassword,
                                 String name, LocalDate birthDate,
                                 String email, PasswordEncoder encoder) {
+        if (loginId == null || rawPassword == null || name == null || birthDate == null || email == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "필수 입력값이 누락되었습니다.");
+        }
         validateLoginId(loginId);
         validatePassword(rawPassword, birthDate);
         String normalizedName = normalizeName(name);
         validateName(normalizedName);
         validateEmail(email);
@@
     private static void validatePassword(String password, LocalDate birthDate) {
+        if (password == null || birthDate == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 또는 생년월일이 누락되었습니다.");
+        }
         if (password.length() < 8 || password.length() > 16) {
             throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자여야 합니다.");
         }

As per coding guidelines: null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java` around
lines 32 - 81, The constructor factory Member.create and its validators
(validatePassword, validateLoginId, normalizeName, validateName, validateEmail)
lack null/blank checks causing NPEs; add guard clauses at the start of create to
validate non-null and non-blank loginId, rawPassword, name, email and non-null
birthDate and throw CoreException(ErrorType.BAD_REQUEST, "...") with appropriate
messages, and update validatePassword to explicitly check birthDate != null
before using it; ensure name is trimmed/blank-checked before normalizeName and
keep Member immutable (defensive copy if needed) and add tests asserting
400/CoreException for null or blank required fields.


public void changePassword(String currentPassword, String newRawPassword,
PasswordEncoder encoder) {
validateNotBlank(currentPassword, "현재 비밀번호는 필수입니다.");
validateNotBlank(newRawPassword, "새 비밀번호는 필수입니다.");

// 현재 비밀번호 확인
if (!encoder.matches(currentPassword, this.password)) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
}
// 새 비밀번호가 현재와 동일한지 확인
if (encoder.matches(newRawPassword, this.password)) {
throw new CoreException(ErrorType.BAD_REQUEST, "새 비밀번호는 현재 비밀번호와 달라야 합니다.");
}
// 새 비밀번호 규칙 검증
validatePassword(newRawPassword, this.birthDate);
// 비밀번호 변경
this.password = encoder.encode(newRawPassword);
}

// Getter
public String getLoginId() {
return loginId;
}

public String getPassword() {
return password;
}

public String getName() {
return name;
}

public LocalDate getBirthDate() {
return birthDate;
}

public String getEmail() {
return email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.loopers.domain.member;

import java.util.Optional;

public interface MemberReader {
boolean existsByLoginId(String loginId);
Optional<Member> findByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.loopers.domain.member;

public interface MemberRepository {
Member save(Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.loopers.domain.member;

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

import java.time.LocalDate;

@RequiredArgsConstructor
@Component
public class MemberService {

private final MemberReader memberReader;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

public Member register(String loginId, String rawPassword, String name,
LocalDate birthDate, String email) {
if (memberReader.existsByLoginId(loginId)) {
throw new CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 로그인ID입니다.");
}

Member member = Member.create(loginId, rawPassword, name, birthDate, email, passwordEncoder);
return memberRepository.save(member);
Comment on lines +18 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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MemberService.java" | head -20

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

Length of output: 163


🏁 Script executed:

find . -type f -name "Member.java" | head -20

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

Length of output: 156


🏁 Script executed:

fd -t f "MemberService" | head -20

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

Length of output: 242


🏁 Script executed:

wc -l ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
wc -l ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 242


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java

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

Length of output: 1427


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 4823


🏁 Script executed:

find . -type f -name "MemberRepository.java" -o -name "MemberReader.java"

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

Length of output: 244


🏁 Script executed:

grep -r "DataIntegrityViolationException" ./apps/commerce-api/src --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/test/java/com/loopers/domain/member/MemberServiceTest.java

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

Length of output: 4987


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java

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

Length of output: 225


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/member/MemberReader.java

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

Length of output: 335


🏁 Script executed:

grep -r "unique\|@Column\|@Index" ./apps/commerce-api/src/main/java/com/loopers/domain/member --include="*.java"

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

Length of output: 67


🏁 Script executed:

find ./apps/commerce-api -type f -name "*.java" | xargs grep -l "implements MemberRepository\|implements MemberReader" | head -20

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

Length of output: 351


🏁 Script executed:

find ./apps/commerce-api -type f -name "*MemberRepository*.java" -o -name "*MemberReader*.java"

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

Length of output: 428


🏁 Script executed:

grep -r "class.*MemberRepository\|class.*MemberReader" ./apps/commerce-api/src --include="*.java"

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

Length of output: 682


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java

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

Length of output: 699


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberReaderImpl.java

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

Length of output: 929


🏁 Script executed:

grep -n "Column\|Index\|unique\|constraint" ./apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 177


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java

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

Length of output: 511


🏁 Script executed:

find ./apps/commerce-api -type f \( -name "*.sql" -o -name "*Migration*.java" -o -name "schema*" \) | head -20

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

Length of output: 67


🏁 Script executed:

find ./apps -type f -name "*.xml" -o -name "*.sql" | xargs grep -l "member" 2>/dev/null | head -10

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

Length of output: 67


🏁 Script executed:

grep -r "DataIntegrityViolationException\|ConstraintViolationException" ./apps/commerce-api/src/main/java --include="*.java" -A 3 | head -30

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

Length of output: 67


🏁 Script executed:

grep -r "@Transactional\|@Column" ./apps/commerce-api/src/main/java/com/loopers/domain/member --include="*.java"

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

Length of output: 67


🏁 Script executed:

find ./apps/commerce-api -type f -name "application*.yml" -o -name "application*.yaml" | xargs cat 2>/dev/null | grep -A 20 "jpa\|hibernate" | head -30

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

Length of output: 356


동시 가입 요청에서 로그인ID 중복 경쟁 상태 처리 필수

현재 코드는 existsByLoginId 검사 후 저장하는 사이에 다른 요청이 동일 loginId를 저장할 수 있어, 데이터베이스 중복 제약 위반 시 DataIntegrityViolationException이 발생하면 500 에러로 노출된다. 운영 안정성을 위해 다음을 필수 처리해야 한다:

  1. Member 엔티티의 loginId 필드에 @Column(unique=true) 추가로 데이터베이스 수준 중복 방지
  2. MemberService.register 메서드에서 DataIntegrityViolationException을 포착하여 CoreException(ErrorType.BAD_REQUEST)로 변환해 일관된 400 응답 반환
  3. 동시 등록 시나리오 테스트 추가 (동일 loginId로 다중 요청 시 하나만 성공, 나머지는 400 반환 확인)
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`
around lines 18 - 25, Add a database-unique constraint on the Member entity's
loginId by annotating the loginId field (Member.loginId) with `@Column`(unique =
true); update MemberService.register to wrap the memberRepository.save(member)
call in a try/catch that catches DataIntegrityViolationException and throws new
CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 로그인ID입니다."); and add an
integration/concurrency test that fires concurrent register requests using the
same loginId and asserts that exactly one request succeeds while the others
receive CoreException/ErrorType.BAD_REQUEST responses.

}

public void changePassword(Member member, String currentPassword, String newPassword) {
member.changePassword(currentPassword, newPassword, passwordEncoder);
memberRepository.save(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.domain.member;

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,11 @@
package com.loopers.infrastructure.member;

import com.loopers.domain.member.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberJpaRepository extends JpaRepository<Member, Long> {
boolean existsByLoginId(String loginId);
Optional<Member> findByLoginId(String loginId);
Comment on lines +8 to +10
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:

fd -type f -name "MemberJpaRepository.java" | head -20

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

Length of output: 255


🏁 Script executed:

fd -type f -name "Member.java" | grep -E "(entity|domain|model)" | head -20

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

Length of output: 255


🏁 Script executed:

# Member 엔티티 파일 찾기
fd -type f -name "*Member*.java" | head -30

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

Length of output: 255


🏁 Script executed:

git ls-files | grep -E "MemberJpaRepository|Member\.java" | head -20

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

Length of output: 245


🏁 Script executed:

# 저장소 구조 확인
find . -type f -name "Member*.java" 2>/dev/null | head -20

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

Length of output: 1338


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 4823


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java

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

Length of output: 511


🏁 Script executed:

find . -type f -name "BaseEntity.java" | head -5 | xargs cat -n

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

Length of output: 2291


🏁 Script executed:

find . -type f \( -name "V*.sql" -o -name "*migration*" \) | grep -i member | head -10

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

Length of output: 67


🏁 Script executed:

find . -type d \( -name "db" -o -name "migration" -o -name "flyway" \) 2>/dev/null

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

Length of output: 67


🏁 Script executed:

find . -type f -name "*.sql" 2>/dev/null | head -20

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

Length of output: 67


🏁 Script executed:

find . -path "*/resources/db*" -o -path "*/resources/migration*" 2>/dev/null | head -20

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

Length of output: 67


🏁 Script executed:

# Member entity에서 `@Index`, `@Column`(unique) 검색
rg -n "@Index|@Column.*unique" apps/commerce-api/src/main/java/com/loopers/domain/member/Member.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/infrastructure/member/MemberRepositoryImpl.java | head -50

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

Length of output: 699


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberReaderImpl.java | head -50

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

Length of output: 929


🏁 Script executed:

find . -type f -name "schema*.sql" -o -name "application*.yml" -o -name "application*.yaml" 2>/dev/null | head -10

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

Length of output: 256


🏁 Script executed:

cat apps/commerce-api/src/main/resources/application.yml

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

Length of output: 1044


🏁 Script executed:

cat apps/commerce-api/src/main/resources/jpa.yml

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

Length of output: 161


🏁 Script executed:

find . -name "jpa.yml" 2>/dev/null

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

Length of output: 125


🏁 Script executed:

ls -la apps/commerce-api/src/main/resources/

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

Length of output: 260


🏁 Script executed:

cat ./modules/jpa/src/main/resources/jpa.yml

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

Length of output: 2231


🏁 Script executed:

find . -type f \( -name "*.sql" -o -name "*.ddl" \) 2>/dev/null | head -20

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

Length of output: 67


🏁 Script executed:

# Member entity 전체 어노테이션 다시 확인
rg -A 20 "^@Entity" apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 611


loginId 컬럼에 인덱스와 유니크 제약조건을 추가해야 한다.

existsByLoginId와 findByLoginId는 로그인 및 회원가입 시 loginId 기반 조회를 수행하며, 현재 loginId 필드에 @Column(unique=true) 또는 @Index 어노테이션이 없다. 회원 수 증가에 따라 이들 메서드는 Full Table Scan을 유발하여 응답 지연과 데이터베이스 부하를 야기한다.

Member 엔티티의 loginId 필드에 @Column(unique = true) 어노테이션을 추가하거나, @Table(indexes = {@Index(columnList = "login_id")}) 를 통해 인덱스를 정의해야 한다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java`
around lines 8 - 10, The Member repository queries (existsByLoginId and
findByLoginId in MemberJpaRepository) will cause full table scans unless the
Member.loginId column has a uniqueness constraint and/or an index; update the
Member entity by adding either `@Column`(unique = true) on the loginId field or
add a class-level `@Table`(indexes = {`@Index`(columnList = "login_id")}) so the
database enforces uniqueness and creates an index to speed up existsByLoginId
and findByLoginId lookups.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.loopers.infrastructure.member;

import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberReader;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

@RequiredArgsConstructor
@Component
public class MemberReaderImpl implements MemberReader {
private final MemberJpaRepository memberJpaRepository;

@Override
public boolean existsByLoginId(String loginId) {
return memberJpaRepository.existsByLoginId(loginId);
}

@Override
public Optional<Member> findByLoginId(String loginId) {
return memberJpaRepository.findByLoginId(loginId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.loopers.infrastructure.member;

import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class MemberRepositoryImpl implements MemberRepository {
private final MemberJpaRepository memberJpaRepository;

@Override
public Member save(Member member) {
return memberJpaRepository.save(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.infrastructure.member;

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

@Component
public class PasswordEncoderImpl implements PasswordEncoder {
private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

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

@Override
public boolean matches(String rawPassword, String encodedPassword) {
return bCryptPasswordEncoder.matches(rawPassword, encodedPassword);
}
}
Loading