Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

이지은님의 블로그

250416 - Java Spring 도서 앱 구현: Redis를 활용한 Refresh Token 관리(@RedisHash, timeToLive, JpaRepository, CrudRepository) 본문

TIL

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를 이용