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
c0dcfd8
remove: deprecated codeguide
hanyoung-kurly Feb 1, 2026
78cf859
feat: claude.md 생성
Namjin-kimm Feb 3, 2026
357da15
feat: 회원 객체 생성 및 단위 테스트 코드 작성
Namjin-kimm Feb 3, 2026
5ac9342
feat: 회원가입 도메인 모델 생성 및 단위 테스트 코드 작성
Namjin-kimm Feb 5, 2026
e488bd0
feat: 회원가입 서비스 및 인프라 구현 및 단위/통합 테스트 코드 작성
Namjin-kimm Feb 5, 2026
6e9f89f
feat: 회원가입 API 엔드포인트 및 E2E 테스트코드 작성
Namjin-kimm Feb 5, 2026
f42cce5
docs: claude.md 수정 - 테스트 데이터 상수 선언 규칙 추가
Namjin-kimm Feb 5, 2026
c2620b8
feat: 인증 인터셉터 구현 및 단위 테스트 코드 작성
Namjin-kimm Feb 6, 2026
d3cdf71
feat: 내 정보 조회 및 비밀번호 변경 테스트코드 및 기능 구현
Namjin-kimm Feb 6, 2026
fea8a99
refactor: Command 객체를 domain에서 application 레이어로 이동
Namjin-kimm Feb 6, 2026
a61624a
refactor: UserService에 트랜잭션 경계 설정
Namjin-kimm Feb 6, 2026
57814f3
fix: UserService 통합테스트 코드 오류 타입 불일치 수정
Namjin-kimm Feb 6, 2026
33a5b13
fix: UserModel 단위테스트 코드에서 하드코딩된 미래 날짜를 항상 현재 날짜의 1년 뒤로 수정
Namjin-kimm Feb 6, 2026
9587b68
fix: UserModel 단위테스트 코드에서 생년월일 검증 테스트 케이스 추가
Namjin-kimm Feb 6, 2026
5d5066c
refactor: 회원가입시 중복 ID 확인 로직을 findByLoginId 메서드를 호출해서 객체를 반환받아서 하는게 아닌…
Namjin-kimm Feb 6, 2026
7e69f0c
refactor: null 객체 생성 방지
Namjin-kimm Feb 6, 2026
c4d7354
refactor: AuthInterceptor 로깅 설정
Namjin-kimm Feb 6, 2026
07b832b
refactor: UserServiceTest 가독성 강화. 새 비밀번호가 현재 비밀번호와 중복 테스트해서 stub을 한 번…
Namjin-kimm Feb 6, 2026
eac2aad
refactor: 인증 인터셉터 테스트에 헤더의 로그인 Id정보가 빈값으로 오는 경우 테스트 코드 추가
Namjin-kimm Feb 6, 2026
c5854c7
refactor: 인증헤더 누락 E2E test에서 HttpEntity 전달 시 null 아닌 빈 HttpEntity를 명시…
Namjin-kimm Feb 6, 2026
File filter

Filter by extension

Filter by extension

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

This file was deleted.

144 changes: 144 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# CLAUDE.md

## 프로젝트 개요

Spring Boot 기반 멀티 모듈 Java 프로젝트 (`loopers-java-spring-template`).
REST API, Batch, Kafka Streaming 3개의 실행 가능한 앱과 재사용 가능한 인프라/지원 모듈로 구성된다.

## 기술 스택 및 버전

| 구성 요소 | 버전 |
|-----------|------|
| Java | 21 (Toolchain) |
| Spring Boot | 3.4.4 |
| Spring Cloud Dependencies | 2024.0.1 |
| Spring Dependency Management | 1.1.7 |
| Kotlin | 2.0.20 |
| springdoc-openapi | 2.7.0 |
| QueryDSL | Jakarta |
| MySQL Connector/J | Spring Boot 관리 |
| Mockito | 5.14.0 |
| spring-mockk | 4.0.2 |
| instancio-junit | 5.0.2 |

## 모듈 구조

