이지은님의 블로그
250416 - Java Spring 도서 앱 구현: Redis를 활용한 Refresh Token 관리(@RedisHash, timeToLive, JpaRepository, CrudRepository) 본문
250416 - Java Spring 도서 앱 구현: Redis를 활용한 Refresh Token 관리(@RedisHash, timeToLive, JpaRepository, CrudRepository)
queenriwon3 2025. 4. 16. 17:26▷ 오늘 배운 것
도서 등록, 구매, 예약을 원하는 사용자에 대한 로그인 인증 기능을 담당했다.
로그인 방식으로는 Spring Security 기반의 JWT 토큰 인증 방식을 도입했으며, 이를 통해 stateless한 인증 처리가 가능하도록 설계했다.
이 과정에서, Refresh Token을 DB에 저장할지 Redis에 저장할지에 대해 고민하게 되었고, 각 방식의 장단점을 비교 분석하며 최적의 방식을 선택한 과정에 대해 작성해보고자 한다.
https://flaxen-swan-41e.notion.site/5-Redis-Refresh-Token-1d5b649ebbbd80a58501e2bdd31b95e3
💬 [5분 브리핑] - Redis를 활용한 Refresh Token 관리 | Notion
11조 프로젝트에서 저는 도서 등록, 구매, 예약을 원하는 사용자에 대한 로그인 인증 기능을 담당했습니다.
flaxen-swan-41e.notion.site
<<목차>>
1. 문제 정의 (Why): MySQL 저장 방식의 한계
2. 해결 방법 (How): Redis를 활용한 Refresh Token 관리
3. 구현 내용 (What): 관계형에서 비관계형으로 구조 변화
1) refresh token 엔티티 클래스
2) RefreshTokenRepository 클래스
3) AuthService 클래스 및 TokenService 클래스
4. 결과 & 효과 (Impact)
5. 회고 & 개선 아이디어 (Reflection)
1. 문제 정의 (Why): MySQL 저장 방식의 한계
기존에는 RefreshToken을 MySQL DB에 저장하고 조회했다.

이 방법은 다음과 같은 문제점이 있었다.
- 수동 관리 문제: token_status 컬럼으로 상태 관리 필요
- 성능 저하: 계속 쌓이는 토큰을 조회하면서 DB 부하
- 보안 리스크: 만료 토큰 축적로 인한 보안 취약점
따라서 기존 DB로 Refresh Token을 관리하는 방식의 대안이 필요했다.
2. 해결 방법 (How): Redis를 활용한 Refresh Token 관리
이 문제를 해결하기 위해, Refresh Token 저장소를 DB에서 Redis로 구현하고자 했다. Redis를 사용해서 관리를 하게되면 다음과 같은 효과가 있다.
- Redis는 인메모리 기반으로, DB보다 빠른 응답속도를 제공함으로 DB 부하를 줄이고 성능이 향상 효과
- TTL(Time-To-Live)을 설정할 수 있어, refresh token의 자동 만료 관리
- Redis는 인메모리 기반으로 데이터 휘발성이 높음. Refresh Token은 로그인을 하면 바로 재발급이 가능한, 즉 사라져도 큰 문제가 없는 데이터이므로 영구 저장이 필요하지 않음. 따라서 데이터 성격에 맞는 방법이라고 판단
물론 아래와 같은 단점도 존재했다.
Redis 구현의 단점
- Redis가 DB보다 비용이 높음
- 서버가 여러 개일 경우 Redis 서버간의 동기화가 필요함
👉 Redis 구현의 단점이 Refresh Token 구현에 크게 치명적이지 않다고 판단하여 Refresh Token 데이터의 성격에 따라 DB보다는 Redis에서 구현하는 것으로 의사 결정
3. 구현 내용 (What): 관계형에서 비관계형으로 구조 변화

