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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
45 changes: 0 additions & 45 deletions .codeguide/loopers-1-week.md

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### Kotlin ###
.kotlin

/.claude
CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.loopers.application.service;

import com.loopers.application.service.dto.MemberRegisterRequest;
import com.loopers.application.service.dto.MyMemberInfoResponse;
import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberExceptionMessage;
import com.loopers.infrastructure.member.MemberRepository;
import com.loopers.support.error.CoreException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;

@Transactional
public void register(MemberRegisterRequest request) {
boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId());

if (isLoginIdAlreadyExists) {
throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message());
}

memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
}
Comment on lines +19 to +28
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: 169


🏁 Script executed:

find . -type f -name "*.sql" -o -name "*.ddl" | head -20

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

Length of output: 67


🏁 Script executed:

find . -type f -path "**/*/Service.java" -o -path "**/*Service.java" | head -10

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

Length of output: 250


🏁 Script executed:

fd -t f "MemberService" --extension java

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

Length of output: 336


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java

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

Length of output: 3628


🏁 Script executed:

find . -type f -name "Member.java" -o -name "*Member*.java" | grep -E "(entity|domain)" | head -10

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

Length of output: 404


🏁 Script executed:

find . -path "**/resources/*" -name "*.sql" -o -name "*.ddl" -o -name "schema*" | head -20

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

Length of output: 67


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 2535


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/MemberExceptionMessage.java

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

Length of output: 4192


🏁 Script executed:

find . -path "**/resources/*" -type f \( -name "*.sql" -o -name "*.xml" \) | xargs grep -l "login_id\|loginId" 2>/dev/null

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

Length of output: 69


🏁 Script executed:

cat -n ./apps/commerce-api/src/test/java/com/loopers/application/MemberServiceIntegrationTest.java

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

Length of output: 8912


🏁 Script executed:

find . -path "**/resources/*" -type f | grep -E "\.(sql|xml|properties|yaml|yml)$" | head -20

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

Length of output: 967


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/support/error/CoreException.java 2>/dev/null || echo "File not found"

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

Length of output: 99


🏁 Script executed:

grep -r "unique.*login" --include="*.java" --include="*.sql" . 2>/dev/null | head -20

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 67


🏁 Script executed:

find . -type f -name "CoreException.java" -o -name "*Exception.java" | grep -E "support|error|exception" | head -20

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

Length of output: 163


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | grep -A 5 -B 5 "@Column"

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

Length of output: 67


🏁 Script executed:

rg "DataIntegrityViolationException" --type java

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

Length of output: 67


🏁 Script executed:

rg "@Column.*unique" --type java

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

Length of output: 67


🏁 Script executed:

find . -path "*/schema*" -o -path "*/migration*" -o -path "*/flyway*" | 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 . -type d -name "db" -o -name "migration" -o -name "flyway" -o -name "liquibase" 2>/dev/null

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 67


🏁 Script executed:

rg "class.*Repository" --type java | grep Member | head -10

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

Length of output: 67


🏁 Script executed:

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

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

Length of output: 2903


🏁 Script executed:

find . -type f -name "application*.yml" -o -name "application*.yaml" | xargs grep -l "jpa\|hibernate" | head -5

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

Length of output: 256


🏁 Script executed:

cat -n ./apps/commerce-api/src/main/resources/application.yml | head -50

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

Length of output: 1297


🏁 Script executed:

rg "hibernate\.ddl-auto|spring\.jpa\.hibernate" --type yaml

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

Length of output: 67


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/infrastructure/member/MemberRepository.java 2>/dev/null || find . -name "MemberRepository.java"

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

Length of output: 174


🏁 Script executed:

rg "catch.*DataIntegrityViolationException" --type java -A 3

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

Length of output: 67


🏁 Script executed:

rg "class.*Service" --type java | head -10

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

Length of output: 683


🏁 Script executed:

find . -type f -name "*Repository.java" -path "*/infrastructure/*" | head -5 | xargs cat | head -50

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

