Skip to content

[#247][be][user] Refresh Token 추가#259

Open
letsgilit wants to merge 13 commits intodevfrom
feat/#247
Open

[#247][be][user] Refresh Token 추가#259
letsgilit wants to merge 13 commits intodevfrom
feat/#247

Conversation

@letsgilit
Copy link
Copy Markdown
Member

@letsgilit letsgilit commented Oct 27, 2025

#️⃣연관된 이슈

변경 타입

  • 신규 기능 추가/수정
  • 버그 수정
  • 리팩토링
  • 설정
  • 비기능 (주석 등 기능에 영향을 주지 않음)

변경 내용

  • to-be(변경 후 설명을 여기에 작성)

    • Refresh Token Rotation 기능 추가
    • API 테스트 코드 추가
    • TODO

체크리스트

  • 코드가 제대로 동작하는지 확인했습니다.
  • 관련 테스트를 추가했습니다.
  • 문서(코드, 주석, README 등)를 업데이트했습니다.

코멘트

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 토큰 갱신 엔드포인트 추가로 만료된 액세스 토큰을 새로운 토큰으로 교체 가능
    • 로그인 응답에 리프레시 토큰 추가로 토큰 갱신 지원
    • 만료된 리프레시 토큰 자동 정리 기능 추가
  • 버그 수정

    • 만료되거나 유효하지 않은 리프레시 토큰에 대한 오류 처리 개선
  • 문서화

    • API 문서에 리프레시 토큰 플로우 추가

@letsgilit letsgilit self-assigned this Oct 27, 2025
@letsgilit letsgilit added ✨ feature 기능 구현 🛠️ backend 백엔드 labels Oct 27, 2025
@letsgilit letsgilit linked an issue Oct 27, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 27, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

개요

리프레시 토큰 기반 인증 시스템을 구현하는 광범위한 변경사항입니다. 새로운 RefreshToken 엔티티, 토큰 관리 리포지토리 메서드, 토큰 갱신 엔드포인트, 만료 토큰 정리 스케줄러가 추가되었습니다. 인증 로직이 UserReadService에서 UserWriteService로 이동하였으며, 로그인 응답에 리프레시 토큰이 포함되도록 수정되었습니다. 또한 오류 처리, DTO 변환, 토큰 로테이션 메커니즘이 함께 도입되었습니다.

시퀀스 다이어그램

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
Loading

예상 코드 리뷰 소요 시간

🎯 4 (복잡함) | ⏱️ ~50분

추가 주의 사항

  • RefreshToken 엔티티 및 User 관계: 양방향 일대다 관계 설정, 7일 만료 기한, 토큰 로테이션 로직 검증 필요
  • 토큰 갱신 로직 (UserWriteService.refresh): 만료 여부 확인, 토큰 로테이션(1일 유효 기간 체크), 새 토큰 발급 프로세스의 정합성 확인
  • Querydsl 구현 (UserJpaRepositoryCustomImpl): findByRefreshToken() 메서드의 fetch join 및 삭제된 사용자 필터링 로직; deleteExpiredRefreshTokens() 쿼리 성능
  • 스케줄러 (UserScheduler): cutoffTime이 정적으로 초기화되는 부분 검토 - 애플리케이션 시작 시간 기준으로 고정되므로 실제 현재 시간과의 차이 가능성
  • 인증 로직 이동: UserReadService에서 UserWriteService로의 validUser() 이동에 따른 트랜잭션 경계 및 기존 호출 코드 영향 확인
  • LoginInfo 및 LoginResponse 변경: 기존 코드에서 토큰 필드명 변경(단일 token → 이중 accessToken, refreshToken)에 대한 호환성 검토
  • TokenProvider 인터페이스: createRefreshToken() 구현이 UUID 기반인데, 토큰 검증 및 유효성 확인 로직과의 일관성 확인

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[#247][be][user] Refresh Token 추가"는 변경 사항의 핵심 내용을 명확하게 요약하고 있습니다. 제목은 새로운 Refresh Token 기능 추가라는 주요 변경사항을 정확하게 반영하고 있으며, 이슈 번호(#247), 백엔드 표시(be), 모듈(user)을 포함하여 구조적이고 명확합니다. 간결하고 구체적이어서 팀원들이 커밋 히스토리를 스캔할 때 주요 변경사항을 쉽게 이해할 수 있습니다.
Linked Issues Check ✅ Passed 연결된 이슈 #247은 "RefreshToken 추가"라는 제목으로 Refresh Token 기능 추가를 목표로 하고 있으며, PR 설명에서 제시된 구체적 목표들인 "Refresh Token Rotation 기능 추가"와 "API 테스트 코드 추가"가 변경사항에서 모두 구현되어 있습니다. RefreshToken 엔티티, rotation 로직, 새로운 refresh 엔드포인트, 만료된 토큰 정리 스케줄러, 그리고 AuthControllerTest의 refresh_test가 모두 구현되어 있어 명시된 목표들을 충족합니다.
Out of Scope Changes Check ✅ Passed 모든 변경사항이 Refresh Token 기능 구현과 직접적으로 관련되어 있습니다. 새로운 DTO(RefreshTokenCommand, TokenInfo), 도메인 엔티티(RefreshToken), 서비스 메서드들, 저장소 쿼리, 컨트롤러 엔드포인트, 그리고 테스트 코드 모두가 Refresh Token 기능의 필수 요소입니다. UserReadService에서 인증 로직이 UserWriteService로 이동한 것도 Token 관리 구조 개선을 위한 필요한 리팩토링으로 범위 내입니다.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between b3c48b4 and cea4b15.

📒 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: 동시성 안전성 검증 완료: 현재 구조에서는 실제 위험 낮음

  • refreshTokensHashSet (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에서 주기적 삭제 구현

❌ 미구현/부족한 기능:

  1. Rate Limiting: 검색 결과 없음 - 동일 사용자의 refresh 요청에 대한 속도 제한 필요
  2. 상세 보안 로깅: IP 주소, 요청 타임스탬프, 실패 사유 등의 감사 로그 부족
  3. 토큰 재사용 공격 대응: 재사용 시도 시 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로 적절하게 위임하고 있습니다. 삭제된 개수를 반환하여 모니터링에 활용할 수 있습니다.

Comment on lines +3 to +5
public record RefreshTokenCommand(
String refreshToken
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
public record RefreshTokenCommand(
String refreshToken
) {
import jakarta.validation.constraints.NotBlank;
public record RefreshTokenCommand(
@NotBlank(message = "리프레시 토큰은 필수입니다.")
String refreshToken
) {
}
Suggested change
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();
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

[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.

Suggested change
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

아이고 ㅠ

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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.

Comment on lines +18 to +26
@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);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Comment on lines +13 to +14
String createRefreshToken();

Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

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" -C2

Length 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 구현에서 여러 보안 이슈 발견 - 즉시 수정 필요

검증 결과, 다음 문제점들을 확인했습니다:

  1. 평문 저장 (심각): RefreshToken 엔티티에서 토큰이 평문 String으로 저장되고 있습니다. 데이터베이스 침해 시 모든 활성 세션이 즉시 노출됩니다. 토큰을 암호화하거나 해시 처리해야 합니다.

  2. 토큰 로테이션 로직 결함 (심각): User.rotate() 메서드에서 expiredAt.isAfter(LocalDateTime.now().plusDays(1)) 조건이 true일 때 기존 토큰을 반환하므로, 새 토큰을 발급하지 않습니다. 이로 인해 만료까지 1일 이상 남은 토큰은 계속 재사용되며, 탈취된 토큰의 유효 기간이 길어집니다. 매 refresh 요청 시 항상 새로운 토큰을 발급해야 합니다.

  3. 토큰 재사용 감지 미흡: 이전 토큰을 단순히 제거하기만 하므로, 동일 토큰의 중복 사용 공격 감지가 명시적으로 구현되지 않았습니다. 탈취된 토큰으로 동시에 여러 refresh 요청이 들어오는 시나리오에 대비한 로직이 필요합니다.

  4. 만료 관리 및 정리: ✓ 스케줄러에 의한 자동 삭제는 정상 구현되어 있습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

RefreshToken 그냥 같은지 확인만 하는 건데 암호화 해야 합니까?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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.

Comment on lines +229 to +249
@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());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

[MAJOR] Refresh Token 갱신 시 동시성 문제 검증 필요

동일한 Refresh Token으로 동시에 여러 번 refresh 요청이 들어올 경우, 두 요청 모두 Line 235의 만료 검증을 통과한 후 Line 246의 rotate 메서드를 호출할 수 있습니다. 이는 예상치 못한 동작이나 데이터 불일치를 초래할 수 있습니다.

다음을 검증하세요:

동시성 제어가 없다면 다음 중 하나를 고려하세요:

  1. User 또는 RefreshToken 엔티티에 @Version 추가 (낙관적 락)
  2. rotate 메서드에서 토큰 존재 여부를 다시 확인하고 없으면 예외 발생
  3. 데이터베이스 수준의 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() 메서드: 락 메커니즘 없이 refreshTokens Set 조작
  • 동시에 같은 토큰으로 요청 시, 두 스레드 모두 isExpiredRefreshToken() 통과 → rotate() 호출 → findRefreshToken(), remove(), saveRefreshToken() 순차 실행 중 경합 발생 가능

추가 이슈:
User.rotate() 메서드(Line 240-250)의 로직도 검토 필요: expiredAt가 현재시간+1일보다 이후면 기존 토큰 반환하는 조건이 rotate의 의도와 맞는지 확인 필요

권장 조치:

  1. User 또는 RefreshToken 엔티티에 @Version 추가 (낙관적 락)
  2. 또는 rotate() 메서드에 @Lock(LockModeType.PESSIMISTIC_WRITE) 적용
  3. 또는 데이터베이스 unique constraint 추가

Comment on lines +54 to +56
public void addUser(User user) {
this.user = user;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
public void addUser(User user) {
this.user = user;
}
Suggested change
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.

Comment on lines +238 to +242
public RefreshToken saveRefreshToken(String token) {
RefreshToken refreshToken = RefreshToken.create(this, token);
this.refreshTokens.add(refreshToken);
return refreshToken;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

토큰 유효성 검증 추가가 필요합니다.

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.

Comment on lines +244 to +253
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

토큰 로테이션 로직에 치명적인 오류가 있습니다.

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.

Suggested change
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.

Comment on lines +13 to +15
Optional<User> findByRefreshToken(String token);

long deleteExpiredRefreshTokens(LocalDateTime cutoffTime);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Refresh token 조회 및 정리 메서드가 추가되었습니다. 성능 최적화를 확인하세요.

메서드 시그니처는 적절하나, 다음 사항들을 확인해주세요:

  1. 인덱스: RefreshToken 테이블 또는 관련 컬럼에 인덱스가 있는지 확인 (refresh token으로 조회 시 성능)
  2. 스케줄러: deleteExpiredRefreshTokens가 주기적으로 실행되는 스케줄러가 구현되어 있는지 확인
  3. 배치 크기: 대량의 만료된 토큰 삭제 시 트랜잭션 크기 및 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.java

Length of output: 4244


@EnableScheduling 위치를 확인하겠습니다:


🏁 Script executed:

#!/bin/bash
rg -n "@EnableScheduling" --type java -B5 -A5

Length 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.java

Length of output: 1827


Refresh token 관련 메서드 구현 시 해결해야 할 필수 문제 3가지

메서드 시그니처는 적절하나, 다음과 같은 중대한 결함이 있습니다:

  1. @EnableScheduling 누락: 전체 프로젝트에 @EnableScheduling이 선언되어 있지 않습니다. UserScheduler@Scheduled 어노테이션이 작동하지 않으므로 스케줄러가 실행되지 않습니다.

    • 해결: 메인 애플리케이션 클래스 또는 설정 클래스에 @EnableScheduling 추가
  2. UserScheduler.java의 cutoffTime 버그 (라인 18):

    private static final LocalDateTime cutoffTime = LocalDateTime.now();

    클래스 로딩 시점에만 초기화되어 매번 같은 시간값을 사용합니다. 매일 다른 시간이 기준이 되어야 합니다.

    • 해결: 메서드 내에서 LocalDateTime cutoffTime = LocalDateTime.now(); 선언
  3. 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.

Comment on lines +12 to 22
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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Breaking Change: 로그인 응답 구조가 변경되었습니다. 클라이언트 코드를 확인하세요.

기존 token 필드가 accessTokenrefreshToken으로 분리되었습니다. 이는 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 5

Length 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.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🚨🚨🚨Test failed: 코드를 다시 확인해주세요.🚨🚨🚨

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🚨🚨🚨Test failed: 코드를 다시 확인해주세요.🚨🚨🚨

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🚨🚨🚨Test failed: 코드를 다시 확인해주세요.🚨🚨🚨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠️ backend 백엔드 ✨ feature 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[user] RefreshToken 추가

1 participant