Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 개요리프레시 토큰 기반 인증 시스템을 구현하는 광범위한 변경사항입니다. 새로운 시퀀스 다이어그램sequenceDiagram
actor User
participant AuthController
participant UserFacade
participant UserWriteService
participant TokenProvider
participant UserRepository
participant RefreshTokenEntity
rect rgb(220, 240, 255)
Note over User,RefreshTokenEntity: 로그인 플로우
User->>AuthController: POST /login (email, password)
AuthController->>UserFacade: login(LoginCommand)
UserFacade->>UserWriteService: validUser(command)
UserWriteService->>UserRepository: findByEmail()
UserWriteService->>UserWriteService: 비밀번호 검증
UserWriteService->>TokenProvider: createAccessToken()
UserWriteService->>TokenProvider: createRefreshToken()
UserWriteService->>UserRepository: saveRefreshToken()
UserRepository->>RefreshTokenEntity: create(user, token)
UserWriteService-->>UserFacade: LoginInfo (accessToken, refreshToken)
UserFacade-->>AuthController: LoginInfo
AuthController-->>User: LoginResponse {accessToken, refreshToken}
end
rect rgb(240, 255, 230)
Note over User,RefreshTokenEntity: 토큰 갱신 플로우
User->>AuthController: POST /refresh (refreshToken)
AuthController->>UserFacade: refresh(RefreshTokenCommand)
UserFacade->>UserWriteService: refresh(command)
UserWriteService->>UserRepository: findByRefreshToken()
UserWriteService->>RefreshTokenEntity: isExpired() 검증
alt 토큰 유효
UserWriteService->>TokenProvider: createAccessToken()
UserWriteService->>TokenProvider: createRefreshToken()
UserWriteService->>UserRepository: rotate(oldToken, newToken)
UserWriteService-->>UserFacade: TokenInfo (new accessToken, new refreshToken)
else 토큰 만료/불일치
UserWriteService-->>UserFacade: UserErrorCode Exception
end
UserFacade-->>AuthController: TokenInfo
AuthController-->>User: TokenResponse {accessToken, refreshToken}
end
rect rgb(255, 240, 220)
Note over UserWriteService,UserRepository: 일일 스케줄러
UserScheduler->>UserWriteService: deleteExpiredRefreshTokens(cutoffTime)
UserWriteService->>UserRepository: deleteExpiredRefreshTokens(cutoffTime)
UserRepository-->>UserWriteService: 삭제 개수
end
예상 코드 리뷰 소요 시간🎯 4 (복잡함) | ⏱️ ~50분 추가 주의 사항
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
back/src/main/java/com/rouby/user/user/application/service/UserReadService.java (1)
16-46: LGTM! 읽기/쓰기 책임 분리가 잘 이루어졌습니다.인증 관련 로직이 제거되어
UserReadService가 순수한 읽기 작업만 담당하게 되었습니다. 이는 단일 책임 원칙(SRP)에 부합하는 좋은 리팩토링입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (21)
back/src/docs/asciidoc/auth.adoc(1 hunks)back/src/main/java/com/rouby/user/user/application/UserFacade.java(4 hunks)back/src/main/java/com/rouby/user/user/application/dto/command/RefreshTokenCommand.java(1 hunks)back/src/main/java/com/rouby/user/user/application/dto/info/LoginInfo.java(1 hunks)back/src/main/java/com/rouby/user/user/application/dto/info/TokenInfo.java(1 hunks)back/src/main/java/com/rouby/user/user/application/exception/UserErrorCode.java(1 hunks)back/src/main/java/com/rouby/user/user/application/scheduler/UserScheduler.java(1 hunks)back/src/main/java/com/rouby/user/user/application/service/UserReadService.java(1 hunks)back/src/main/java/com/rouby/user/user/application/service/UserWriteService.java(2 hunks)back/src/main/java/com/rouby/user/user/application/service/token/TokenProvider.java(1 hunks)back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.java(1 hunks)back/src/main/java/com/rouby/user/user/domain/entity/User.java(3 hunks)back/src/main/java/com/rouby/user/user/domain/repository/UserRepository.java(2 hunks)back/src/main/java/com/rouby/user/user/infrastructure/persistence/jpa/UserJpaRepositoryCustom.java(1 hunks)back/src/main/java/com/rouby/user/user/infrastructure/persistence/jpa/UserJpaRepositoryCustomImpl.java(2 hunks)back/src/main/java/com/rouby/user/user/infrastructure/token/TokenProviderImpl.java(2 hunks)back/src/main/java/com/rouby/user/user/presentation/AuthController.java(2 hunks)back/src/main/java/com/rouby/user/user/presentation/dto/request/RefreshTokenRequest.java(1 hunks)back/src/main/java/com/rouby/user/user/presentation/dto/response/LoginResponse.java(1 hunks)back/src/main/java/com/rouby/user/user/presentation/dto/response/TokenResponse.java(1 hunks)back/src/test/java/com/rouby/user/user/presentation/AuthControllerTest.java(4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: letsgilit
PR: HI-dle/rouby#230
File: back/src/main/java/com/rouby/batch/config/BatchConfig.java:68-73
Timestamp: 2025-09-09T11:11:40.513Z
Learning: letsgilit prefers simpler configurations over complex ones when the current solution is sufficient for the use case.
🧬 Code graph analysis (3)
back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.java (2)
back/src/main/java/com/rouby/user/user/domain/entity/User.java (1)
Entity(29-265)back/src/main/java/com/rouby/user/user/infrastructure/security/dto/SecurityUser.java (1)
Builder(10-61)
back/src/main/java/com/rouby/user/user/application/dto/info/TokenInfo.java (1)
front/src/features/auth/authService.js (1)
user(102-102)
back/src/main/java/com/rouby/user/user/application/service/UserWriteService.java (1)
back/src/main/java/com/rouby/user/user/application/exception/UserException.java (1)
UserException(6-11)
🔇 Additional comments (20)
back/src/main/java/com/rouby/user/user/application/dto/info/LoginInfo.java (1)
8-11: LGTM! 리프레시 토큰 필드 추가가 적절합니다.레코드에
refreshToken필드가 추가되어 액세스 토큰과 리프레시 토큰을 함께 반환할 수 있게 되었습니다. 구현이 명확하고 간결합니다.back/src/main/java/com/rouby/user/user/domain/entity/User.java (2)
61-62: LGTM! RefreshToken 관계 추가가 적절합니다.
RefreshToken과의 일대다 관계가 올바르게 설정되었으며, cascade 및 orphanRemoval 옵션이 적절합니다.
259-264: 동시성 안전성 검증 완료: 현재 구조에서는 실제 위험 낮음
refreshTokens는HashSet(thread-safe하지 않음)이지만, 모든 접근이@Transactional메서드(validUser(),refresh()) 내에서 이루어져 트랜잭션 수준의 격리가 제공됩니다.- 표준 서블릿 모델에서 각 요청은 독립적인 스레드로 처리되며, 엔티티 인스턴스는 요청별로 격리됩니다.
- 현재 코드베이스에는 명시적 병렬 처리(
@Async,@Scheduled), 엔티티 캐싱(L2 캐시), 또는 공유 상태가 없습니다.권장사항:
- 현재 구조에서는 즉시 문제가 되지 않으나, 향후 reactive 모델, virtual thread, 또는 명시적 병렬 처리 도입 시
ConcurrentHashMap사용을 검토하거나 동시성 가정을 문서화하세요.back/src/docs/asciidoc/auth.adoc (1)
31-45: LGTM! 리프레시 토큰 API 문서가 적절하게 추가되었습니다.새로운 리프레시 API 섹션이 기존 로그인 API와 일관된 형식으로 작성되었으며, 요청 예시와 응답 형식이 명확하게 문서화되었습니다.
back/src/main/java/com/rouby/user/user/presentation/dto/response/TokenResponse.java (1)
5-12: LGTM! 간결하고 명확한 DTO 구조입니다.Record를 사용한 불변 DTO 설계가 적절하며,
from메서드를 통한 변환 로직도 명확합니다.back/src/main/java/com/rouby/user/user/application/dto/info/TokenInfo.java (1)
3-8: LGTM! 애플리케이션 계층의 토큰 정보 전달용 DTO입니다.간결한 구조로 accessToken과 refreshToken을 함께 전달하는 역할이 명확합니다.
back/src/main/java/com/rouby/user/user/application/exception/UserErrorCode.java (1)
34-35: LGTM! Refresh token 관련 에러 코드가 적절하게 추가되었습니다.만료된 토큰과 유효하지 않은 토큰을 명확히 구분하여 클라이언트가 적절한 처리를 할 수 있도록 했습니다. HTTP 상태 코드(401)도 적절합니다.
back/src/main/java/com/rouby/user/user/presentation/AuthController.java (1)
35-39: Refresh 엔드포인트 토큰 로테이션은 구현되었으나, Rate Limiting과 상세 보안 로깅이 필요합니다.검증 결과:
✅ 구현된 기능:
- Token Rotation:
user.rotate()메서드에서 기존 토큰 제거 후 새 토큰 생성- Token Expiration:
isExpiredRefreshToken()메서드로 만료 확인 및 검증- 만료 토큰 정리: UserScheduler에서 주기적 삭제 구현
❌ 미구현/부족한 기능:
- Rate Limiting: 검색 결과 없음 - 동일 사용자의 refresh 요청에 대한 속도 제한 필요
- 상세 보안 로깅: IP 주소, 요청 타임스탬프, 실패 사유 등의 감사 로그 부족
- 토큰 재사용 공격 대응: 재사용 시도 시
IllegalStateException발생하지만, RFC 6819에서 권장하는 "사용된 refresh token 감지 시 사용자의 모든 토큰 무효화" 메커니즘 미구현Rate Limiting 추가 구현과 보안 감시 로깅을 검토하세요.
back/src/main/java/com/rouby/user/user/application/UserFacade.java (2)
97-99: 로그인 로직 이동 승인인증 로직이 ReadService에서 WriteService로 이동한 것은 적절합니다. 이제 Refresh Token을 생성하고 저장하는 쓰기 작업이 포함되므로 WriteService에서 처리하는 것이 맞습니다.
129-131: 새로운 refresh 메서드 구현 확인Facade 패턴에 따라 적절하게 WriteService로 위임하고 있습니다.
back/src/main/java/com/rouby/user/user/domain/repository/UserRepository.java (2)
21-21: Refresh Token 조회 메서드 추가 승인토큰으로 사용자를 조회하는 메서드가 적절하게 정의되었습니다. Optional 반환 타입으로 null 안전성을 확보하고 있습니다.
23-23: 만료된 토큰 삭제 메서드 정의 확인삭제된 토큰 개수를 반환하여 모니터링 및 로깅에 활용할 수 있도록 설계되었습니다.
back/src/test/java/com/rouby/user/user/presentation/AuthControllerTest.java (2)
39-70: 로그인 테스트 업데이트 확인AccessToken과 RefreshToken을 모두 반환하도록 테스트가 올바르게 업데이트되었습니다. LoginInfo 생성자 시그니처 변경에 맞게 수정되었습니다.
104-131: Refresh Token API 테스트 추가 확인새로운 refresh 엔드포인트에 대한 테스트가 적절하게 작성되었습니다. 요청/응답 구조와 REST Docs 문서화가 포함되어 있습니다.
back/src/main/java/com/rouby/user/user/infrastructure/token/TokenProviderImpl.java (1)
21-24: UUID 기반 Refresh Token 생성 확인UUID.randomUUID()를 사용한 Refresh Token 생성 방식은 적절합니다. UUID는 충분히 무작위성이 높고 충돌 가능성이 극히 낮습니다.
back/src/main/java/com/rouby/user/user/presentation/dto/request/RefreshTokenRequest.java (1)
6-13: RefreshTokenRequest DTO 구현 확인Record를 사용한 깔끔한 DTO 설계입니다.
@NotBlank검증과 Application 레이어로의 변환 메서드가 적절하게 구현되었습니다.back/src/main/java/com/rouby/user/user/infrastructure/persistence/jpa/UserJpaRepositoryCustomImpl.java (2)
38-50: findByRefreshToken 구현 확인fetchJoin을 사용하여 N+1 문제를 방지하고 있으며, 삭제되지 않은 사용자만 필터링하는 로직이 적절합니다. Optional 래핑도 올바르게 구현되었습니다.
52-58: 만료 시각 비교 조건 검증 필요Line 56에서
before(cutoffTime)을 사용하고 있습니다. 이는 "strictly before" (미만) 비교입니다. 만료 시각이 정확히 cutoffTime과 같은 토큰도 삭제해야 한다면loe()(less than or equal) 또는before(cutoffTime.plusNanos(1))를 사용해야 합니다. 비즈니스 요구사항을 확인하세요.또한, 토큰 갱신 중에 삭제 작업이 동시에 실행될 경우의 동작을 검증하세요.
@Transactional격리 수준을 고려하여 race condition이 발생하지 않는지 확인이 필요합니다.back/src/main/java/com/rouby/user/user/application/service/UserWriteService.java (2)
211-227: validUser 메서드 구현 확인로그인 인증 및 Refresh Token 생성 로직이 적절하게 구현되었습니다. 이메일 검증, 비밀번호 검증, 토큰 생성 순서가 올바릅니다.
251-254: 만료된 토큰 삭제 메서드 확인Repository로 적절하게 위임하고 있습니다. 삭제된 개수를 반환하여 모니터링에 활용할 수 있습니다.
| public record RefreshTokenCommand( | ||
| String refreshToken | ||
| ) { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
입력 검증 추가를 권장합니다.
refreshToken이 null이거나 빈 문자열일 때 검증이 없습니다. Command 객체는 유효한 데이터만 전달되도록 보장해야 합니다.
다음 중 하나를 적용하세요:
옵션 1: Bean Validation 사용
+import jakarta.validation.constraints.NotBlank;
+
public record RefreshTokenCommand(
+ @NotBlank(message = "리프레시 토큰은 필수입니다.")
String refreshToken
) {옵션 2: 팩토리 메서드로 검증
public record RefreshTokenCommand(
String refreshToken
) {
+ public static RefreshTokenCommand of(String refreshToken) {
+ if (refreshToken == null || refreshToken.isBlank()) {
+ throw new IllegalArgumentException("리프레시 토큰은 필수입니다.");
+ }
+ return new RefreshTokenCommand(refreshToken);
+ }
}📝 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.
| public record RefreshTokenCommand( | |
| String refreshToken | |
| ) { | |
| import jakarta.validation.constraints.NotBlank; | |
| public record RefreshTokenCommand( | |
| @NotBlank(message = "리프레시 토큰은 필수입니다.") | |
| String refreshToken | |
| ) { | |
| } |
| public record RefreshTokenCommand( | |
| String refreshToken | |
| ) { | |
| public record RefreshTokenCommand( | |
| String refreshToken | |
| ) { | |
| public static RefreshTokenCommand of(String refreshToken) { | |
| if (refreshToken == null || refreshToken.isBlank()) { | |
| throw new IllegalArgumentException("리프레시 토큰은 필수입니다."); | |
| } | |
| return new RefreshTokenCommand(refreshToken); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/application/dto/command/RefreshTokenCommand.java
around lines 3 to 5, the record accepts refreshToken without validation; add
input validation so the command never carries null/blank values. Either annotate
the component with Bean Validation (e.g., add
javax.validation.constraints.NotBlank on the refreshToken record component and
ensure validation is triggered where commands are constructed), or replace the
public record constructor with a static factory method that checks for
null/blank and throws an appropriate runtime exception (e.g.,
IllegalArgumentException) before returning the record; implement one of these
approaches and update usages accordingly.
| @Component | ||
| public class UserScheduler { | ||
|
|
||
| private static final LocalDateTime cutoffTime = LocalDateTime.now(); |
There was a problem hiding this comment.
[CRITICAL] cutoffTime이 클래스 로드 시점에 고정되는 문제
static final 필드로 선언된 cutoffTime은 클래스가 로드될 때 단 한 번만 초기화되어 애플리케이션이 재시작될 때까지 변경되지 않습니다. 이는 스케줄러가 매일 실행되더라도 첫 실행 시점의 시간을 계속 사용하게 되어, 만료된 토큰이 제대로 삭제되지 않는 심각한 버그입니다.
다음과 같이 수정하여 메서드 실행 시점의 시간을 사용하도록 변경하세요:
- private static final LocalDateTime cutoffTime = LocalDateTime.now();
private final UserWriteService userWriteService;
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void deleteExpiredRefreshTokens() {
try {
+ LocalDateTime cutoffTime = LocalDateTime.now();
long count = userWriteService.deleteExpiredRefreshTokens(cutoffTime);
log.info("[" + cutoffTime + "] 기준 만료된 RefreshToken 삭제 개수: " + count);
} catch (Exception e) {
log.error(e.getMessage(), 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.
| private static final LocalDateTime cutoffTime = LocalDateTime.now(); | |
| private final UserWriteService userWriteService; | |
| @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") | |
| public void deleteExpiredRefreshTokens() { | |
| try { | |
| LocalDateTime cutoffTime = LocalDateTime.now(); | |
| long count = userWriteService.deleteExpiredRefreshTokens(cutoffTime); | |
| log.info("[" + cutoffTime + "] 기준 만료된 RefreshToken 삭제 개수: " + count); | |
| } catch (Exception e) { | |
| log.error(e.getMessage(), e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/application/scheduler/UserScheduler.java
around line 15, cutoffTime is declared as a static final and therefore fixed at
class-load time; change to compute the cutoff time at method execution (e.g.,
call LocalDateTime.now() inside the scheduled method where it's used) so each
scheduler run uses the current time, remove the static final field or replace
references with a local variable computed at runtime, and ensure any tests or
callers are updated accordingly.
There was a problem hiding this comment.
@letsgilit 괜찮습니다! 😊 제안한 수정사항대로 변경하시면 스케줄러가 매번 실행될 때마다 현재 시간을 사용하게 됩니다. 추가로 도움이 필요하시면 언제든지 말씀해 주세요!
🧠 Learnings used
Learnt from: letsgilit
PR: HI-dle/rouby#230
File: back/src/main/java/com/rouby/batch/config/BatchConfig.java:68-73
Timestamp: 2025-09-09T11:11:40.513Z
Learning: letsgilit prefers simpler configurations over complex ones when the current solution is sufficient for the use case.
| @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") | ||
| public void deleteExpiredRefreshTokens() { | ||
| try { | ||
| long count = userWriteService.deleteExpiredRefreshTokens(cutoffTime); | ||
| log.info("[" + cutoffTime + "] 기준 만료된 RefreshToken 삭제 개수: " + count); | ||
| } catch (Exception e) { | ||
| log.error(e.getMessage(), e); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
스케줄러 예외 처리 개선 제안
현재 모든 예외를 catch하고 로깅만 하고 있습니다. 스케줄러가 실패할 경우 알림을 받을 수 있도록 모니터링 메트릭을 추가하거나, 특정 예외에 대해서는 재시도 로직을 고려해보세요.
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/application/scheduler/UserScheduler.java
around lines 18 to 26, the scheduler currently swallows all exceptions and only
logs them; update the exception handling to (1) catch specific transient
exceptions separately (e.g., database/IO timeouts) and apply a retry strategy
(use your existing retry mechanism or Spring Retry/@Retryable or a
RetryTemplate) for those cases, (2) record failures to your monitoring system
(e.g., increment a Micrometer counter or push a custom metric/event) inside the
catch so on-call/alerts can trigger, and (3) for non-recoverable exceptions, log
full context and rethrow or propagate an application-specific runtime exception
so the scheduler framework or supervisor can detect persistent failure;
implement these changes while preserving the existing info log on success.
| String createRefreshToken(); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Refresh token 생성 메서드가 추가되었습니다. 전체 보안 플로우를 검증하세요.
메서드 시그니처는 명확하나, refresh token의 보안 요구사항을 충족하는지 다음 사항들을 확인해주세요:
- Refresh token의 저장 방식 (암호화 여부, 해싱)
- Token rotation 구현 (재사용 방지)
- 만료 시간 관리 및 자동 정리 메커니즘
- 탈취 시나리오 대응 (같은 refresh token의 중복 사용 감지)
다음 스크립트로 refresh token 관련 구현을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Refresh token 저장 및 관리 로직 확인
# RefreshToken 엔티티 구조 확인
echo "=== RefreshToken 엔티티 ==="
fd -e java RefreshToken.java --exec cat {}
echo -e "\n=== TokenProviderImpl 구현 ==="
fd -e java TokenProviderImpl.java --exec cat {}
echo -e "\n=== UserWriteService의 refresh 메서드 ==="
ast-grep --pattern $'class UserWriteService {
$$$
refresh($$$) {
$$$
}
$$$
}'Length of output: 3214
추가 정보를 수집하겠습니다.
🏁 Script executed:
#!/bin/bash
# UserWriteService의 refresh 메서드 검색
echo "=== UserWriteService 파일 위치 ==="
fd -e java -path "*UserWriteService*"
echo -e "\n=== UserWriteService의 refresh 메서드 ==="
rg -nP --type=java '\brefresh\s*\(' -C5 | head -100
echo -e "\n=== RefreshToken 저장/사용 로직 ==="
rg -nP --type=java "RefreshToken" -C3 | head -150
echo -e "\n=== 토큰 재사용 감지 로직 검색 ==="
rg -nP --type=java "token.*reuse|reuse.*token|rotation|duplicate" -C2Length of output: 28264
추가 검증이 필요합니다. User 엔티티의 rotate() 메서드 구현을 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# User 엔티티 파일 찾기 및 rotate, saveRefreshToken 메서드 확인
echo "=== User 엔티티 파일 ==="
fd -type f -name "User.java" -path "*/domain/entity/*"
echo -e "\n=== User 엔티티 rotate 및 saveRefreshToken 메서드 ==="
rg -nA20 "public.*rotate|public.*saveRefreshToken" -C2 --type-list
echo -e "\n=== RefreshToken 저장소 쿼리 ==="
rg -nA10 "findByRefreshToken|deleteExpiredRefreshTokens" -C2
echo -e "\n=== User 엔티티 전체 ==="
find . -type f -name "User.java" -path "*/domain/entity/*" -exec cat {} \;Length of output: 14092
Refresh token 구현에서 여러 보안 이슈 발견 - 즉시 수정 필요
검증 결과, 다음 문제점들을 확인했습니다:
-
평문 저장 (심각):
RefreshToken엔티티에서 토큰이 평문 String으로 저장되고 있습니다. 데이터베이스 침해 시 모든 활성 세션이 즉시 노출됩니다. 토큰을 암호화하거나 해시 처리해야 합니다. -
토큰 로테이션 로직 결함 (심각):
User.rotate()메서드에서expiredAt.isAfter(LocalDateTime.now().plusDays(1))조건이 true일 때 기존 토큰을 반환하므로, 새 토큰을 발급하지 않습니다. 이로 인해 만료까지 1일 이상 남은 토큰은 계속 재사용되며, 탈취된 토큰의 유효 기간이 길어집니다. 매 refresh 요청 시 항상 새로운 토큰을 발급해야 합니다. -
토큰 재사용 감지 미흡: 이전 토큰을 단순히 제거하기만 하므로, 동일 토큰의 중복 사용 공격 감지가 명시적으로 구현되지 않았습니다. 탈취된 토큰으로 동시에 여러 refresh 요청이 들어오는 시나리오에 대비한 로직이 필요합니다.
-
만료 관리 및 정리: ✓ 스케줄러에 의한 자동 삭제는 정상 구현되어 있습니다.
There was a problem hiding this comment.
RefreshToken 그냥 같은지 확인만 하는 건데 암호화 해야 합니까?
There was a problem hiding this comment.
@letsgilit 좋은 질문입니다! "단순 비교"라는 관점에서는 그렇게 보일 수 있지만, 이는 비밀번호를 평문 저장하는 것과 동일한 논리입니다. 비밀번호도 "같은지 확인만 하는 것"이지만, 반드시 해싱해서 저장하죠.
보안 관점에서 해싱이 필요한 이유:
데이터베이스가 SQL 인젝션이나 직접 접근으로 침해되면, 평문으로 저장된 모든 refresh token이 노출되어 공격자가 즉시 사용할 수 있습니다. 해싱은 데이터베이스 침해 시 유효한 토큰 유출을 방지하고 문자열 비교 타이밍 공격도 방어합니다.
업계 표준 및 권고사항:
- OAuth 2.0 Threat Model RFC는 "인증 서버는 자격증명을 평문으로 저장해서는 안 되며, 대신 해시를 저장하거나 암호화하는 것이 일반적인 접근방식"이라고 명시합니다.
- 비밀번호처럼 refresh token도 시크릿으로 취급해야 하며, 해시된 버전(예: SHA-256)을 저장하고, 검증 시 들어온 토큰을 해싱하여 저장된 해시와 비교합니다.
구현 방법:
// TokenProviderImpl에서 토큰 생성 시
public String createRefreshToken() {
return UUID.randomUUID().toString();
}
// 저장 전 해싱 (UserWriteService에서)
private String hashToken(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
// 저장 시
RefreshToken refreshToken = user.saveRefreshToken(hashToken(newRefreshToken));
// 검증 시 (refresh 메서드에서)
User user = userRepository.findByRefreshToken(hashToken(command.refreshToken()))
.orElseThrow(() -> new UserException(INVALID_REFRESH_TOKEN));결론:
단순 비교용이라도 해싱은 필수입니다. 데이터베이스 침해는 "만약"이 아닌 "언제"의 문제로 간주해야 하며, 해싱은 방어 계층(defense-in-depth)의 기본입니다. 구현도 복잡하지 않으니 적용을 권장드립니다.
🧠 Learnings used
Learnt from: letsgilit
PR: HI-dle/rouby#230
File: back/src/main/java/com/rouby/batch/config/BatchConfig.java:68-73
Timestamp: 2025-09-09T11:11:40.513Z
Learning: letsgilit prefers simpler configurations over complex ones when the current solution is sufficient for the use case.
| @Transactional | ||
| public TokenInfo refresh(RefreshTokenCommand command) { | ||
| String oldRefreshToken = command.refreshToken(); | ||
| User user = userRepository.findByRefreshToken(oldRefreshToken) | ||
| .orElseThrow(() -> UserException.from(INVALID_REFRESH_TOKEN)); | ||
|
|
||
| if (user.isExpiredRefreshToken(oldRefreshToken)) { | ||
| throw UserException.from(EXPIRED_REFRESH_TOKEN); | ||
| } | ||
|
|
||
| String newAccessToken = tokenProvider.createAccessToken( | ||
| user.getId().toString(), | ||
| user.getRole().toString(), | ||
| user.getEmail() | ||
| ); | ||
|
|
||
| String newRefreshToken = tokenProvider.createRefreshToken(); | ||
| RefreshToken refreshToken = user.rotate(oldRefreshToken, newRefreshToken); | ||
|
|
||
| return new TokenInfo(newAccessToken, refreshToken.getToken()); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
[RECOMMENDED] Refresh 엔드포인트에 Rate Limiting 추가 권장
현재 refresh 엔드포인트에 별도의 rate limiting이 없는 것으로 보입니다. 악의적인 사용자가 토큰을 반복적으로 갱신하여 시스템에 부하를 줄 수 있습니다.
Spring의 Bucket4j 또는 RateLimiter를 사용하여 사용자당/IP당 refresh 요청 빈도를 제한하는 것을 권장합니다.
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/application/service/UserWriteService.java
around lines 229 to 249, add rate limiting for the refresh operation: enforce a
quota (e.g., X requests per minute) per user or per IP before executing the
token rotation to prevent abuse. Implement this by applying a rate-limiter at
the controller or service entry (preferred at controller) using Bucket4j or
Spring RateLimiter backed by an in-memory or distributed cache (Redis) for
clustered deployments; check the limiter using the user id or client IP as the
key, and when the limit is exceeded throw a specific exception (e.g.,
TooManyRequests) or return a 429 response. Ensure the limiter check runs before
looking up/rotating tokens and that the limiter state is updated atomically to
avoid race conditions.
🧩 Analysis chain
[MAJOR] Refresh Token 갱신 시 동시성 문제 검증 필요
동일한 Refresh Token으로 동시에 여러 번 refresh 요청이 들어올 경우, 두 요청 모두 Line 235의 만료 검증을 통과한 후 Line 246의 rotate 메서드를 호출할 수 있습니다. 이는 예상치 못한 동작이나 데이터 불일치를 초래할 수 있습니다.
다음을 검증하세요:
동시성 제어가 없다면 다음 중 하나를 고려하세요:
- User 또는 RefreshToken 엔티티에
@Version추가 (낙관적 락) rotate메서드에서 토큰 존재 여부를 다시 확인하고 없으면 예외 발생- 데이터베이스 수준의 unique constraint 활용
🏁 Script executed:
#!/bin/bash
# User 엔티티의 rotate 메서드가 낙관적 락(@Version) 또는 비관적 락을 사용하는지 확인
ast-grep --pattern $'class User {
$$$
rotate($$$) {
$$$
}
$$$
}'
# RefreshToken 엔티티에 버전 필드가 있는지 확인
ast-grep --pattern $'class RefreshToken {
$$$
@Version
$$$
}'Length of output: 0
정보 수집을 위해 추가 검증을 진행하겠습니다.
이후 파일 위치를 파악한 후, 파일 내용을 검토하여 다음을 확인하겠습니다:
1. User 엔티티의 `@Version` 또는 `@Lock` 어노테이션 존재 여부
2. RefreshToken 엔티티의 버전 제어 메커니즘
3. `rotate()` 메서드의 구현 로직
4. 동시성 제어 장치 유무
---
🏁 Script executed:
```shell
#!/bin/bash
# Find User and RefreshToken entity files
echo "=== Locating User and RefreshToken entity files ==="
fd -t f "User\.java|RefreshToken\.java" --path "*/user/*" back/
# Check if files exist in common paths
find back -name "User.java" -o -name "RefreshToken.java" 2>/dev/null | head -20
Length of output: 419
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine User entity
echo "=== User.java ==="
cat back/src/main/java/com/rouby/user/user/domain/entity/User.java
echo -e "\n=== RefreshToken.java ==="
cat back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.java
Length of output: 9725
동시성 문제 확인됨 - 낙관적/비관적 락 미적용
검증 결과, 원본 리뷰 코멘트의 우려가 타당합니다.
문제점:
User엔티티:@Version또는@Lock어노테이션 없음RefreshToken엔티티:@Version어노테이션 없음rotate()메서드: 락 메커니즘 없이refreshTokensSet 조작- 동시에 같은 토큰으로 요청 시, 두 스레드 모두
isExpiredRefreshToken()통과 →rotate()호출 →findRefreshToken(),remove(),saveRefreshToken()순차 실행 중 경합 발생 가능
추가 이슈:
User.rotate() 메서드(Line 240-250)의 로직도 검토 필요: expiredAt가 현재시간+1일보다 이후면 기존 토큰 반환하는 조건이 rotate의 의도와 맞는지 확인 필요
권장 조치:
User또는RefreshToken엔티티에@Version추가 (낙관적 락)- 또는
rotate()메서드에@Lock(LockModeType.PESSIMISTIC_WRITE)적용 - 또는 데이터베이스 unique constraint 추가
| public void addUser(User user) { | ||
| this.user = user; | ||
| } |
There was a problem hiding this comment.
addUser 메서드의 용도를 명확히 하거나 제거하세요.
이 메서드는 양방향 관계를 깨뜨릴 위험이 있습니다:
User.saveRefreshToken()은 이미 관계를 설정합니다- 외부에서
addUser를 호출하면User.refreshTokens컬렉션과 불일치가 발생할 수 있습니다
다음 중 하나를 선택하세요:
옵션 1: 메서드 제거 (권장)
- public void addUser(User user) {
- this.user = user;
- }옵션 2: 용도가 명확하다면 패키지 전용으로 변경하고 Javadoc 추가
- public void addUser(User user) {
+ /**
+ * JPA 관계 동기화를 위한 내부 메서드. 직접 호출하지 마세요.
+ * User.saveRefreshToken() 사용을 권장합니다.
+ */
+ void addUser(User user) {
this.user = user;
}📝 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.
| public void addUser(User user) { | |
| this.user = user; | |
| } |
| public void addUser(User user) { | |
| this.user = user; | |
| } | |
| /** | |
| * JPA 관계 동기화를 위한 내부 메서드. 직접 호출하지 마세요. | |
| * User.saveRefreshToken() 사용을 권장합니다. | |
| */ | |
| void addUser(User user) { | |
| this.user = user; | |
| } |
🤖 Prompt for AI Agents
In back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.java around
lines 54 to 56, the addUser(User) setter risks breaking the bidirectional
consistency because User.saveRefreshToken() already manages both sides; either
remove this public addUser method (recommended) or make it package-private and
add a Javadoc explaining it is only for internal persistence use and must not be
called externally, ensuring all callers use User.saveRefreshToken() to maintain
the relationship consistency.
| public RefreshToken saveRefreshToken(String token) { | ||
| RefreshToken refreshToken = RefreshToken.create(this, token); | ||
| this.refreshTokens.add(refreshToken); | ||
| return refreshToken; | ||
| } |
There was a problem hiding this comment.
토큰 유효성 검증 추가가 필요합니다.
saveRefreshToken 메서드가 토큰 파라미터의 null 체크나 빈 문자열 검증을 하지 않습니다. 잘못된 토큰이 저장될 위험이 있습니다.
다음과 같이 검증 로직을 추가하세요:
public RefreshToken saveRefreshToken(String token) {
+ if (token == null || token.isBlank()) {
+ throw new IllegalArgumentException("리프레시 토큰은 필수입니다.");
+ }
RefreshToken refreshToken = RefreshToken.create(this, token);
this.refreshTokens.add(refreshToken);
return refreshToken;
}🤖 Prompt for AI Agents
In back/src/main/java/com/rouby/user/user/domain/entity/User.java around lines
238 to 242, add validation for the token parameter: check for null or blank
(e.g., token == null || token.trim().isEmpty()) and throw an appropriate
unchecked exception (IllegalArgumentException) before creating or adding a
RefreshToken; do not modify the entity state if validation fails. Optionally
trim the token before passing it to RefreshToken.create to avoid storing
surrounding whitespace.
| public RefreshToken rotate(String oldRefreshToken, String newRefreshToken) { | ||
| RefreshToken existingToken = findRefreshToken(oldRefreshToken); | ||
|
|
||
| if (existingToken.getExpiredAt().isAfter(LocalDateTime.now().plusDays(1))) { | ||
| return existingToken; | ||
| } | ||
|
|
||
| this.refreshTokens.remove(existingToken); | ||
| return saveRefreshToken(newRefreshToken); | ||
| } |
There was a problem hiding this comment.
토큰 로테이션 로직에 치명적인 오류가 있습니다.
Line 247의 조건문이 반대로 작동합니다. 현재 로직은 토큰이 1일 이상 남았을 때 로테이션을 하지 않고, 만료 임박 시 로테이션합니다. 이는 의도와 반대입니다.
일반적인 토큰 로테이션 전략:
- 토큰이 충분히 남았으면 → 로테이션 수행
- 토큰이 곧 만료되면 → 기존 토큰 유지 또는 에러
로직을 수정하세요:
public RefreshToken rotate(String oldRefreshToken, String newRefreshToken) {
RefreshToken existingToken = findRefreshToken(oldRefreshToken);
- if (existingToken.getExpiredAt().isAfter(LocalDateTime.now().plusDays(1))) {
+ if (existingToken.getExpiredAt().isBefore(LocalDateTime.now().plusDays(1))) {
return existingToken;
}
this.refreshTokens.remove(existingToken);
return saveRefreshToken(newRefreshToken);
}또는 의도가 "만료 임박한 토큰만 로테이션"이라면, 조건문의 의미를 명확히 하는 주석이나 메서드명 변경이 필요합니다.
📝 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.
| public RefreshToken rotate(String oldRefreshToken, String newRefreshToken) { | |
| RefreshToken existingToken = findRefreshToken(oldRefreshToken); | |
| if (existingToken.getExpiredAt().isAfter(LocalDateTime.now().plusDays(1))) { | |
| return existingToken; | |
| } | |
| this.refreshTokens.remove(existingToken); | |
| return saveRefreshToken(newRefreshToken); | |
| } | |
| public RefreshToken rotate(String oldRefreshToken, String newRefreshToken) { | |
| RefreshToken existingToken = findRefreshToken(oldRefreshToken); | |
| if (existingToken.getExpiredAt().isBefore(LocalDateTime.now().plusDays(1))) { | |
| return existingToken; | |
| } | |
| this.refreshTokens.remove(existingToken); | |
| return saveRefreshToken(newRefreshToken); | |
| } |
🤖 Prompt for AI Agents
In back/src/main/java/com/rouby/user/user/domain/entity/User.java around lines
244 to 253 the expiration check is inverted: it currently returns the existing
token when expires after now+1 day (so it skips rotation for tokens with plenty
of life). Change the logic to return the existing token only when it is expiring
soon — i.e., if
existingToken.getExpiredAt().isBefore(LocalDateTime.now().plusDays(1)) return
existingToken; otherwise remove the existing token and call
saveRefreshToken(newRefreshToken) to perform rotation; alternatively swap the
branches (or invert the boolean) so tokens with >1 day left are rotated and add
a short comment clarifying the intended strategy if needed.
| Optional<User> findByRefreshToken(String token); | ||
|
|
||
| long deleteExpiredRefreshTokens(LocalDateTime cutoffTime); |
There was a problem hiding this comment.
🧩 Analysis chain
Refresh token 조회 및 정리 메서드가 추가되었습니다. 성능 최적화를 확인하세요.
메서드 시그니처는 적절하나, 다음 사항들을 확인해주세요:
- 인덱스:
RefreshToken테이블 또는 관련 컬럼에 인덱스가 있는지 확인 (refresh token으로 조회 시 성능) - 스케줄러:
deleteExpiredRefreshTokens가 주기적으로 실행되는 스케줄러가 구현되어 있는지 확인 - 배치 크기: 대량의 만료된 토큰 삭제 시 트랜잭션 크기 및 lock 시간 고려
다음 스크립트로 관련 구현을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Refresh token 관련 데이터베이스 구조 및 스케줄러 확인
echo "=== RefreshToken 엔티티 및 인덱스 ==="
ast-grep --pattern $'@Entity
class RefreshToken {
$$$
}'
echo -e "\n=== 스케줄러 구현 확인 ==="
rg -n "@Scheduled|@EnableScheduling" --type java
echo -e "\n=== deleteExpiredRefreshTokens 구현 ==="
ast-grep --pattern $'deleteExpiredRefreshTokens($$$) {
$$$
}'Length of output: 4451
추가 정보가 필요합니다. 다음을 확인하겠습니다:
🏁 Script executed:
#!/bin/bash
echo "=== RefreshToken 엔티티 전체 내용 ==="
cat back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.java
echo -e "\n=== UserScheduler 내용 ==="
cat back/src/main/java/com/rouby/user/user/application/scheduler/UserScheduler.java
echo -e "\n=== UserJpaRepositoryCustom 구현체 확인 ==="
find . -name "UserJpaRepositoryCustomImpl.java" -type f | head -1 | xargs cat
echo -e "\n=== @EnableScheduling 확인 ==="
rg -n "@EnableScheduling" --type java
echo -e "\n=== @Index 어노테이션 확인 (refresh_token 관련) ==="
rg -n "@Index|@Column.*unique" back/src/main/java/com/rouby/user/user/domain/entity/RefreshToken.javaLength of output: 4244
@EnableScheduling 위치를 확인하겠습니다:
🏁 Script executed:
#!/bin/bash
rg -n "@EnableScheduling" --type java -B5 -A5Length of output: 0
🏁 Script executed:
#!/bin/bash
find . -name "*Config*.java" -o -name "*Application*.java" | head -20 | xargs grep -l "EnableScheduling\|@Bean.*Scheduler" 2>/dev/null || echo "설정 파일에서 EnableScheduling 발견 안됨"Length of output: 84
🏁 Script executed:
#!/bin/bash
cat ./back/src/main/java/com/rouby/batch/notification/config/DispatcherThreadConfig.javaLength of output: 1827
Refresh token 관련 메서드 구현 시 해결해야 할 필수 문제 3가지
메서드 시그니처는 적절하나, 다음과 같은 중대한 결함이 있습니다:
-
@EnableScheduling 누락: 전체 프로젝트에
@EnableScheduling이 선언되어 있지 않습니다.UserScheduler의@Scheduled어노테이션이 작동하지 않으므로 스케줄러가 실행되지 않습니다.- 해결: 메인 애플리케이션 클래스 또는 설정 클래스에
@EnableScheduling추가
- 해결: 메인 애플리케이션 클래스 또는 설정 클래스에
-
UserScheduler.java의 cutoffTime 버그 (라인 18):
private static final LocalDateTime cutoffTime = LocalDateTime.now();
클래스 로딩 시점에만 초기화되어 매번 같은 시간값을 사용합니다. 매일 다른 시간이 기준이 되어야 합니다.
- 해결: 메서드 내에서
LocalDateTime cutoffTime = LocalDateTime.now();선언
- 해결: 메서드 내에서
-
RefreshToken.java의 인덱스 누락:
token필드와expiredAt필드에 인덱스가 없어findByRefreshToken()조회와deleteExpiredRefreshTokens()삭제 시 전체 테이블 스캔이 발생합니다.- 해결: 필드에
@Index어노테이션 추가:
@Index(name = "idx_refresh_token", columnList = "token") private String token; @Index(name = "idx_refresh_token_expired", columnList = "expiredAt") private LocalDateTime expiredAt;
- 해결: 필드에
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/infrastructure/persistence/jpa/UserJpaRepositoryCustom.java
around lines 13 to 15: implement three fixes for refresh-token handling—(1)
enable scheduling project-wide by adding @EnableScheduling to the main
application class or a configuration class so UserScheduler's @Scheduled methods
run; (2) fix the cutoffTime bug in UserScheduler by computing
LocalDateTime.now() inside the scheduled method (not as a static class field) so
each run uses the current time; (3) add database indexes for RefreshToken.token
and RefreshToken.expiredAt (via @Index on the entity/table mapping) to avoid
full table scans for findByRefreshToken and deleteExpiredRefreshTokens and
ensure corresponding DB migrations are applied.
| public record LoginResponse( | ||
| String accessToken, | ||
| String refreshToken | ||
| ) { | ||
|
|
||
| public static LoginResponse from(LoginInfo info){ | ||
| return LoginResponse.builder() | ||
| .token(info.accessToken()) | ||
| .accessToken(info.accessToken()) | ||
| .refreshToken(info.refreshToken()) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Breaking Change: 로그인 응답 구조가 변경되었습니다. 클라이언트 코드를 확인하세요.
기존 token 필드가 accessToken과 refreshToken으로 분리되었습니다. 이는 breaking change이므로 다음을 확인해주세요:
- 프론트엔드에서 로그인 응답 파싱 로직 업데이트 완료
- API 문서 업데이트 완료
- 모바일 클라이언트 등 다른 클라이언트가 있다면 해당 코드 업데이트 필요
다음 스크립트로 클라이언트 코드에서 로그인 응답 처리 부분을 확인하세요:
🏁 Script executed:
#!/bin/bash
# 프론트엔드에서 로그인 응답 처리 확인
echo "=== 프론트엔드 로그인 관련 코드 ==="
rg -n -C 5 "token.*login|login.*token" --type js --type ts -g '!node_modules' -g '!dist'
echo -e "\n=== API 문서 확인 ==="
fd -e adoc -e md --exec rg -l "login|auth" {}Length of output: 148
추가 검증이 필요합니다. API 문서와 코드베이스를 더 자세히 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# API 문서에서 로그인 응답 구조 확인
echo "=== auth.adoc 문서 내용 ==="
cat ./back/src/docs/asciidoc/auth.adoc | head -100
echo -e "\n=== user.adoc 문서 내용 ==="
cat ./back/src/docs/asciidoc/user.adoc | head -100
echo -e "\n=== 코드베이스에서 'token' 필드 참조 확인 ==="
rg -n "\"token\"|\.token|token:" --type java -C 3 -g '!node_modules' | head -50
echo -e "\n=== LoginResponse 사용 위치 확인 ==="
rg -n "LoginResponse" --type java -C 2 -g '!node_modules'Length of output: 11178
추가 검증을 계속하겠습니다. REST Docs 스니펫과 테스트 코드를 확인해야 합니다.
🏁 Script executed:
#!/bin/bash
# REST Docs 생성 스니펫 찾기
echo "=== REST Docs 스니펫 위치 ==="
find . -path "*/login-auth-200*" -o -path "*/login*response*" 2>/dev/null | head -20
echo -e "\n=== 로그인 관련 테스트 코드 ==="
rg -n "login.*200|LoginResponse|LoginInfo" --type java -A 10 -B 2 | grep -A 10 -B 2 "test\|Test" | head -80
echo -e "\n=== LoginInfo 구조 확인 ==="
fd -n "LoginInfo.java" --exec cat {} \;
echo -e "\n=== 기존 token 필드 참조 (record/class 정의) ==="
rg -n "record.*token|class.*token" --type java -C 5 | grep -i "login\|auth\|response" -A 5 -B 5Length of output: 5331
더 자세한 검증이 필요합니다. 테스트 코드 전체와 git 변경사항을 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# 테스트 파일 전문 확인
echo "=== AuthControllerTest.java 전체 로그인 테스트 ==="
cat back/src/test/java/com/rouby/user/user/presentation/AuthControllerTest.java | grep -A 30 "@Test" | grep -A 30 "login"
echo -e "\n=== REST Docs 응답 필드 설정 확인 ==="
rg -n "responseFields|response-fields" --type java -A 5
echo -e "\n=== 현재 PR에서 LoginResponse 변경 사항 ==="
git log --oneline -20 back/src/main/java/com/rouby/user/user/presentation/dto/response/LoginResponse.java 2>/dev/null || echo "Git not available"
echo -e "\n=== LoginResponse 파일 전체 내용 ==="
cat back/src/main/java/com/rouby/user/user/presentation/dto/response/LoginResponse.java
echo -e "\n=== 최근 커밋에서 LoginResponse 변경 ==="
git diff HEAD~1 back/src/main/java/com/rouby/user/user/presentation/dto/response/LoginResponse.java 2>/dev/null || echo "Git diff not available"Length of output: 15289
로그인 응답 구조 변경은 검증되었습니다. 추가로 REST Docs 응답 필드 문서화가 필요합니다.
Git diff에서 확인: LoginResponse가 기존 String token에서 String accessToken, String refreshToken으로 변경되었습니다. 이는 breaking change입니다.
다만 AuthControllerTest.java의 로그인 테스트(lines 45-70)에서 responseFields 정의가 누락되어 있습니다. REST Docs가 새로운 응답 구조를 문서화할 수 없습니다.
필수 조치:
- AuthControllerTest의 login_test() 메서드에
responseFields추가:fieldWithPath("accessToken")fieldWithPath("refreshToken")
클라이언트 코드 업데이트(프론트엔드, 모바일 등)를 진행하기 전에 API 문서가 정확하게 생성되어야 합니다.
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/user/user/presentation/dto/response/LoginResponse.java
around lines 12 to 22 the response shape changed from a single token to
accessToken and refreshToken but the test documentation was not updated; update
AuthControllerTest.java (login_test() method, lines ~45-70) to add
responseFields for the new structure: add fieldWithPath("accessToken") and
fieldWithPath("refreshToken") to the REST Docs responseFields snippet (and
ensure the necessary static imports for fieldWithPath/responseFields are
present) so the generated API docs reflect the new response DTO.
# Conflicts: # back/src/main/java/com/rouby/user/user/application/service/UserReadService.java # back/src/main/java/com/rouby/user/user/application/usecase/UserUsecase.java # back/src/main/java/com/rouby/user/user/domain/entity/User.java # back/src/test/java/com/rouby/user/user/presentation/AuthControllerTest.java
#️⃣연관된 이슈
변경 타입
변경 내용
to-be(변경 후 설명을 여기에 작성)
체크리스트
코멘트
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
문서화