Length of output: 672


🏁 Script executed:

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

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

Length of output: 524


🏁 Script executed:

find . -path "*/db/migration*" -o -path "*/flyway*" -o -path "*/changelog*" 2>/dev/null

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

Length of output: 67


🏁 Script executed:

rg "^CREATE TABLE.*member" -A 20 --type sql

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

Length of output: 233


🏁 Script executed:

rg "@Transactional.*Propagation" --type java | head -10

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/application/MemberServiceTest.java

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

Length of output: 5710


🏁 Script executed:

rg "@Column.*unique\|@UniqueConstraint" --type java

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

Length of output: 67


🏁 Script executed:

rg "Propagation\|Isolation" --type java | head -20

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

Length of output: 67


🏁 Script executed:

grep -n "loginId" ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 234


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | grep -A 2 -B 2 "loginId"

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

Length of output: 773


🏁 Script executed:

rg "IllegalArgumentException" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type java

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

Length of output: 1107


🏁 Script executed:

rg "UNIQUE.*login_id\|login_id.*UNIQUE" --type sql --type java

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

Length of output: 67


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java | head -35

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

Length of output: 1002


🏁 Script executed:

find . -type f -name "*.java" -path "*/test/*" | xargs grep -l "concurrent\|parallel\|ExecutorService\|CountDownLatch" | head -5

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

Length of output: 67


🏁 Script executed:

rg "class.*Service.*{" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type java -A 5 | head -30

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

Length of output: 182


🏁 Script executed:

rg "throw new.*Exception" ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java

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

Length of output: 430


🏁 Script executed:

find . -path "*/infrastructure/*" -name "*Repository*.java" | xargs grep -l "save\|persist" | head -3

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

Length of output: 67


🏁 Script executed:

rg "@Column.*loginId" ./modules/jpa/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:

rg "class.*Repository" ./apps/commerce-api/src/main/java/com/loopers/infrastructure/ --type java -A 10 | head -50

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

Length of output: 1102


🏁 Script executed:

rg "catch.*DataIntegrityViolationException\|catch.*ConstraintViolation" --type java

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

Length of output: 67


🏁 Script executed:

find . -name "*.java" -path "*/service/*" | xargs grep -l "@Transactional" | head -5 | xargs grep -A 20 "DataIntegrityViolation\|ConstraintViolation"

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

Length of output: 69


🏁 Script executed:

rg "synchronized\|Lock\|ReentrantLock" --type java

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

Length of output: 67


🏁 Script executed:

cat -n ./modules/jpa/src/main/java/com/loopers/domain/member/Member.java

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

Length of output: 2535


🏁 Script executed:

rg "CoreException" ./apps/commerce-api/src/main/java/com/loopers/application/service/ --type java -B 2 -A 2

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

Length of output: 759


🏁 Script executed:

find . -name "ErrorType.java" -o -name "*ErrorCode*.java" | head -5

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

Length of output: 159


🏁 Script executed:

grep -r "DataIntegrityViolation" . --include="*.java" 2>/dev/null

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

Length of output: 67


🏁 Script executed:

rg "exists.*then.*save\|check.*then.*insert" --type java | head -10

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/ErrorType.java

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

Length of output: 949


🏁 Script executed:

rg "CoreException" ./apps/commerce-api/src/main/java/com/loopers/ --type java | grep "throw\|new" | head -20

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

Length of output: 750


🏁 Script executed:

grep -n "import.*CoreException\|import.*ErrorType" ./apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java

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

Length of output: 134


🏁 Script executed:

rg "findByLoginId\|existsByLoginId" ./apps/commerce-api/src/main/java/com/loopers/ --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/application/service/MemberService.java | head -30

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

Length of output: 1457


동시 가입 요청 시 중복 로그인 ID 검증이 원자적이지 않아 경쟁 조건이 발생한다