```
root
├── apps/ # 실행 가능한 Spring Boot 애플리케이션
│ ├── commerce-api/ # REST API (포트 8080, 관리 포트 8081)
│ ├── commerce-batch/ # Spring Batch 처리
│ └── commerce-streamer/ # Kafka 스트리밍 처리
├── modules/ # 재사용 가능한 인프라 모듈
│ ├── jpa/ # JPA/Hibernate + HikariCP 설정
│ ├── redis/ # Redis Master-Replica 설정 (Lettuce)
│ └── kafka/ # Kafka Producer/Consumer 설정
└── supports/ # 부가 기능 모듈
├── jackson/ # JSON 직렬화 설정
├── logging/ # Logback + Slack 알림
└── monitoring/ # Prometheus + Micrometer 메트릭
```

- `apps/` 모듈만 `bootJar` 활성화, 나머지는 `jar` 태스크만 활성화

## 아키텍처 패턴

Layered Hexagonal Architecture (Ports & Adapters):

```
interfaces/ → Controller, API Spec, DTO (HTTP 계층)
application/ → Facade (유스케이스 조율)
domain/ → Model, Service, Repository 인터페이스 (비즈니스 로직)
infrastructure/→ Repository 구현체, JPA Repository (기술 어댑터)
support/ → 공통 에러, 유틸리티
```

의존성 방향: `interfaces → application → domain ← infrastructure`

## 빌드 및 실행

```bash
# 빌드
./gradlew build

# commerce-api 실행
./gradlew :apps:commerce-api:bootRun

# commerce-batch 실행 (job 이름 지정 필수)
./gradlew :apps:commerce-batch:bootRun --args='--spring.batch.job.name=demoJob'

# 테스트
./gradlew test

# 인프라 (MySQL, Redis, Kafka)
docker compose -f infra-compose.yml up -d

# 모니터링 (Prometheus, Grafana)
docker compose -f monitoring-compose.yml up -d
```

## 주요 컨벤션

### 엔티티
- `BaseEntity`를 상속하여 `id`, `created_at`, `updated_at`, `deleted_at` 자동 관리
- Soft delete 패턴 (`delete()`, `restore()` 메서드)
- Protected 기본 생성자, `@PrePersist`/`@PreUpdate` 훅 사용
- 타임존: UTC 저장, Asia/Seoul 표시

### API 응답
```json
{
"meta": { "result": "SUCCESS|FAIL", "errorCode": "...", "message": "..." },
"data": { }
}
```

### 에러 처리
- `CoreException(ErrorType, message)` → `ApiControllerAdvice`에서 일괄 처리
- ErrorType: `BAD_REQUEST(400)`, `NOT_FOUND(404)`, `CONFLICT(409)`, `INTERNAL_ERROR(500)`

### DTO 매핑 흐름
```
Entity → ExampleInfo (application DTO) → ExampleV1Dto (API DTO) → ApiResponse
```
- Java Record로 불변 DTO 정의, `from()` 팩토리 메서드 사용

### 네이밍
- 패키지: `com.loopers.*`
- API 버전: 클래스명에 `V1`, `V2` 접미사 (예: `ExampleV1Controller`)
- DTO: `Dto` 또는 `Info` 접미사
- Repository: `Repository` (인터페이스), `JpaRepository` (JPA)

### 테스트
- JUnit 5, `@Nested` + `@DisplayName`(한국어) 사용
- 통합 테스트: `@SpringBootTest(webEnvironment = RANDOM_PORT)` + `TestRestTemplate`
- Testcontainers: MySQL, Redis, Kafka
- `DatabaseCleanUp` 픽스처로 테스트 간 데이터 정리 (`@AfterEach`)
- JaCoCo 코드 커버리지 (XML 리포트)
- 프로파일: `test`, 순차 실행 (`maxParallelForks = 1`)
- 테스트 데이터 중 여러 테스트에서 반복 사용되는 값은 클래스 레벨 상수(`private static final`)로 선언한다

### 코드 스타일
- Lombok: `@RequiredArgsConstructor`, `@Getter`, `@Slf4j`
- Jackson: `NON_NULL` 직렬화, 빈 문자열 → null 역직렬화
- 비즈니스 컨텍스트 주석은 한국어 사용

## 환경 프로파일

`local`, `test`, `dev`, `qa`, `prd`

| 설정 | local/test | dev/qa/prd |
|------|-----------|------------|
| DDL auto | create | none |
| SQL 로깅 | 활성화 | 비활성화 |
| Swagger | 활성화 | prd만 비활성화 |
| Batch 스키마 | always | never |

