Spring boot 프로젝트 accounts(3) - Redis

·2024년 3월 24일
0

오늘은 redis에 대해 알아보고 redis를 적용하여 redis를 통한 refreshToken을 관리해보자. 먼저 캐시에 대해 알아보자.

Cache

캐시는 한번 읽은(처리된) 데이터를 임시로 저장하고, 필요에 따라 전송, 갱신, 삭제하는 기술이다. 보통 서버의 메모리를 사용하는 경우가 많은데 매번 Disk로부터 데이터를 조회하는 것보다 훨씬 빠른 I/O 성능을 얻을 수 있다. 하지만 서버가 다운되거나 재부팅되는 경우 데이터가 사라지는 휘발성의 성격을 가지고 있다. 따라서 영속적으로 보관할 수 없는 임시적으로 보관하고 빠르게 해당 정보에 접근하기 위한 용도로 사용되어야 한다.
Redis의 경우, 주기적으로 Disk에 백업해 둘 수 있다.

Redis

redis는 key-value 저장소로 Sql 없이 List, Hash, Set, Sorted Set 등 여러 형식의 자료구조를 지원하는 NoSQL이다. 메모리에 상주하며 DBMS의 캐시 솔루션으로 주로 사용된다.

특징

  • Key-Value Store
    • key-value 쌍으로 저장하는 거대한 Map 형태의 데이터 저장소이다.
    • 데이터를 쉽고 편하게 읽고 쓸 수 있어 익히기 쉽고 직관적이라는 장점이 있지만, Key-Value 형태로 저장된 데이터를 Redis 내에서 처리하는 것이 어렵다는 단점이 있다.
  • 다양한 데이터 타입
    • Value의 타입을 다양하게 저장할 수도 있다.
    • List, String, Set, Sorted Set 등 여러 데이터를 저장하여 손쉽고 편하게 저장할 수 있다.
  • Persistence
    • 데이터를 메모리가 아닌 Disk에 저장할 수도 있다.
    • 따라서 서버가 shutdown 된 후에 재실행 하더라도 Disk에 저장해놓은 데이터를 다시 읽어 데이터가 유실되지 않는다.
    • Disk 저장 방식으로는 Snapshot, AOF 방식이 있다.
      • Snapshot: RDB에서도 사용하고 있는 방식이다. 특정 시점의 데이터를 Disk에 옮겨담는 방식으로 Blocking 방식의 SAVE, Non-blocking 방식의 BGSAVE 방식이 있다.
      • AOF: Redis의 모든 write/update 연산 자체를 log 파일에 기록하는 방식이다. 서버 재시작 시 write/update 연산을 순차적으로 재실행하여 데이터를 복구한다.
    • Redis 공식 문서에는 두 방식을 혼용해서 활용하는 것을 권장한다.
      • 주기적으로 snapshot으로 백업해두고, 다음 snapshot 까지 그 사이에 발생한 연산들을 AOF 방식으로 저장하는 것.

RefreshToken

리프레시 토큰이란 액세스 토큰의 유효기간을 짧게 하여 보안도 높이고, 편의성도 챙기기 위해 존재하는데, Refresh Token은 Access Token이 만료되었을 때, 새로 발급해주는 토큰이라고 보면 된다.
1. 로그인을 하면 Access Token 과 Refresh Token을 발급해준다. ( Access 토큰만 클라이언트 쿠키에 저장 & Access 토큰과 Refresh 토큰을 레디스에 저장)
2. 클라이언트는 API를 호출할 때마다 발급받은 Access Token을 활용하여 요청을 한다.
3. 토큰을 사용하던 중, 만료되어 Invalid Token Error가 발생한다면 사용자가 보낸 Access Token으로 레디스의 Refresh Token을 찾아보고 Refresh 토큰이 유효하다면, Access Token을 다시 발급해준다.
4. Redis에 Refresh Token과 짝을 이루는 Access Token을 새로 발급한 토큰으로 업데이트한다.
5. 만약, Refresh Token도 만료되었다면, 다시 로그인을 하도록 요청한다.
6. 만약 사용자가 로그아웃을 하면, refresh token을 삭제하고 사용이 불가하도록 한다.