existsByLoginId() 조회 후 save() 사이에 다른 요청이 동일 ID로 가입하면 중복 레코드가 생성되거나 DataIntegrityViolationException으로 500 오류가 발생해 운영에서 간헐 장애로 보일 수 있다.

현재 Member 엔티티에는 login_id 컬럼에 유니크 제약이 없으므로 다음을 함께 수행해야 한다:

  1. DB 스키마에 login_id에 UNIQUE 제약을 추가한다
  2. save() 호출 시 DataIntegrityViolationException을 캐치해 중복 오류로 변환한다
  3. existsByLoginId는 UX용 조기 피드백으로만 유지한다

동시 가입 요청을 병렬로 실행하고 하나만 성공, 나머지는 중복 오류로 떨어지는지 검증하는 통합 테스트를 추가한다.

🔧 수정 제안
+import org.springframework.dao.DataIntegrityViolationException;
+
 `@Transactional`
 public void register(MemberRegisterRequest request) {
     boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId());
@@ -26,7 +27,11 @@ public class MemberService {
         if (isLoginIdAlreadyExists) {
             throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message());
         }
-
-        memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
+        try {
+            memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
+        } catch (DataIntegrityViolationException e) {
+            throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message(), e);
+        }
 }
📝 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
@Transactional
public void register(MemberRegisterRequest request) {
boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId());
if (isLoginIdAlreadyExists) {
throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message());
}
memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
}
`@Transactional`
public void register(MemberRegisterRequest request) {
boolean isLoginIdAlreadyExists = memberRepository.existsByLoginId(request.loginId());
if (isLoginIdAlreadyExists) {
throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message());
}
try {
memberRepository.save(Member.register(request.loginId(), request.password(), request.name(), request.birthdate(), request.email()));
} catch (DataIntegrityViolationException e) {
throw new IllegalArgumentException(MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS.message(), e);
}
}
🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 19 - 28, 현재 MemberService.register에서
memberRepository.existsByLoginId(...) 조회와 memberRepository.save(...) 사이에 경쟁 조건이
있어 동시 가입 시 중복이 발생합니다; DB 스키마에 login_id에 UNIQUE 제약을 추가하고 MemberService.register에서
save 호출을 감싸 DataIntegrityViolationException을 잡아
MemberExceptionMessage.LoginId.DUPLICATE_ID_EXISTS로 변환해 던지도록 처리하며
existsByLoginId는 UX용 조기 피드백으로만 유지하세요; 또한 Member 엔티티의 매핑과 마이그레이션을 통해 login_id에
유니크 제약을 추가하고, 통합 테스트를 작성해 여러 스레드/프로세스로 병렬 등록을 시도할 때 하나만 성공하고 나머지는 중복 예외로 실패하는지
검증하십시오.


@Transactional(readOnly = true)
public MyMemberInfoResponse getMyInfo(String userId, String password) {
// 1. 회원 조회 (없으면 예외 발생 - MemberExceptionMessage.Common.NOT_FOUND 사용)
Member member = memberRepository.findByLoginId(userId)
.orElseThrow(() -> new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message()));

// 2. 비밀번호 일치 여부 확인 (도메인 모델의 isSamePassword 활용)
if (!member.isSamePassword(password)) {
// 비밀번호 불일치 시 예외 발생 (인증 관련 메시지 사용)
throw new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message());
}
Comment on lines +31 to +40
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

IllegalArgumentException 사용으로 오류 포맷과 원인 보존이 일관되지 않다
현재 IllegalArgumentException을 직접 던지면 에러 코드/포맷이 CoreException 경로와 달라져 운영에서 클라이언트 파싱과 장애 분석이 흔들릴 위험이 있다.
중복/인증 실패 등은 CoreException으로 통일하고, 하위 예외가 있는 경우 cause를 보존해 래핑하는 것이 안전하다.
중복 ID, 인증 실패 각각에 대해 동일한 에러 스키마(코드·메시지·필드)가 내려오는지 컨트롤러/E2E 테스트를 추가해야 한다.
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: 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다.

