1. 왜 Redis를 도입했는가?
JWT 로그아웃 기능을 구현하기 위해 Redis를 도입하게 됐다.
JWT는 기본적으로 서버 상태를 저장하지 않는 Stateless 방식이기 때문에, 발급된 토큰을 서버에서 직접 "무효화"하는 것이 어렵다. 이로 인해 사용자가 로그아웃을 해도 기존 토큰은 만료되기 전까지 유효하게 동작한다.
이를 해결하기 위해 로그아웃 시 사용한 JWT를 Redis에 저장하고,
이후 요청마다 해당 토큰이 Redis에 존재하는지를 확인하는 방식으로 블랙리스트를 관리했다.
* 블랙리스트 관리에 Redis 를 사용한 이유
- In-memory 기반으로 빠르게 블랙리스트 조회 가능
- TTL(Time To Live) 기능을 활용해 JWT 만료 시점과 동일하게 자동 제거 가능
- 간단한 Key-Value 저장 구조로 토큰 저장/조회에 적합
2. Redis 환경 구축
팀원들과 동일한 Redis 환경을 공유하고 초기 설정 부담을 줄이기 위해
Redis Cloud 의 무료 플랜을 사용하였다. (30MB 무료 공간 제공)
https://redis.io/ko/redis-enterprise-cloud/
Redis 클라우드 | Redis
Redis-as-a-Service 를 통하여 가장 빠르고 생생한 실시간 앱을 구축해보세요.
redis.io
데이터가 잘 담기는지 확인하기 위해
Redis Insight 툴도 함께 사용하였다.
Redis Insight
Find, verify, & fix issues faster Browse, filter, and act on your Redis data with full CRUD and batch support for key-value types. Troubleshoot any issues with our slow log inspection, command profiler, database analyzer, and more. Solve your problems wi
redis.io
3. Redis 의존성 추가 및 설정
Gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.yml 설정
spring:
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}
* 설정 값은 환경변수로 관리
RedisConfig 클래스 추가
- 기본 연결 설정만 했다.
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setPassword(password);
return new LettuceConnectionFactory(config);
}
}
4. JwtBlacklistService 구현
@Service
@RequiredArgsConstructor
public class JwtBlacklistService {
private static final String BLACKLIST_PREFIX = "BL:";
private final RedisTemplate<String, String> redisTemplate;
private final JwtUtil jwtUtil;
// 토큰을 블랙리스트에 저장
public void addBlacklist(String token) {
long expirationMillis = jwtUtil.getRemainingExpiration(token);
redisTemplate.opsForValue()
.set(getKey(token), "true", expirationMillis, TimeUnit.MILLISECONDS);
}
// 요청 시 토큰이 블랙리스트인지 확인
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(getKey(token)));
}
private String getKey(String token) {
return BLACKLIST_PREFIX + token;
}
}
* 로직에 대한 간단한 설명 (PR 리뷰로 대체합니다)

5. 사용 예시
로그아웃 시
- 토큰 무료화 적용
@PostMapping("/accounts/logout")
public ResponseEntity<Void> logout(@RequestHeader("Authorization") String bearerToken) {
// 토큰 무효화
invalidateToken(bearerToken);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
private void invalidateToken(String bearerToken) {
String token = jwtUtil.extractToken(bearerToken);
jwtBlacklistService.addBlacklist(token);
}
* Redis DB 에 이런 형식으로 행이 추가된다.

다른 요청 시
- JwtFilter 내 로직에, 블랙리스트 검증 로직 추가
// 블랙리스트 검증
if (jwtBlacklistService.isBlacklisted(jwt)) {
response.sendError(ServletResponseError.EXPIRED_JWT_TOKEN.getHttpStatus(), ServletResponseError.EXPIRED_JWT_TOKEN.getMessage());
return;
}
* 로그아웃 된 토큰으로 api 요청 시, 예외 발생

'Spring > 응용' 카테고리의 다른 글
| Spring 로그 레벨(Log Level) (1) | 2025.06.18 |
|---|---|
| [코드 개선 과제] Interceptor vs AOP, 어떤 방식이 더 적합할까? (0) | 2025.06.11 |