Skip to content

feat: 로그아웃 구현#16

Merged
JoonKyoLee merged 10 commits intomainfrom
feat/logout
Nov 19, 2025
Merged

feat: 로그아웃 구현#16
JoonKyoLee merged 10 commits intomainfrom
feat/logout

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 19, 2025

✨ 작업 내용

  • 로그아웃 로직 구현

📝 적용 범위

  • /user/auth

📌 참고 사항

Summary by CodeRabbit

  • 새로운 기능

    • 로그아웃 엔드포인트가 추가되어 클라이언트에서 로그아웃을 호출할 수 있습니다.
    • 로그아웃 시 액세스 토큰 무효화(블랙리스트) 및 리프레시 토큰 삭제(쿠키 제거)가 수행됩니다.
    • 성공 응답 메시지에 로그아웃 성공 항목이 추가되었습니다.
  • 보안

    • 로그아웃 경로에 인증이 필요하도록 접근 제어가 강화되었습니다.
  • 테스트

    • 로그아웃 및 토큰 폐기 관련 단위/통합 테스트가 추가되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Walkthrough

로그아웃 기능이 추가되었습니다. 액세스 토큰 블랙리스트(레디스) 저장·조회 로직과 토큰 폐기 흐름(revoke), 컨트롤러 엔드포인트(DELETE /api/v1/auth/logout), 보안 설정 및 필터의 블랙리스트 검사 통합이 포함됩니다.

Changes

응집군 / 파일(들) 변경 요약
API 상수 레이어
src/main/java/com/almang/inventory/global/api/SuccessMessage.java
LOGOUT_SUCCESS("로그아웃 성공") enum 상수 추가
보안 설정
src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java
RedisService 의존성 추가, TokenAuthenticationFilter 빈 생성 시 redisService 전달, /api/v1/auth/logout 경로에 인증 요구 규칙 추가
보안 필터
src/main/java/com/almang/inventory/global/security/jwt/TokenAuthenticationFilter.java
RedisService 주입 및 액세스 토큰 블랙리스트 검사 추가; 블랙리스트면 인증 무효 처리
컨트롤러 및 DTO
src/main/java/com/almang/inventory/user/auth/controller/AuthController.java,
src/main/java/com/almang/inventory/user/auth/dto/response/LogoutResponse.java
DELETE /logout 엔드포인트 추가, LogoutResponse 레코드 추가
서비스: 인증/토큰/레디스
src/main/java/com/almang/inventory/user/auth/service/AuthService.java,
src/main/java/com/almang/inventory/user/auth/service/TokenService.java,
src/main/java/com/almang/inventory/user/auth/service/RedisService.java
logout(...)revokeTokens(...) 메서드 추가, 액세스 토큰 블랙리스트 저장/조회 메서드 추가, 리프레시 토큰 삭제 및 쿠키 제거 로직 추가
테스트
src/test/java/.../AuthControllerTest.java,
src/test/java/.../AuthServiceTest.java,
src/test/java/.../RedisServiceTest.java,
src/test/java/.../TokenServiceTest.java,
src/test/java/.../TestSecurityConfig.java
로그아웃 경로·서비스·레디스 블랙리스트·토큰 폐기 흐름에 대한 단위/통합 테스트 추가 및 시큐리티 테스트 설정 업데이트

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Controller as AuthController
    participant AuthSvc as AuthService
    participant TokenSvc as TokenService
    participant Redis as RedisService
    participant Repo as RefreshTokenRepo

    User->>Controller: DELETE /api/v1/auth/logout (Bearer accessToken, Cookie refreshToken)
    activate Controller
    Controller->>AuthSvc: logout(userId, request, response)
    activate AuthSvc
    AuthSvc->>TokenSvc: revokeTokens(request, response, userId)
    activate TokenSvc

    TokenSvc->>TokenSvc: resolveToken(request) -> accessToken
    alt accessToken present & valid
        TokenSvc->>TokenSvc: calc remainingMillis
        alt remainingMillis > 0
            TokenSvc->>Redis: addAccessTokenToBlacklist(accessToken, remainingMillis)
            activate Redis
            Redis-->>Redis: set blacklist key with TTL
            deactivate Redis
        end
    end

    TokenSvc->>Repo: delete refresh token by userId
    TokenSvc->>TokenSvc: clearRefreshTokenCookie(response)
    deactivate TokenSvc

    AuthSvc-->>Controller: LogoutResponse(true)
    deactivate AuthSvc
    Controller-->>User: 200 OK + ApiResponse(LOGOUT_SUCCESS)
    deactivate Controller

    Note over TokenSvc,Redis: 향후 요청 시 TokenAuthenticationFilter가 Redis에서 블랙리스트 조회 후 인증 거부
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

