Refresh Token 관리: Redis와 Server Memory 비교

이성민·2025년 5월 5일
post-thumbnail

Refresh Token을 서버 메모리에 저장했을 때의 한계

제가 개발 중인 서비스 IDam은 학생과 기업을 매칭하는 플랫폼입니다.
JWT 기반 인증 방식을 사용하며, Refresh Token을 서버 내 메모리(ConcurrentHashMap)에 저장하는 방식으로 관리하고 있었습니다.

@Component
public class RefreshTokenStore {
    private final Map<Long, String> refreshTokenStore = new ConcurrentHashMap<>();

    public void save(Long userId, String refreshToken) {
        refreshTokenStore.put(userId, refreshToken);
    }

    public String get(Long userId) {
        return refreshTokenStore.get(userId);
    }

    public void delete(Long userId) {
        refreshTokenStore.remove(userId);
    }
}

이 방식은 간단하고 잘 동작했지만, 다음과 같은 문제점이 있었습니다.

  1. 서버를 재시작하면 저장된 Refresh Token이 모두 사라진다.
  2. 서버를 여러 대로 늘리면 각 서버가 서로 다른 메모리를 가지므로 token 공유가 불가능하다.
  3. token의 만료 관리를 직접 로직으로 작성해야 한다.

→ 서버 메모리 방식은 단기적으론 괜찮지만, 운영 환경에선 안정성과 확장성에 문제가 생길 수 있다는 한계를 느꼈습니다.


Redis 장점

Redis는 메모리 기반 Key-Value 저장소로, 다음과 같은 장점이 있습니다.

  1. 서버 재시작해도 데이터 유지 가능
  2. TTL 기능으로 token의 만료 시간 자동 관리
  3. 서버 여러 대가 하나의 Redis에 접근 → token 공유 가능
  4. 별도의 데이터베이스 schema 없이 단순 key-value 저장 가능
  5. 애플리케이션 메모리와 분리 → JVM 메모리 영향 없음

Redis 선택 이유

제가 생각했던 저의 서비스에서 Refresh Token 관리에 요구되는 조건은 다음과 같습니다.

  • 여러 디바이스에서 로그인 가능 (PC, 모바일, 태블릿 등)
  • 사용자 1명당 1개의 Refresh Token만 유지 (새 로그인 시 기존 토큰은 삭제/덮어쓰기)
  • 로그아웃 시 Refresh Token 삭제
  • Refresh Token은 TTL(만료시간)으로 자동 삭제
  • 서버 재시작 시에도 Refresh Token이 유지
  • 향후 서버를 여러 대로 확장해도 token 공유가 가능

→ 이 요구사항을 만족하는 저장소는 Redis밖에 없다고 생각하여 Refresh Token 저장소로 도입하였습니다.
→ 또한, 제 서비스는 서버가 1대지만, 미래 확장을 고려해 처음부터 Redis로 관리하자는 판단을 내렸습니다.


서버 메모리 vs Redis 코드 비교

제가 Redis로 변경하면서 작성한 코드는 아래와 같습니다.

@Component
public class RefreshTokenStore {

    private final RedisTemplate<String, String> redisTemplate;
    private final long refreshTokenValidity = 7 * 24 * 60 * 60; // 7일 (초 단위)

    public RefreshTokenStore(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 저장 (디바이스별 저장, TTL 설정)
    public void save(Long userId, String deviceId, String refreshToken) {
        String key = buildKey(userId, deviceId);
        redisTemplate.opsForValue().set(key, refreshToken, refreshTokenValidity, TimeUnit.SECONDS);
    }

    // 조회
    public String get(Long userId, String deviceId) {
        String key = buildKey(userId, deviceId);
        return redisTemplate.opsForValue().get(key);
    }

    // 삭제
    public void delete(Long userId, String deviceId) {
        String key = buildKey(userId, deviceId);
        redisTemplate.delete(key);
    }

    // key 생성
    private String buildKey(Long userId, String deviceId) {
        return "refreshToken:" + userId + ":" + deviceId;
    }
}

핵심 변경점:

  • Map<Long, String>Redis opsForValue()
  • userId를 key로 사용 (기존 토큰 덮어쓰기 가능)
  • TTL 지정 → 만료 시 자동 삭제

결론

제 서비스(IDam)는 Refresh Token 관리에 Redis를 도입함으로써:

  • 로그아웃 안 했는데도 로그인 풀리는 문제 해결
  • 향후 서버 확장 대비
  • 자동 만료 관리 → 코드 간결화
  • 애플리케이션 메모리 보호

이 모든 장점을 가져올 수 있었습니다.

Redis는 단순 캐시가 아니라, 토큰 관리에 최적화된 저장소였습니다.

지금은 서버 1대지만,
“처음부터 Redis로 관리” → 미래 확장에 대비하고, 운영 안정성을 확보한 최고의 선택이었습니다.


+ Spring Boot에서 Redis 직접 연결

Spring Boot는 application.propertiesspring.redis.host 설정 등을 기반으로
자동으로 RedisConnectionFactory (Redis 연결 팩토리)를 생성합니다.

하지만 Docker 환경에서는

  • 컨테이너 이름(redis) 인식 문제
  • 네트워크 설정 문제
  • 자동 연결 객체 생성 타이밍 문제
    같은 이유로 자동 생성된 RedisConnectionFactory로 연결 실패가 발생할 수 있습니다.

그래서 저는 직접 RedisConnectionFactory@Bean으로 등록해서 Redis 연결 객체를 명시적으로 만들어줬습니다.
이렇게 하면 Spring이 RedisTemplate을 생성할 때 이 연결 객체를 사용합니다.

RedisConnectionFactoryRedis 연결을 생성하고 관리하는 인터페이스이며
LettuceConnectionFactory는 Spring에서 Redis와 연결할 때 사용하는 연결 객체.

  • Lettuce 클라이언트를 기반으로 Redis 서버와 비동기 통신을 처리한다.
  • Docker 환경 등에서 연결 문제 시 직접 Bean으로 등록해 사용!

코드

// RedisConfig.java
@Configuration
public class RedisConfig {

    // application.properties의 spring.redis.host 값을 주입
    @Value("${spring.redis.host}")
    private String redisHost;

    // application.properties의 spring.redis.port 값을 주입
    @Value("${spring.redis.port}")
    private int redisPort;

    // Redis 연결 팩토리 직접 Bean 등록
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
        return new LettuceConnectionFactory(config); // Lettuce 클라이언트를 사용해 연결
    }

    // RedisTemplate에 위의 연결 팩토리 주입
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

이 글은 제가 미래에 “왜 Redis를 Refresh Token 저장소로 선택하며 겪었던 시행착오”를 잊지 않기 위한 기록이며, 비슷한 상황에 처한 개발자들에게 하나의 선택지를 보여주고 싶어서 작성했습니다.

profile
BE 개발자

0개의 댓글