관계형 데이터베이스의 영속성 기반 구조 → 비관계형 메모리 기반 저장소로의 구조적 전환
기존 Refresh Token의 저장소인 MySQL가 관계형 데이터베이스(RDBMS)인 것과는 다르게 Redis는 Key-Value 형식에 대한 비관계형 저장소로 자료구조를 어떻게 구성해야할지에 대한 생각을 하게 되었다.
또한 Redis로 변경하면서 JPA 및 영속성 컨텍스트를 사용할 수 없음을 인지했다.
Redis의 Key-Value 자료구조로 변환하며 데이터를 어떻게 저장할지 생각해보았다.
- Key: token 값을 쉽게 조회하기 위해 token 저장
- Value: 저장할 토큰은 유저 정보가 포함되지 않은 UUID 랜덤값으로 유저 정보와 함께 저장하는 것이 필요 → userId 값 저장
1) refresh token 엔티티 클래스
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private String token;
@Enumerated(EnumType.STRING)
private TokenState tokenState;
@Builder
public RefreshToken(Long userId) {
this.userId = userId;
this.token = UUID.randomUUID().toString();
this.tokenState = TokenState.VALID;
}
public void updateTokenStatus(TokenState tokenStatus){
this.tokenState = tokenStatus;
}
}
기존의 코드는 tokenState라는 필드가 있어 refresh token의 유효성을 구분하는데 사용했다. (VALID/UNVALID)
하지만 redis에서는 TTL 또는 토큰을 업데이트하는 방법을 사용하여 해당방법이 필요가 없어졌다.
Redis를 이용한 Refresh Token을 사용한 방법은 다음과 같다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@RedisHash(value = "refreshToken", timeToLive = REFRESH_TOKEN_TIME)
public class RefreshToken {
@Id
private String token;
private Long userId;
@Builder
public RefreshToken(Long userId) {
this.userId = userId;
this.token = UUID.randomUUID().toString();
}
public String updateToken() {
this.token = UUID.randomUUID().toString();
return token;
}
}
기존 코드에서 변경사항의 핵심은 JPA를 사용하지 않는다는 것이다.
따라서 @Entity 대신 @RedisHash 어노테이션을 사용하여 이 객체가 Redis에 저장될 수 있도록 지정하였고, timeToLive 속성을 통해 토큰의 만료 시간도 자동으로 설정되도록 구현했다.(7일)
2) RefreshTokenRepository 클래스
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long>{
Optional<RefreshToken> findByToken(@Param("token") String token);
}
기존에는 JpaRepository를 통해 JPA영속성 컨텍스트를 사용하였는데, Redis는 비관계형이므로, 사용할 필요가 없다. 따라서 JpaRepository 대신 CrudRepository를 사용해야한다.
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByToken(String token);
}
JpaRepository과 CrudRepository의 차이점은 다음과 같다.
| 항목 | CrudRepository | JpaRepository |
| 기능 범위 | CRUD 기능 (기본적) | CRUD + JPA 기능 확장 |
| 페이징/정렬 | 직접 구현해야 함 | PagingAndSortingRepository 포함해서 제공 |
| flush(), batch 등 JPA 기능 | ❌ 없음 | ✅ 있음 (flush(), saveAllAndFlush() 등) |
| Entity Graph, Batch 처리 | ❌ 제한적 | ✅ 더 다양한 JPA 기능 지원 |
| 사용 권장 | 단순 CRUD만 필요한 경우 | 대부분의 JPA 기반 프로젝트에서 권장 |
현재 Redis는 CRUD만 필요하므로 CrudRepository를 사용했다.
3) AuthService 클래스 및 TokenService 클래스
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserService userService;
private final TokenService tokenService;
private final PasswordEncoder passwordEncoder;
/* 회원가입 */
@Transactional
public AuthTokensResponse signUp(AuthSignUpRequest request) {
if (!request.password().equals(request.passwordCheck())) {
throw new BadRequestException(PASSWORD_CONFIRMATION_MISMATCH.getMessage());
}
Users users = userService.saveUser(request);
return getTokenResponse(users);
}
/* 로그인 */
@Transactional(readOnly = true)
public AuthTokensResponse signIn(AuthSignInRequest request) {
Users users = userService.findByEmailOrElseThrow(request.email());
if (users.isDeleted()) {
throw new UnauthorizedException(DEACTIVATED_USER_EMAIL.getMessage());
}
if (!passwordEncoder.matches(request.password(), users.getPassword())) {
throw new UnauthorizedException(INVALID_PASSWORD.getMessage());
}
return getTokenResponse(users);
}
/* Access Token, Refresh Token 재발급 */
@Transactional(readOnly = true)
public AuthTokensResponse reissueToken(String refreshToken) {
RefreshToken findRefreshToken = tokenService.findRefreshToken(refreshToken);
Users users = userService.findByIdOrElseThrow(findRefreshToken.getUserId());
String reissuedAccessToken = tokenService.createAccessToken(users);
String reissuedRefreshToken = findRefreshToken.updateToken();
return AuthTokensResponse.of(reissuedAccessToken, reissuedRefreshToken);
}
/* Access Token, Refresh Token 생성 및 저장 */
public AuthTokensResponse getTokenResponse(Users users) {
String accessToken = tokenService.createAccessToken(users);
String refreshToken = tokenService.createRefreshToken(users);
return AuthTokensResponse.of(accessToken, refreshToken);
}
}
@Service
@RequiredArgsConstructor
public class TokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final JwtUtil jwtUtil;
/* Access Token 생성 */
public String createAccessToken(Users users) {
return jwtUtil.createAccessToken(users.getId(), users.getEmail(), users.getNickname(), users.getUserRole());
}
/* Refresh Token 생성 */
public String createRefreshToken(Users users) {
RefreshToken refreshToken = refreshTokenRepository.save(RefreshToken.of(users.getId()));
return refreshToken.getToken();
}
public RefreshToken findRefreshToken(String token) {
return refreshTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundException(REFRESH_TOKEN_NOT_FOUND.getMessage()));
}
}
기존 코드에서 달라진 점은 토큰 상태를 체크하는 것이다. 해당 사항을 생략하고 토큰을 저장 및 업데이트 할 때, refreshTokenRepository.save()를 사용하면 쉽게 업데이트 할 수 있다.
4. 결과 & 효과 (Impact)