## 환경 변수

| 변수 | 용도 |
|------|------|
| `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PWD` | MySQL 접속 |
| `REDIS_MASTER_HOST`, `REDIS_MASTER_PORT` | Redis Master |
| `REDIS_REPLICA_1_HOST`, `REDIS_REPLICA_1_PORT` | Redis Replica |
| `BOOTSTRAP_SERVERS` | Kafka 브로커 |
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 (PasswordEncoder 사용)
implementation("org.springframework.security:spring-security-crypto")
Comment on lines +13 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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 파일 존재 확인 및 내용 조회
find . -name "build.gradle.kts" -type f | head -20

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

Length of output: 429


🏁 Script executed:

# 특정 파일 내용 확인
if [ -f "apps/commerce-api/build.gradle.kts" ]; then
  cat -n "apps/commerce-api/build.gradle.kts"
else
  echo "File not found at apps/commerce-api/build.gradle.kts"
fi

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

Length of output: 1290


🏁 Script executed:

# 버전 관리 관련 파일 검색
find . -type f \( -name "gradle.properties" -o -name "libs.versions.toml" -o -name "versions.gradle" -o -name "*.bom" \) | head -20

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

Length of output: 104


🏁 Script executed:

# spring-security 관련 의존성 검색
rg "spring-security" -A 2 -B 2 --type gradle

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

Length of output: 233


🏁 Script executed:

# gradle.properties 파일 내용 확인
cat -n gradle.properties

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

Length of output: 747


🏁 Script executed:

# 상위 build.gradle.kts 파일 확인
cat -n build.gradle.kts | head -100

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

Length of output: 4157


🏁 Script executed:

# spring-security 관련 의존성 검색 (파일별)
echo "=== 모든 build.gradle.kts 파일에서 spring-security 검색 ==="
grep -r "spring-security" --include="build.gradle.kts" . 2>/dev/null | head -50

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

Length of output: 247


🏁 Script executed:

# springSecurityCryptoVersion이 정의되어 있는지 확인
grep -r "springSecurityCryptoVersion\|spring-security" gradle.properties 2>/dev/null

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

Length of output: 67


🏁 Script executed:

# Spring Boot 버전 확인 및 spring-security 포함 여부 검색
echo "=== Spring Boot Version ==="
grep -A 2 "springBootVersion" gradle.properties

echo -e "\n=== 다른 build.gradle.kts 파일들의 의존성 관리 패턴 ==="
for file in ./apps/*/build.gradle.kts ./modules/*/build.gradle.kts ./supports/*/build.gradle.kts; do
  if [ -f "$file" ]; then
    echo "=== $file ==="
    grep "implementation\|annotationProcessor" "$file" | head -10
  fi
done

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

Length of output: 3251


🏁 Script executed:

# Spring Security 관련 의존성 관리 확인
echo "=== Spring Security 관련 의존성 전체 검색 ==="
grep -r "spring-security" . --include="*.gradle" --include="*.gradle.kts" --include="*.properties" --include="*.toml" 2>/dev/null

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

Length of output: 232


spring-security-crypto 의존성의 버전을 gradle.properties에서 명시적으로 관리해야 한다.

현재 버전이 고정되지 않아 빌드 시점에 따라 해상되는 버전이 달라질 수 있고, 운영 환경에서 보안 취약점 발생 시 대응이 지연된다. 프로젝트의 다른 외부 라이브러리(springdoc-openapi 등)는 gradle.properties에서 버전을 관리하고 있으므로 spring-security-crypto도 동일한 패턴으로 통일해야 한다.

gradle.properties에 springSecurityCryptoVersion= 항목을 추가하고, build.gradle.kts에서 ${project.properties["springSecurityCryptoVersion"]}로 참조하도록 수정해야 한다. 추가로 CI 파이프라인에 의존성 취약점 스캔 단계를 추가하여 정기적으로 의존성 버전의 보안 상태를 점검하는 편이 낫다.

수정 예시

gradle.properties에 추가:

springSecurityCryptoVersion=6.4.2

build.gradle.kts 수정:

-    implementation("org.springframework.security:spring-security-crypto")
+    implementation("org.springframework.security:spring-security-crypto:${project.properties["springSecurityCryptoVersion"]}")
🤖 Prompt for AI Agents
In `@apps/commerce-api/build.gradle.kts` around lines 13 - 14, Add a new
gradle.properties entry named springSecurityCryptoVersion and update the
implementation dependency in build.gradle.kts to reference that property (use
project.properties["springSecurityCryptoVersion"] or the Kotlin DSL property
accessor) instead of a hard-coded coordinate; specifically, add the
springSecurityCryptoVersion key to gradle.properties and replace the
implementation("org.springframework.security:spring-security-crypto") line in
apps/commerce-api/build.gradle.kts to use the property reference; also ensure CI
dependency-vulnerability scanning is added/updated to regularly check the
project's dependencies.

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,7 @@
package com.loopers.application.user;

public record ChangePasswordCommand(
String loginId,
String currentPassword,
String newPassword
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.application.user;

public record SignupCommand(
String loginId,
String password,
String name,
String birthday,
String email
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.loopers.application.user;

import com.loopers.domain.user.UserModel;
import com.loopers.domain.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class UserFacade {
private final UserService userService;

public UserInfo signUp(SignupCommand command) {
UserModel user = userService.signup(
command.loginId(), command.password(), command.name(), command.birthday(), command.email()
);
return UserInfo.from(user);
}

public UserInfo getMyInfo(String loginId) {
UserModel user = userService.findByLoginId(loginId);
return UserInfo.from(user);
}

public void changePassword(ChangePasswordCommand command) {
userService.changePassword(command.loginId(), command.currentPassword(), command.newPassword());
}
}
Comment on lines +8 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 . -name "UserService.java" -type f

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

Length of output: 159


🏁 Script executed:

rg -n "@Transactional|@Service|public.*signup|public.*changePassword|public.*findByLoginId" --type=java -A 2 -B 1 -g "**/UserService.java"

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