추가로 주의해서 살필 항목:

  • TokenAuthenticationFilter: 블랙리스트 감지 시 SecurityContext 정리 및 에러코드(ACCESS_TOKEN_INVALID) 설정 위치와 HTTP 응답 흐름 일관성 검증.
  • Redis TTL 계산: 남은 밀리초 계산 방식과 음수/제로 처리(현재 no-op)에서 경계 조건(클럭 스큐)을 점검하세요.
  • 트랜잭션 경계: @Transactionallogout 내에서 DB 삭제와 Redis I/O가 섞여있음 — Redis 실패 시 기대하는 롤백/정합성 보장 여부 확인.
  • 쿠키 제거 구현: Set-Cookie 헤더 속성(경로, 도메인, Secure, SameSite)이 발급 시점과 일치하는지 테스트(브라우저 동작 차이 고려).
  • 보안 규칙: 테스트용 SecurityConfig와 실제 SecurityConfig에서 /api/v1/auth/logout 접근 제어가 일치하는지 확인.

Possibly related PRs

"칭찬" (짧고 위트): 잘했어요 — 보안 경로에 마지막 잠금장치를 잘 걸어두셨습니다 🔐

시 🎭

토큰은 흘러가고, Redis는 기억하네
로그아웃 한 번에 안전을 더하고
필터가 지키고 서비스가 정리하니
사용자는 떠나도 걱정 말아요
다시 올 때는 새로 시작이면 되죠 🌤️

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
Title check ✅ Passed PR 제목 '로그아웃 구현'은 이 변경사항의 핵심 기능을 명확하고 간결하게 요약합니다.
Description check ✅ Passed PR 설명은 필수 섹션(작업 내용, 적용 범위, 참고 사항)을 모두 포함하고 있으며 충분히 완성되어 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 연결 이슈 #10의 모든 목표(액세스 토큰 블랙리스트 저장, 로그아웃 구현)를 만족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #10의 로그아웃 기능 구현 범위 내에 있으며, 범위 외 변경은 없습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/logout

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

@JoonKyoLee JoonKyoLee self-assigned this Nov 19, 2025
@JoonKyoLee JoonKyoLee added the enhancement New feature or request label Nov 19, 2025
Copy link

@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: 1

🧹 Nitpick comments (4)
src/test/java/com/almang/inventory/user/auth/service/AuthServiceTest.java (1)

150-161: 로그아웃 테스트 구조가 잘 작성되었습니다.

기존 테스트 패턴을 따르며 핵심 동작을 검증합니다. 다만, 토큰 폐기 중 예외 발생 시나리오도 고려하면 더 견고한 테스트가 될 수 있습니다. 예를 들어, tokenService.revokeTokens()가 예외를 던질 때의 동작을 검증하는 테스트를 추가하는 것이 좋습니다.

src/main/java/com/almang/inventory/user/auth/dto/response/LogoutResponse.java (1)

3-5: 간결한 DTO 구현입니다.

Java record 사용은 적절하지만, success 필드가 항상 true를 반환한다면 HTTP 상태 코드만으로 충분할 수 있습니다. 만약 향후 로그아웃 응답에 추가 정보(예: 로그아웃된 세션 수, 타임스탬프 등)를 포함할 계획이 있다면 현재 구조가 확장에 유리합니다. 그렇지 않다면 ApiResponse<Void> 형태도 고려해볼 만합니다.

참고: 성공/실패를 명시적으로 표현하는 현재 방식도 API 일관성 측면에서 충분히 합리적입니다.

src/main/java/com/almang/inventory/global/security/jwt/TokenAuthenticationFilter.java (1)

73-90: 블랙리스트 검증 로직이 올바른 위치에 배치되었습니다.

토큰 유효성 검증 후, 사용자 조회 전에 블랙리스트를 확인하는 것은 보안 측면에서 적절합니다. 다만, 모든 인증 요청마다 Redis 조회가 발생하므로 성능에 영향을 줄 수 있습니다.

개선 제안:

  • Redis 응답 시간을 모니터링하여 병목이 발생하는지 확인하세요.
  • 향후 트래픽이 증가하면 로컬 캐시 레이어(예: Caffeine Cache)를 추가로 도입하여 최근 검증된 토큰을 짧은 시간(예: 10초) 동안 캐싱하는 방안을 고려할 수 있습니다.