refresh token 개변을 활용하려면, 불가피하게 서버측에서 토큰 정보를 저장할 공간이 필요한데, 만료가 되는 영구적 데이터가 아니고 Mysql과 같이 데이터베이스에 직접 저장하기에는 중요한 데이터가 아니라고 보고 레디스를 사용하기로 하자.
레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지이다. 모든 데이터를 메모리에(메인 메모리인 RAM) 저장하고 조회하는 in-memory 데이터 베이스 이다.
인메모리 상태에서 데이터를 처리하기 때문에, 다른 DB들보다 빠르고 가볍다는 장점이 있다.

  • 참고

    맴캐쉬드는 명로하고 단순함을 위하여 개발된 반면, 레디스는 다양한 용도에 효과적으로 사용할 수 있도록 많은 특징을 가지고 개발되었다. 맴캐쉬드는 문자열만 지원하기 때문에 레디스로 하도록 하겠다.

구현하기

1. 의존성 추가

	implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.2.0'
	implementation 'org.springframework.session:spring-session-data-redis:3.1.1'

2. application.yml에 추가

redis:
  host: localhost
  port: 6379

3. RedisConfig 작성

@Configuration
public class RedisConfig {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

스프링 애플리케이션에서 redis를 사용하기 위해 Config 파일을 작성하여 빈을 등록해준다.
Lettuce로 RedisConnectionFactory를 통해 Redis와 연결하고 redisTemplate()매서드로 RedisTemplate 객체를 생성하고 구성한다. 이때 Redis 연결 팩토리를 설정하고, 키와 값을 직렬화(serializer)한다.
Spring 에서 Redis를 사용하는 방법에는 2가지가 있다.

4. RedisUtil 작성

Spring 에서 Redis를 사용하는 방법에는 2가지가 있다.
첫번째 방법은 crudrepository방식이다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@RedisHash(value = "refresh_token")
public class RefreshToken {

    @Id
    private String authId;

    @Indexed
    private String token;

    private String role;

    @TimeToLive
    private long ttl;

    public RefreshToken update(String token, long ttl) {
        this.token = token;
        this.ttl = ttl;
        return this;
    }

}

위와 같이 엔티티를 작성하여

  • @Id : 키(key) 값. 우리가 엔티티 작성할 때 흔히 쓰는 어노테이션이다. 식별자로 사용!
  • @RedisHash(value = " ") : 설정한 값을 Redis의 key 값 prefix로 사용한다.
  • @Indexed : 값으로 검색을 할 때 인덱스로 사용!
  • @TimeToLive : 유효시간 설정(second 단위)
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {

    Optional<RefreshToken> findByToken(String token);
    Optional<RefreshToken> findByAuthId(String authId);
}

와 같이 작성하여 마치 JpaRepository를 사용하듯 사용하는 방법이다.
필자는 두번째 방법인 템플릿을 사용하여 구현해 보겠다.
다음과 같이 RedisUtil을 작성한다.

@Component
@RequiredArgsConstructor
public class RedisUtil {

    private final RedisTemplate<String, Object> redisTemplate;

    public void save(String key, Object val, Long time, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, val, time, timeUnit);
    }

    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public boolean delete(String key) {
        return Boolean.TRUE.equals(redisTemplate.delete(key));
    }
}

이렇게 하면 다음과 같이 refreshToken을 생성할때 redis를 사용하여 refreshToken을 저장할 수 있다.

    public String createJwtRefreshToken(CustomUserDetails customUserDetails) {
        Instant issuedAt = Instant.now();
        Instant expiration = issuedAt.plusMillis(refreshExpMs);

        String refreshToken = Jwts.builder()
                .header()
                .add("alg", "HS256")
                .add("typ", "JWT")
                .and()
                .claim("email", customUserDetails.getUsername())
                .claim("is_staff", customUserDetails.getStaff())
                .issuedAt(Date.from(issuedAt))
                .expiration(Date.from(expiration))
                .signWith(secretKey)
                .compact();

        redisUtil.save(
                customUserDetails.getUsername(),
                refreshToken,
                refreshExpMs,
                TimeUnit.MILLISECONDS
        );
        return refreshToken;
    }
    public boolean validateRefreshToken(String refreshToken) {
        // refreshToken validate
        String username = getUserEmail(refreshToken);

        //redis 확인
        if (!redisUtil.hasKey(username)) {
            throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN);
        }
        return true;
    }

와 같이 Redis를 사용하여 refresh token의 유효성을 검사하게 할 수있다. refresh token이 Redis에 저장되어 있는지 확인하여 유효성을 검증한다.

profile
고민0

0개의 댓글