Length of output: 1622


🏁 Script executed:

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

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

Length of output: 3451


트랜잭션 경계가 없어 데이터 정합성 위험이 존재한다.

UserService@Transactional 어노테이션이 전혀 적용되지 않았다. signup 메서드는 중복 체크(읽기) 후 사용자를 저장(쓰기)하고, changePassword 메서드도 사용자 조회(읽기) 후 비밀번호를 변경하고 저장(쓰기)한다. 이 과정에서 예외 발생 시 데이터 정합성이 보장되지 않는다.

예를 들어 signup에서 중복 체크는 통과했으나 저장 중 DB 오류가 발생하면 부분적으로 성공한 상태가 남을 수 있다. changePassword에서도 검증은 완료했으나 저장 실패 시 불일치 상태가 된다.

개선안:

  • UserService@Transactional 적용 (클래스 레벨 또는 signup, changePassword 메서드 레벨)
  • 테스트: 각 메서드에서 저장 단계 예외 발생 시 변경사항이 롤백되는지 검증
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java`
around lines 8 - 28, The UserService currently lacks transaction boundaries
causing potential data inconsistency; add `@Transactional` to the UserService
(class-level or on the signup and changePassword methods) so the signup and
changePassword flows (which perform read-then-write: UserService.signup and
UserService.changePassword) run in a single transaction and roll back on
exceptions, ensure the annotation uses default rollback-on-runtime-exception
semantics (or explicitly set rollbackFor if checked exceptions are used), and
add unit/integration tests that simulate an exception during the
persistence/save step to verify the operations rollback and no partial state is
persisted.

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

import com.loopers.domain.user.UserModel;

import java.time.LocalDate;
import java.util.Objects;

public record UserInfo(
Long id,
String loginId,
String name,
LocalDate birthday,
String email
) {
public static UserInfo from(UserModel userModel) {
Objects.requireNonNull(userModel, "userModel은 null일 수 없습니다.");
return new UserInfo(
userModel.getId(),
userModel.getLoginId(),
userModel.getName(),
userModel.getBirthday(),
userModel.getEmail()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.loopers.config;

import com.loopers.domain.user.UserService;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RequiredArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);

private static final String HEADER_LOGIN_ID = "X-Loopers-LoginId";
private static final String HEADER_LOGIN_PW = "X-Loopers-LoginPw";
Comment on lines +20 to +21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

보안 및 운영 관점에서 인증 방식 검토가 필요하다.

매 요청마다 헤더에 비밀번호를 평문으로 전송하는 방식은 다음 문제가 있다:

  1. 보안 위험: 비밀번호가 매 요청마다 네트워크를 통해 전송되어 노출 위험이 증가한다. HTTPS 환경이더라도 로그, 프록시, CDN 등에서 헤더가 기록될 수 있다.
  2. 운영 부담: 매 요청마다 authenticate()가 호출되어 DB 조회 + 비밀번호 해시 비교가 발생한다. 트래픽 증가 시 DB 부하가 급증할 수 있다.

권장 수정안: JWT 토큰 또는 세션 기반 인증으로 전환하여, 로그인 시 한 번만 인증하고 이후 토큰/세션으로 검증하는 방식을 고려해야 한다.

학습/프로토타입 목적이라면 현재 방식을 유지하되, 프로덕션 배포 전 반드시 개선이 필요하다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/config/AuthInterceptor.java`
around lines 16 - 17, 현재 AuthInterceptor가 HEADER_LOGIN_ID/HEADER_LOGIN_PW을 통해
매요청 평문 비밀번호를 전송받아 authenticate()를 호출하는 방식은 보안·성능상 문제이므로, AuthInterceptor를 JWT 또는
세션 기반으로 전환하도록 수정하세요: 로그인 엔드포인트에서 사용자 인증 후 JWT 토큰(또는 세션 ID)을 발급하고 클라이언트는 이후 요청에
HEADER_LOGIN_ID 대신 Authorization: Bearer <token>을 전송하도록 변경하며, AuthInterceptor는
HEADER_LOGIN_PW와 직접 비밀번호 비교를 하지 않고 토큰의 서명/만료를 검증하거나 세션 스토어 조회만 수행하도록 구현하세요; 학습
목적으로 단기 유지 시에도 authenticate()를 매요청 호출하지 않도록 토큰 캐시/세션 캐시를 도입하여 DB 조회를 줄이도록 조치하세요.