참고: Spring Cache와 Redis를 조합한 다층 캐싱 전략에 대해서는 Spring Cache Abstraction 문서를 참고하세요.

src/main/java/com/almang/inventory/user/auth/service/AuthService.java (1)

53-60: 로그아웃 로직이 명확하게 구현되었습니다.

다만, @Transactional 어노테이션의 필요성을 재검토해보세요. 현재 logout 메서드는:

  1. tokenService.revokeTokens() 호출 (Redis 작업 수행)
  2. 로그 출력 및 응답 반환

고려사항:

  • Redis 작업은 Spring의 트랜잭션 관리 대상이 아닙니다.
  • DB 작업이 없다면 @Transactional은 불필요한 트랜잭션 오버헤드를 발생시킵니다.
  • 향후 DB 작업(예: 로그아웃 이력 저장)을 추가할 계획이 있다면 유지하는 것이 좋습니다.

참고: Spring 트랜잭션 관리에 대해서는 Transaction Management 문서를 참고하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7099eff and 5961379.

📒 Files selected for processing (12)
  • src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1 hunks)
  • src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java (2 hunks)
  • src/main/java/com/almang/inventory/global/security/jwt/TokenAuthenticationFilter.java (3 hunks)
  • src/main/java/com/almang/inventory/user/auth/controller/AuthController.java (3 hunks)
  • src/main/java/com/almang/inventory/user/auth/dto/response/LogoutResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/user/auth/service/AuthService.java (2 hunks)
  • src/main/java/com/almang/inventory/user/auth/service/RedisService.java (3 hunks)
  • src/main/java/com/almang/inventory/user/auth/service/TokenService.java (1 hunks)
  • src/test/java/com/almang/inventory/user/auth/controller/AuthControllerTest.java (3 hunks)
  • src/test/java/com/almang/inventory/user/auth/service/AuthServiceTest.java (3 hunks)
  • src/test/java/com/almang/inventory/user/auth/service/RedisServiceTest.java (2 hunks)
  • src/test/java/com/almang/inventory/user/auth/service/TokenServiceTest.java (1 hunks)
🔇 Additional comments (11)
src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1)

13-13: 깔끔한 메시지 추가입니다! 👍

기존 패턴과 일관성 있게 로그아웃 성공 메시지가 추가되었습니다.

src/test/java/com/almang/inventory/user/auth/service/RedisServiceTest.java (1)

122-186: 블랙리스트 기능에 대한 테스트 커버리지가 우수합니다! 🎯

새로 추가된 Redis 작업들에 대한 테스트가 포괄적으로 작성되었습니다:

  • 정상 케이스와 엣지 케이스(TTL ≤ 0) 모두 커버
  • verifyNoInteractions 사용으로 불필요한 호출이 없음을 명시적으로 검증
  • 블랙리스트 존재 여부에 대한 true/false 케이스 모두 검증

테스트 코드의 가독성과 완결성이 뛰어납니다.

src/test/java/com/almang/inventory/user/auth/controller/AuthControllerTest.java (1)

220-260: 로그아웃 엔드포인트 테스트가 완벽합니다! ✨

성공 시나리오와 예외 처리 시나리오 모두를 검증하며, 기존 테스트 패턴과의 일관성도 유지하고 있습니다. DELETE 메서드 사용, 인증 컨텍스트 설정, 응답 구조 검증이 모두 적절합니다.

src/main/java/com/almang/inventory/user/auth/service/TokenService.java (1)

103-113: 쿠키 삭제 로직이 정확합니다.

Max-Age=0 설정으로 쿠키를 삭제하며, setRefreshTokenCookie와 동일한 속성(httpOnly, secure, sameSite, path)을 사용하여 일관성을 유지합니다. 이는 브라우저가 올바른 쿠키를 식별하고 삭제할 수 있도록 보장합니다.

참고: HTTP 쿠키 속성에 대한 자세한 내용은 MDN Cookie 문서를 참고하세요.

src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java (1)

30-30: 깔끔한 의존성 주입입니다! 👍

RedisService를 TokenAuthenticationFilter에 전달하여 블랙리스트 검증을 가능하게 하는 구조가 명확합니다. 생성자 주입 패턴을 일관되게 사용하고 있어 좋습니다.

Also applies to: 72-72

src/test/java/com/almang/inventory/user/auth/service/TokenServiceTest.java (3)

149-182: 탁월한 테스트 커버리지입니다! 🎯

토큰 폐기 로직의 모든 주요 동작을 검증하고 있습니다:

  • 액세스 토큰 블랙리스트 등록 (TTL 포함)
  • 리프레시 토큰 삭제
  • 쿠키 속성 검증 (Max-Age=0, HttpOnly, Secure, SameSite)