Also applies to: 55-60

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 31 - 40, Replace the direct IllegalArgumentException throws in
MemberService.getMyInfo with CoreException-based errors that use the appropriate
MemberExceptionMessage (e.g., ExistsMember.CANNOT_LOGIN) so the API error schema
is consistent with ApiControllerAdvice; specifically, throw new
CoreException(...) (or the project’s CoreException factory) and pass the
original cause when wrapping (preserve cause) rather than throwing raw
IllegalArgumentException, separate the error instances for "not found" vs
"authentication failed" using the correct MemberExceptionMessage constants, and
add/update controller/E2E tests to assert the unified error payload (code,
message, field) is returned for duplicate/auth failures.


// 3. DTO 변환 및 이름 마스킹 처리
return new MyMemberInfoResponse(
member.getLoginId(),
maskName(member.getName()), // 마스킹 로직 적용
member.getBirthDate(),
member.getEmail()
);
}

@Transactional
public void updatePassword(String userId, String currentPassword, String newPassword) {
// 1. 회원 조회
Member member = memberRepository.findByLoginId(userId)
.orElseThrow(() -> new IllegalArgumentException(MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message()));

// 2. 본인 확인 (기존 비밀번호 일치 여부)
if (!member.isSamePassword(currentPassword)) {
throw new IllegalArgumentException(MemberExceptionMessage.Password.PASSWORD_INCORRECT.message()); // 적절한 메시지로 변경 가능
}
Comment on lines +54 to +60
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

비밀번호 오류 메시지 차이로 계정 존재 여부가 노출될 수 있다
존재하지 않는 아이디는 CANNOT_LOGIN, 비밀번호 오류는 PASSWORD_INCORRECT로 구분되어 응답이 달라져 계정 열거 공격에 악용될 수 있다.
외부로는 동일한 메시지/코드(CANNOT_LOGIN)로 통일하고, 상세 원인은 내부 로그로만 남기는 방식이 안전하다.
잘못된 아이디와 잘못된 비밀번호 두 케이스 모두 동일한 에러 응답이 내려오는지 통합 테스트를 추가해야 한다.

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java`
around lines 54 - 60, The current login flow in MemberService differentiates
missing accounts (memberRepository.findByLoginId -> CANNOT_LOGIN) from wrong
passwords (member.isSamePassword -> PASSWORD_INCORRECT), which leaks account
existence; change the password-check branch to throw the same CANNOT_LOGIN error
as the not-found branch (keep detailed cause in an internal log using the member
id or login attempt and exception text), ensure the throw uses
MemberExceptionMessage.ExistsMember.CANNOT_LOGIN.message(), and add/adjust an
integration test to assert that both non-existent user and incorrect password
produce identical external responses while internal logs capture the distinct
reasons.


// 4. 도메인 정책 검증 및 수정 (생년월일 포함 여부 등은 도메인 내 로직에서 처리)
member.updatePassword(newPassword);
}

/**
* 이름의 마지막 글자를 *로 마스킹 처리
*/
private String maskName(String name) {
if (name == null || name.isEmpty()) {
return "";
}
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,15 @@
package com.loopers.application.service.dto;

import lombok.Builder;

import java.time.LocalDate;

@Builder
public record MemberRegisterRequest (
String loginId,
String password,
String name,
LocalDate birthdate,
String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.application.service.dto;

import java.time.LocalDate;

public record MyMemberInfoResponse(
String loginId,
String name,
LocalDate birthdate,
String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.application.service.dto;

public record PasswordUpdateRequest(
String newPassword
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.loopers.controller;

import com.loopers.application.service.MemberService;
import com.loopers.application.service.dto.MemberRegisterRequest;
import com.loopers.application.service.dto.MyMemberInfoResponse;
import com.loopers.application.service.dto.PasswordUpdateRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {

private final MemberService memberService;

/**
* 회원가입
*/
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public void register(@RequestBody MemberRegisterRequest request) {
memberService.register(request);
}

/**
* 내 정보 조회
* 헤더 인증 (ID, PW) 기반
*/
@GetMapping("/me")
public MyMemberInfoResponse getMyInfo(
@RequestHeader("X-Loopers-LoginId") String loginId,
@RequestHeader("X-Loopers-LoginPw") String password
) {
return memberService.getMyInfo(loginId, password);
}

/**
* 비밀번호 수정
* 헤더 인증 (ID, 기존 PW) + 바디 (새 PW)
*/
@PatchMapping("/password")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updatePassword(
@RequestHeader("X-Loopers-LoginId") String loginId,
@RequestHeader("X-Loopers-LoginPw") String currentPassword,
@RequestBody PasswordUpdateRequest request
Comment on lines +31 to +48
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/controller/MemberController.java

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

Length of output: 2029


🏁 Script executed:

# 로그 필터 또는 마스킹 설정 파일 검색
find . -type f \( -name "*Log*" -o -name "*Filter*" \) -path "*/commerce-api/*" | head -20

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

Length of output: 67


🏁 Script executed:

# 보안/인증 관련 설정 파일 검색
find . -type f \( -name "*Security*" -o -name "*Auth*" -o -name "*Config*" \) -path "*/commerce-api/*" | head -20

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

Length of output: 67


🏁 Script executed:

# 헤더 마스킹이나 로깅 관련 설정 검색
rg -i "mask|header|password|sensitive" --type java -A 2 apps/commerce-api/src/main/java/

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

Length of output: 6587


🏁 Script executed:

# MemberService.java 전체 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/application/service/MemberService.java

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

Length of output: 3628


🏁 Script executed:

# 테스트 코드 확인
find . -type f -name "*MemberControllerTest*" -o -name "*MemberServiceTest*" | head -10

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

Length of output: 165


🏁 Script executed:

# 로깅 설정 파일 검색
find . -type f \( -name "logback.xml" -o -name "logback-spring.xml" -o -name "log4j*.xml" -o -name "application*.yml" -o -name "application*.yaml" \) -path "*/commerce-api/*"

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

Length of output: 139


🏁 Script executed:

# 헤더 로깅 관련 필터 구현 여부 확인
rg -i "filter|interceptor|logging" --type java -l apps/commerce-api/src/main/java/ | head -20

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

Length of output: 67


🏁 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:

# MemberServiceTest.java 확인
cat -n apps/commerce-api/src/test/java/com/loopers/application/MemberServiceTest.java

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

Length of output: 5710


🏁 Script executed:

# 전체 프로젝트의 Filter/Interceptor 구현 검색
find . -type f -name "*Filter*.java" -o -name "*Interceptor*.java" | grep -v node_modules | head -20

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

Length of output: 67


🏁 Script executed:

# 헤더 로그 마스킹 관련 구현 확인
rg "X-Loopers" --type java -B 2 -A 2

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

Length of output: 4666


비밀번호를 커스텀 헤더로 전달하면 운영 환경에서 심각한 정보 유출 위험이 있다

비밀번호를 X-Loopers-LoginPw 헤더로 전달하면 접근 로그, APM, 프록시, 네트워크 모니터링 도구에서 헤더가 그대로 수집되어 보안 사고로 이어질 수 있다. 현재 구조에서는 비밀번호 마스킹을 위한 로그 필터가 구현되지 않았다.

표준 Authorization 헤더(예: Bearer 토큰, Basic 인증) 또는 세션/쿠키 기반 인증으로 전환해야 한다. 불가피하게 헤더 기반 인증을 유지하려면 다음을 필수로 적용해야 한다:

  • 로그 필터에서 X-Loopers-LoginPw 헤더를 마스킹 처리
  • 모든 로깅 라이브러리(Logback, SLF4J 등)에서 민감 헤더를 제외하거나 마스킹

인증 실패/성공 시나리오와 민감 헤더가 로그에 기록되지 않음을 검증하는 보안 테스트를 추가해야 한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/controller/MemberController.java`
around lines 31 - 48, The controller currently reads raw passwords from the
custom header "X-Loopers-LoginPw" in MemberController.getMyInfo and
updatePassword; remove this practice by eliminating the
`@RequestHeader`("X-Loopers-LoginPw") parameters and instead accept credentials
via a standard mechanism (preferably Authorization header with Bearer/Basic
handled by your security layer or session/cookie authentication) and update
memberService.getMyInfo and memberService.updatePassword signatures to take the
authenticated principal or token-derived user identity and a request body for
new passwords (e.g., PasswordUpdateRequest); if you cannot migrate immediately,
add a Servlet filter/logging mask that strips or masks "X-Loopers-LoginPw" from
request/response logs and ensure all logging frameworks (Logback/SLF4J) are
configured to exclude or mask that header, then add security tests verifying no
sensitive header values appear in logs on success/failure.

) {
memberService.updatePassword(loginId, currentPassword, request.newPassword());
Comment on lines +21 to +50
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find apps/commerce-api/src/main -name "*Controller.java" -type f | head -5

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

Length of output: 253


🏁 Script executed:

find apps/commerce-api/src/main -name "*ControllerAdvice.java" -o -name "*ExceptionHandler.java" | head -5

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

Length of output: 168


🏁 Script executed:

find apps -name "CoreException.java" -o -name "*Exception.java" | grep -i core | head -5

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

Length of output: 161


🏁 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: 7255


🏁 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:

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

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

Length of output: 2029


필수 헤더 누락 시 표준 에러 포맷이 깨질 수 있다

@RequestHeader의 기본값은 required=true이므로, 헤더가 누락되면 MissingRequestHeaderException이 발생한다. 그런데 ApiControllerAdvice에는 이 예외에 대한 명시적 핸들러가 없어서, 제네릭 Throwable 핸들러로 빠진다. 결과적으로 INTERNAL_ERROR 상태코드와 일반 메시지를 반환하게 되어, 다른 검증 오류들(MissingServletRequestParameterException, HttpMessageNotReadableException)의 BAD_REQUEST 포맷과 일치하지 않는다.

LoginId, LoginPw 헤더가 필수인 경우, required=false로 설정한 후 null/blank를 수동으로 검증하고 CoreException으로 변환해야 한다. 이렇게 하면 ApiControllerAdvice의 CoreException 핸들러를 거쳐 일관된 BAD_REQUEST 응답이 반환된다.

<details>
<summary>예시</summary>

```java
`@GetMapping`("/me")
public MyMemberInfoResponse getMyInfo(
        `@RequestHeader`(value = "X-Loopers-LoginId", required = false) String loginId,
        `@RequestHeader`(value = "X-Loopers-LoginPw", required = false) String password
) {
    if (loginId == null || loginId.isBlank()) {
        throw new CoreException(ErrorType.BAD_REQUEST, "필수 헤더 'X-Loopers-LoginId'가 누락되었습니다.");
    }
    if (password == null || password.isBlank()) {
        throw new CoreException(ErrorType.BAD_REQUEST, "필수 헤더 'X-Loopers-LoginPw'가 누락되었습니다.");
    }
    return memberService.getMyInfo(loginId, password);
}
```

헤더/바디 누락 케이스에서 BAD_REQUEST 상태코드와 표준 에러 포맷이 반환되는지 단위 테스트와 E2E 테스트를 추가한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/controller/MemberController.java`
around lines 21 - 50, MemberController's header parameters (getMyInfo and
updatePassword) currently rely on required=true which throws
MissingRequestHeaderException and bypasses ApiControllerAdvice's CoreException
handler; change the `@RequestHeader` annotations for "X-Loopers-LoginId" and
"X-Loopers-LoginPw" in MemberController.getMyInfo and
MemberController.updatePassword to required=false, then add explicit null/blank
checks that throw new CoreException(ErrorType.BAD_REQUEST, "<clear message>")
when missing; ensure these checks are applied to both loginId and password (and
currentPassword where applicable) so ApiControllerAdvice handles them and add
unit/E2E tests asserting BAD_REQUEST and the standard error body for missing
header and missing body cases.

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

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

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

boolean existsByLoginId(String inputId);

Optional<Member> findByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
return failureResponse(ErrorType.INTERNAL_ERROR, null);
}

// TODO: 예외처리 변경하는 게 좋을 것 같음
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("IllegalArgumentException : {}", e.getMessage());
return ResponseEntity.status(401)
.body(ApiResponse.fail("UNAUTHORIZED", e.getMessage()));
}
Comment on lines +116 to +122
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

HTTP 상태 코드 시맨틱 위반: 모든 IllegalArgumentException에 401 반환

IllegalArgumentException은 다양한 검증 실패(아이디 중복, 형식 오류, 비밀번호 불일치 등)에서 발생한다. 이를 모두 401 Unauthorized로 반환하면:

  1. 운영 관점: 클라이언트가 인증 실패와 입력 검증 실패를 구분할 수 없어 재시도 로직 구현이 어렵다.
  2. 모니터링 관점: 401 에러율 알림 설정 시 실제 인증 문제와 단순 입력 오류가 섞여 장애 판단이 어렵다.
  3. API 규약: RFC 7235에 따르면 401은 인증 자격 증명 누락/무효 시에만 사용해야 한다.

TODO 주석에서 인지하고 있듯이, 예외 타입을 세분화하거나 CoreException으로 라우팅하여 적절한 상태 코드를 반환해야 한다.

수정안:

  • 인증 실패(PASSWORD_INCORRECT, CANNOT_LOGIN) → 401
  • 입력 검증 실패(INVALID_ID_FORMAT, DUPLICATE_ID_EXISTS) → 400 또는 409
  • CoreException 기반으로 통합하여 일관된 에러 처리 패턴 유지
수정 제안 예시
-    // TODO: 예외처리 변경하는 게 좋을 것 같음
-    `@ExceptionHandler`(IllegalArgumentException.class)
-    public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException e) {
-        log.warn("IllegalArgumentException : {}", e.getMessage());
-        return ResponseEntity.status(401)
-                .body(ApiResponse.fail("UNAUTHORIZED", e.getMessage()));
-    }
+    // IllegalArgumentException 대신 CoreException을 사용하여
+    // 각 예외 상황에 맞는 ErrorType을 지정하도록 도메인/서비스 레이어 수정 필요

추가 테스트: 아이디 중복, 형식 오류, 비밀번호 불일치 각각에 대해 올바른 HTTP 상태 코드가 반환되는지 E2E 테스트를 보강해야 한다. Based on learnings: "enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format."

🤖 Prompt for AI Agents
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java`
around lines 116 - 122, handleIllegalArgumentException in ApiControllerAdvice
currently maps all IllegalArgumentException to 401 which misrepresents
validation vs auth failures; change error handling so IllegalArgumentException
is not blanket-mapped to 401: introduce or use specific exception types (e.g.,
AuthenticationException / PasswordIncorrectException for auth failures,
ValidationException for input format errors, DuplicateResourceException for
duplicate-id) or wrap into a CoreException with an error code, then add/modify
handlers in ApiControllerAdvice (methods handling CoreException,
AuthenticationException, ValidationException, DuplicateResourceException) to
return appropriate status codes (401 for auth, 400 for validation, 409 for
conflicts) and consistent ApiResponse payloads; update any throw sites to throw
the specific exceptions or CoreException and extend E2E tests to assert correct
HTTP status for duplicate-id, invalid-format, and password-incorrect cases.


private String extractMissingParameter(String message) {
Pattern pattern = Pattern.compile("'(.+?)'");
Matcher matcher = pattern.matcher(message);
Expand Down
Loading