public static final String ATTR_LOGIN_ID = "loginId";

private final UserService userService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String loginId = request.getHeader(HEADER_LOGIN_ID);
String loginPw = request.getHeader(HEADER_LOGIN_PW);

if (loginId == null || loginId.isBlank() || loginPw == null || loginPw.isBlank()) {
log.warn("인증 헤더 누락 - URI: {}, RemoteAddr: {}", request.getRequestURI(), request.getRemoteAddr());
throw new CoreException(ErrorType.UNAUTHORIZED, "인증 헤더가 누락되었습니다.");
}

try{
userService.authenticate(loginId, loginPw);
}catch(CoreException e){
log.warn("인증 실패 - loginId: {}, URI: {}", loginId, request.getRequestURI());
Comment on lines +38 to +39
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

로그에 loginId(사용자 식별자)가 기록되고 있다.

loginId는 사용자 식별 정보(PII)에 해당할 수 있다. 인증 실패 시 디버깅 목적으로 유용하지만, 로그 수집 시스템에 평문으로 적재되면 개인정보 보호 규정(GDPR/CCPA 등) 위반 소지가 있다. 운영 환경에서는 loginId를 마스킹하거나 해시 처리하여 기록하는 것을 권장한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/config/AuthInterceptor.java`
around lines 38 - 39, AuthInterceptor currently logs the raw loginId on
authentication failure (see the catch block in AuthInterceptor where
log.warn("인증 실패 - loginId: {}, URI: {}", loginId, request.getRequestURI()) is
called); change this to log a non-PII-safe representation by masking or hashing
the loginId before logging (implement a helper like maskLoginId(String loginId)
or hashLoginId(String loginId) and use it in the catch block) so the log call
records only the masked/hashed value and not the plain identifier.

throw e;
}
Comment on lines +36 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "ApiControllerAdvice.java" -o -name "*ControllerAdvice*.java" | head -20

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

Length of output: 170


🏁 Script executed:

find . -name "ErrorType.java" -o -name "*ErrorType*.java" | head -20

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

Length of output: 159


🏁 Script executed:

# Also search for AuthInterceptor to confirm its exact location and structure
find . -name "AuthInterceptor.java" -exec head -50 {} +

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

Length of output: 1821


🏁 Script executed:

cat ./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: 5938


🏁 Script executed:

cat ./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: 917


🏁 Script executed:

# Verify Spring MVC interceptor exception handling in Spring Boot context
# Check if there are any other exception handlers that might affect interceptor exceptions
rg "ExceptionHandler|@ControllerAdvice|@RestControllerAdvice" --type java -A 2 | head -40

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

Length of output: 3854


🏁 Script executed:

# Check if there are any tests or documentation about interceptor exception handling
fd "test" --type f -name "*Test.java" -o -name "*Tests.java" | xargs grep -l "AuthInterceptor\|preHandle" | head -5

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

Length of output: 315


인증 헤더 누락 시 원격 주소 대신 loginId를 로그에 남기는 것은 민감정보 노출 위험이다.

라인 39에서 log.warn("인증 실패 - loginId: {}, URI: {}", loginId, request.getRequestURI());로 사용자 식별자(loginId)를 직접 로그에 기록한다. loginId는 사용자 개인정보이므로 운영 환경 로그에 남길 경우 보안 정책 위반 위험이 있다.

로그를 log.warn("인증 실패 - URI: {}, RemoteAddr: {}", request.getRequestURI(), request.getRemoteAddr());로 수정하여 사용자 식별 정보는 제외하고, 요청 발원 정보만 기록하도록 수정한다.

🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/config/AuthInterceptor.java`
around lines 36 - 41, The catch block in AuthInterceptor around
userService.authenticate currently logs the sensitive loginId; update the
log.warn call in the CoreException handler to remove the loginId and instead log
non-sensitive request details (use request.getRequestURI() and
request.getRemoteAddr()) so no user identifiers are written; keep the catch
throwing the exception after logging. Ensure the change is applied to the
log.warn line referenced in the CoreException catch within AuthInterceptor.

request.setAttribute(ATTR_LOGIN_ID, loginId);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.loopers.config;

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

@Configuration
public class SecurityConfig {

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

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthInterceptor authInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/v1/users/signup");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.loopers.domain.user;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;

public class Password {

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

public static void validate(String value, String birthday) {
if (value == null || value.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 비어있을 수 없습니다.");
}

if (value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8자리 이상, 16자리 이하이어야 합니다.");
}

if (!value.matches(PASSWORD_PATTERN)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 형식이 올바르지 않습니다.");
}

String birthdayWithoutDash = birthday.replace("-", "");
if (value.contains(birthday) || value.contains(birthdayWithoutDash)) {
throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다.");
Comment on lines +12 to +27
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

birthday가 null이면 NPE로 500이 발생할 수 있다
birthday가 null일 때 replace 호출로 NPE가 발생하면 운영에서 400이 아니라 500으로 떨어져 모니터링 지표와 장애 원인 분석이 왜곡된다.
birthday null/blank를 먼저 검증해 CoreException(BAD_REQUEST)로 처리하거나, null이면 생일 포함 검사를 건너뛰도록 분기하는 방식으로 수정하는 편이 안전하다.
추가 테스트로 birthday가 null 또는 공백인 경우 BAD_REQUEST가 반환되는 단위 테스트를 추가하는 편이 낫다.

🔧 예시 수정안
 public static void validate(String value, String birthday) {
     if (value == null || value.isBlank()) {
         throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 비어있을 수 없습니다.");
     }
 
+    if (birthday == null || birthday.isBlank()) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다.");
+    }
     if (value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
         throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8자리 이상, 16자리 이하이어야 합니다.");
     }
 
     if (!value.matches(PASSWORD_PATTERN)) {
         throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호 형식이 올바르지 않습니다.");
     }
 
     String birthdayWithoutDash = birthday.replace("-", "");
     if (value.contains(birthday) || value.contains(birthdayWithoutDash)) {
         throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다.");
     }
 }
🤖 Prompt for AI Agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/Password.java` around
lines 12 - 27, Password.validate currently calls birthday.replace("-","") and
will NPE if birthday is null; update Password.validate to first handle
null/blank birthday by either throwing CoreException(ErrorType.BAD_REQUEST) when
birthday is null/blank or by skipping the birthdate containment check when
birthday is null; specifically guard the birthday variable before calling
birthday.replace(...) and before checking value.contains(...), and add unit
tests for Password.validate covering birthday == null and birthday == "" to
assert BAD_REQUEST (or the chosen behavior) so the method no longer throws an
NPE.

}
}
}
Loading