ArgumentCaptor를 활용한 Set-Cookie 헤더 검증도 정확합니다.


184-203: 중요한 엣지 케이스를 잘 포착했습니다! 🛡️

Authorization 헤더가 없는 경우에도 리프레시 토큰 삭제와 쿠키 클리어가 정상 동작하는지 확인하는 것은 매우 중요합니다. 이를 통해 로그아웃이 부분적으로만 실패하는 상황을 방지할 수 있습니다.


205-224: 성능 최적화를 검증하는 스마트한 테스트입니다! ⚡

만료된 토큰을 블랙리스트에 추가하지 않는 로직은 불필요한 Redis 연산을 줄이는 좋은 최적화입니다. Mockito.never()를 사용하여 메서드가 호출되지 않음을 명확하게 검증하고 있습니다.

src/main/java/com/almang/inventory/user/auth/controller/AuthController.java (1)

82-95: REST 원칙을 잘 따르는 로그아웃 엔드포인트입니다! 🎯

구현이 깔끔합니다:

  • DELETE 메서드 사용이 RESTful 관행에 부합
  • 인증된 사용자만 로그아웃 가능하도록 보안 처리
  • userId 로깅으로 감사 추적(audit trail) 확보
  • 서비스 레이어로의 적절한 위임

다른 엔드포인트들과 일관된 응답 구조를 유지하고 있어 API 사용성이 좋습니다.

src/main/java/com/almang/inventory/user/auth/service/RedisService.java (2)

15-15: 일관된 네이밍 컨벤션을 유지하고 있습니다! 📝

기존 코드의 패턴을 따라 ACCESS_BLACKLIST_PREFIX 상수와 blacklistAccessTokenKey 헬퍼 메서드를 구현했습니다. 키 생성 로직의 캡슐화가 잘 되어 있어 유지보수성이 높습니다.

Also applies to: 30-32


78-86: 똑똑한 TTL 관리 전략입니다! ⏱️

블랙리스트 구현이 매우 효율적입니다:

  1. 자동 만료: 토큰의 남은 수명만큼만 블랙리스트에 유지하여 Redis 메모리를 효율적으로 사용
  2. 조기 종료: remainingMillis <= 0 체크로 이미 만료된 토큰에 대한 불필요한 Redis 연산 방지
  3. 단순 검증: hasKey를 사용한 간결한 블랙리스트 확인

이 방식은 Spring Security와 Redis를 함께 사용하는 JWT 무효화의 모범 사례입니다.

💡 참고: Spring Data Redis의 hasKey() 메서드는 키가 존재하지 않으면 false를 반환하므로, 블랙리스트에 없는 토큰을 올바르게 처리합니다.

