RushDeal 프로젝트에서 로그아웃 API를 구현하던 중, 예상보다 흥미로운 고민이 생겼다.
겉보기엔 단순한 기능이지만, 실제로는 "토큰을 검증해야 하는가, 아니면 멱등성을 보장하는 것이 맞는가"
라는 선택의 순간이 있었다.
이 글은 그 과정에서 했던 판단과 선택의 이유를 과정을 공유합니다.
1. 프로젝트 상황 및 초기 목표
Framework: Spring Boot
Language: Java 21
RushDeal 프로젝트의 인증·인가 시스템을 구성하면서, 그 중 한 기능으로 로그아웃 API를 설계하게 되었다.
겉으로 보기엔 단순해 보이는 기능이었지만, 실제 구현 과정에서는 생각보다 많은 고민을 던져준 지점이었다.
1-1. 초기 구현
처음 로그아웃 API는 JWT 발급 구조에 맞춰, 토큰을 검증한 뒤 블랙리스트에 추가하는 방식으로 구현했다.
// Controller
@PostMapping("/logout")
public ResponseEntity<Void> logOut(
@RequestHeader("Authorization") String authorization,
HttpServletRequest request
) {
String accessToken = tokenUtils.extractAccessToken(authorization);
String refreshToken = tokenUtils.extractRefreshToken(request);
LogoutCommand command = new LogoutCommand(accessToken, refreshToken);
authService.logOut(command);
ResponseCookie expiredCookie = tokenUtils.createExpiredRefreshTokenCookie();
return ResponseEntity.noContent()
.header(HttpHeaders.SET_COOKIE, expiredCookie.toString())
.build();
}
// Service
@Transactional
public LogoutResult logout(LogoutCommand command) {
String accessToken = command.accessToken();
String refreshToken = command.refreshToken();
// 🔴 토큰 유효성 검증
accessTokenProvider.validateToken(accessToken);
// Refresh Token 삭제
Optional.ofNullable(refreshToken)
.ifPresent(refreshTokenRepository::deleteByToken);
// Access Token 블랙리스트 추가
LocalDateTime expiryDate = accessTokenProvider.getExpiryDate(
accessToken
);
tokenBlacklistService.blacklistAccessToken(accessToken, expiryDate);
}
얼핏 보면 문제가 없어 보인다.
하지만 실제로는 심각한 결함이 숨어있었다..
2. 이슈
2-1. 문제점
시나리오 1: 만료된 토큰으로 로그아웃
1. 사용자가 30분 전 로그인 (Access Token 만료 시간: 30분)
2. 31분 후, 로그아웃 버튼 클릭
3. accessTokenProvider.validateToken(accessToken) 호출
4. ❌ "유효하지 않은 토큰입니다" 예외 발생
5. 블랙리스트 추가 로직이 실행되지 않음
6. 사용자는 로그아웃 실패로 인식
시나리오 2: 잘못된 형식의 토큰
1. 네트워크 오류 등으로 토큰이 손상됨
2. validateToken() 파싱 실패
3. ❌ 예외 발생
4. 로그아웃 실패
두 경우 모두 결과는 동일했다.
토큰의 상태와 관계없이 성공해야 하는 로그아웃이, 오히려 검증 때문에 실패하고 있었다.
사용자는 로그아웃이 되지 않았다고 느끼게 된다.
2-2. 고민
문제는 결국 이 질문에 닿았다.
로그아웃은 “토큰을 검증해야 하는 작업인가?”
아니면
“ 항상 성공해야 하는 멱등적 작업’인가? ”
| 방식 | 장점 | 단점 |
| 검증한다 | 인증 정책의 일관성, 보안 강도 유지 | 만료·손상 토큰 → 로그아웃 실패 |
| 검증하지 않는다 | 어떤 상태에서도 성공, 멱등성 보장, UX 개선 | 무효 토큰도 처리 대상? 정책 모호성 |
이 시점에서 인증 흐름을 다시 생각해야 했다.
2-3. 인사이트
로그인의 목적을 생각하면 결론은 의외로 단순했다.
목적은 토큰을 “판별”하는 것이 아니라,
더 이상 사용할 수 없도록 “세션을 종료”하는 것이다.
결론
- 이미 만료된 토큰은 설계적으로 아무 의미가 없다
- 손상된 토큰도 API 호출에 사용할 수 없다
- 그렇다면 로그아웃은 언제나 성공해야 하는 작업이다
즉,
로그아웃의 본질은 멱등성(Idempotency) 이다.
3. 해결 방안
문제의 원인은 결국 “로그아웃 과정에서 토큰을 굳이 검증하려 했던 것”이었다.
그래서 개선은 두 단계로 진행했다.
3-1. 1차 개선: 토큰 검증 제거
@Transactional
public LogoutResult logout(LogoutCommand command) {
String accessToken = command.accessToken();
String refreshToken = command.refreshToken();
// ❌ 검증 제거!
// accessTokenProvider.validateToken(accessToken); // 삭제
// Refresh Token 삭제
Optional.ofNullable(refreshToken)
.ifPresent(refreshTokenRepository::deleteByToken);
// 만료 시간을 추출
LocalDateTime expiryDate = accessTokenProvider.getExpiryDate(
accessToken
);
// Access Token 블랙리스트 추가
tokenBlacklistService.blacklistAccessToken(accessToken, expiryDate);
}
하지만 문제는 남아 있었다.
getExpiryDate() 자체가 토큰 파싱을 수행하기 때문에,
만료된 토큰·손상된 토큰은 여전히 예외를 유발했다.
3-2. 2차 개선: 안전한 실패 처리 (try-catch)
try-catch 를 적용해 만료·손상된 토큰은 안전하게 흡수하도록 예외 처리를 추가했다.
@Transactional
public void logOut(String accessToken, String refreshToken) {
// 1. Refresh Token 삭제
Optional.ofNullable(refreshToken).ifPresent(
refreshTokenRepository::deleteByToken
);
// ✅ try catch 추가
// 2. 만료 시간을 추출
LocalDateTime expiryDate;
try {
expiryDate = accessTokenProvider.getExpiryDate(accessToken);
} catch (Exception e) {
// 이미 만료되었거나 깨진 토큰 → 블랙리스트 처리 불필요
return;
}
// 3. 블랙리스트 처리
tokenBlacklistService.blacklistAccessToken(accessToken, expiryDate);
}
3-3. 전체 흐름

4. 결론
로그아웃 API를 설계하면서 느낀 점은,
기능의 성격에 따라 우선해야 할 기준이 달라진다는 것이었다.
처음에는 “인증 관련 API니까 당연히 토큰 검증이 먼저”라고 생각했지만,
막상 로그아웃의 본질을 다시 바라보니 검증보다 사용자가 언제든 성공할 수 있어야 한다는 특성,
즉 멱등성이 더 중요한 경우가 있었다.
이 경험 덕분에, 기능을 구현하기 전에
“이 기능이 실제로 해결해야 하는 문제는 무엇인가?”를 먼저 고민하는 습관이 생겼다.
어떻게 구현할지는 그다음 문제였다.
'스파르타 자바 심화 4기 RushCrew Project' 카테고리의 다른 글
| [의사결정] MSA 설계 시 동기·비동기 통신 선택 가이드 (0) | 2025.12.15 |
|---|---|
| [트러블 슈팅] Java 21 업그레이드 후 Eureka DNS 해석 실패 (1) | 2025.12.02 |
| [기획] 이벤트 스토밍 도입기 (0) | 2025.11.28 |