구현과정을 거쳐 refresh token을 저장하면 저장된 내용을 다음과 같이 확인할 수 있다.
가장 큰 효과는 TTL을 사용하여 만료된 토큰을 자동 삭제해 Refresh token이 만료기간 이후에 재사용이 불가하게 만들 수 있어 데이터 관리가 용이하다는 것이다.
또한 성능상 이점을 확인하자면..


| 기존 DB 저장 방식 | Redis 저장 방식 | 성능 향상 | |
| 로그인 | 151ms | 135ms | 약 10.6% 향상 |
| 토큰 재발급 | 60ms | 21ms | 약 65% 향상 |
Redis는 DB보다 읽기 및 쓰기가 빠르게 동작하기 때문에 인증과정의 전체적인 성능이 개선된 것을 확인할 수 있다.
5. 회고 & 개선 아이디어 (Reflection)
배운 점
- 단순한 저장소 변경이지만, 데이터 성격에 따라 기술 선택과 의사결정이 필요하다는 것을 느낌. 어떤 데이터를 다루는지에 따라 저장소를 배교 검토하는 경험
- Redis의 TTL 기능을 활용하면 운영 관리 코드를 줄이고 보안적인 측면에서 안정성도 높일 수 있었음.
개선 포인트
- 많은 사용자가 토큰을 생성할 때의 데이터 정합성의 문제는 없는지, 또는 JPA에서 제공하는 이점(@Transactional)을 사용하지 못해 발생하는 문제점은 없는지에 대해 확인하며 Redis 사용
- AWS를 사용하여 아키텍처 구조를 생각해보고 있으므로, AWS가 지원하는 elastic Cache를 이용