Comment on lines +85 to +101
public void revokeTokens(HttpServletRequest request, HttpServletResponse response, Long userId) {
String accessToken = resolveToken(request);

// 액세스 토큰 무효화
if (accessToken != null && !accessToken.isBlank()) {
long remainMillis = jwtTokenProvider.getRemainingMillis(accessToken);
if (remainMillis > 0) {
redisService.addAccessTokenToBlacklist(accessToken, remainMillis);
}
}

// 리프레시 토큰 삭제
redisService.deleteByUserId(userId.toString());

// 리프레시 쿠키 삭제
clearRefreshTokenCookie(response);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

토큰 폐기 로직이 전반적으로 잘 구현되었습니다.

다만, 액세스 토큰을 추출할 수 없는 경우(Line 86에서 accessToken이 null인 경우)에도 리프레시 토큰은 삭제됩니다. 이는 다음 시나리오를 고려해야 합니다:

잠재적 문제:

  • 사용자가 유효한 액세스 토큰을 보유하고 있지만 요청 헤더에 포함하지 않고 로그아웃을 시도하는 경우
  • 리프레시 토큰은 삭제되지만 액세스 토큰은 블랙리스트에 추가되지 않아 만료 시까지 사용 가능

개선 방안:

  1. 로그아웃 엔드포인트를 인증 필수로 만들어 항상 액세스 토큰이 존재하도록 보장
  2. 액세스 토큰이 없을 경우 예외를 발생시켜 명시적으로 실패 처리

현재 AuthController의 로그아웃 엔드포인트가 인증이 필요한지 확인이 필요합니다.

다음 스크립트로 로그아웃 엔드포인트의 인증 요구 사항을 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: Verify if logout endpoint requires authentication

# Check AuthController logout endpoint and SecurityConfig
echo "=== AuthController logout endpoint ==="
rg -nP -A10 'DELETE.*logout|@DeleteMapping.*logout' --type=java

echo -e "\n=== SecurityConfig authentication requirements ==="
rg -nP -A5 -B5 '/logout|permitAll|authenticated' --type=java -g '*SecurityConfig*'

Length of output: 4190


보안 설정 모순으로 인한 토큰 폐기 로직 실패 위험

검증 결과, AuthController의 logout 엔드포인트는 @AuthenticationPrincipal로 인증을 요구하려 하지만, SecurityConfig.anyRequest().permitAll() (43줄)이 이를 무효화합니다. 결과적으로 인증되지 않은 사용자가 logout을 호출하면 userPrincipal이 null이 되어 88줄에서 NullPointerException 발생하고, 더 심각하게는 해당 사용자의 토큰이 전혀 폐기되지 않습니다.

즉시 조치 필요:

  1. SecurityConfig.java.anyRequest().permitAll().requestMatchers("/logout").authenticated()로 변경하여 logout 엔드포인트를 인증 필수로 강제 (또는 Spring Security의 인증 필터가 401을 반환하도록 설정)
  2. 또는 명시적 에러 핸들링: userPrincipal이 null일 경우 BadCredentialsException 발생시켜 실패 처리

참고: 현재 SecurityConfig의 주석 "추후 변경 예정"과 함께 이 보안 설정은 개발/테스트 환경 전용입니다. 프로덕션 배포 전에 필수로 수정하세요.

🤖 Prompt for AI Agents
In src/main/java/com/almang/inventory/user/auth/service/TokenService.java around
lines 85–101, the revokeTokens flow assumes an authenticated principal and can
NPE if logout is invoked without authentication; fix by enforcing authentication
on the logout endpoint in SecurityConfig (replace .anyRequest().permitAll() with
a rule that requires .requestMatchers("/logout").authenticated() or otherwise
ensure the security filter returns 401 for unauthenticated requests), and/or add
an explicit null check in revokeTokens to throw a BadCredentialsException (or
similar authentication exception) when the user principal is null so the method
fails safely without proceeding to token revocation; implement one or both fixes
before production.

Copy link

@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: 0

🧹 Nitpick comments (1)
src/test/java/com/almang/inventory/store/global/config/TestSecurityConfig.java (1)

17-18: 로그아웃 엔드포인트 인증 강제 설정, 방향 좋습니다 👍

/api/v1/auth/logout 에 대해 authenticated() 를 먼저 두고, 그 뒤에 anyRequest().permitAll() 을 배치한 구조가 Spring Security 매처 우선순위(먼저 매칭되는 룰이 적용)에 잘 맞습니다. 실제 보안 설정과 “로그아웃은 반드시 인증된 사용자만 호출”이라는 정책을 테스트 환경에서도 일관되게 가져가고 있어서 괜찮습니다.

다만 개선 여지는 하나 있습니다:

  • 현재 TestSecurityConfig 에서는 로그아웃만 인증을 요구하고 나머지는 permitAll() 이라, 실제 SecurityConfig 와 전체 보안 정책이 다를 가능성이 큽니다. 이 PR 목적(로그아웃 로직 검증)에는 충분하지만, 장기적으로는
    • 테스트에서 실제 SecurityConfig 를 그대로 import 하거나,
    • 공용 보안 설정을 별도 설정 클래스로 추출한 뒤 prod/test 양쪽에서 재사용
      하는 식으로 “테스트 환경의 보안 룰과 운영 환경의 룰을 최대한 동일하게 유지”하는 것이 좋습니다. 이렇게 해야 나중에 다른 인증/인가 관련 버그가 테스트에서 더 잘 잡힙니다. 관련 개념은 Spring Security 공식 문서의 authorizeHttpRequests 및 Testing 섹션을 참고하면 좋습니다.

테스트에서 실제 SecurityConfig 또는 공용 보안 설정을 재사용하기 어렵지는 않은지 한 번 검토해 주세요. 필요하다면 해당 구조 리팩터링 방향도 함께 논의해 볼 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5961379 and f28ee5b.

📒 Files selected for processing (2)
  • src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java (3 hunks)
  • src/test/java/com/almang/inventory/store/global/config/TestSecurityConfig.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java

@JoonKyoLee JoonKyoLee merged commit 4613686 into main Nov 19, 2025
1 check passed
@JoonKyoLee JoonKyoLee deleted the feat/logout branch November 19, 2025 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 로그아웃 로직

1 participant