[Spring] Redis 사용법 + jwt 함께 로그인, 로그아웃 구현

hyewon jeong·2023년 6월 27일
3

Spring

목록 보기
48/65
post-thumbnail

1. 레디스 란

1-1.Redis란?

Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템 (DBMS)입니다.

데이터베이스, 캐시, 메세지 브로커로 사용되며 인메모리 데이터 구조를 가진 저장소입니다.

1-2. 레디스의 데이터 구조

2. Redis & RedisCache 사용법 (Spring boot)

2-1. 레디스 의존성 추가

    //redis를 사용하기 위한 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
        //캐시 
    implementation 'org.springframework.boot:spring-boot-starter-cache' 
  • Spring Data Redis는 Spring 프레임워크와 Redis를 통합하여 Redis 데이터베이스와 상호작용하기 위한 기능을 제공하는 라이브러리입니다. Spring Data Redis는 객체 매핑, 쿼리 작성, 트랜잭션 관리 등을 편리하게 처리할 수 있도록 지원합니다.
  • spring-boot-starter-cache는 Spring Boot에서 캐시 관련 기능을 제공하는 의존성 패키지입로 레디스의 캐시기능을 사용할 경우 위의 두번째 의존성도 함께 추가해야 한다.

2-2. application.yml 설정

spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 6
  • spring.redis.host: Redis 서버의 호스트 주소를 지정합니다. 위의 설정에서는 localhost로 설정되어 있으며, 동일한 호스트에서 실행 중인 Redis 서버에 연결하려는 경우 사용됩니다. 만약 Redis 서버가 다른 호스트에서 실행 중이라면 해당 호스트의 IP 주소나 도메인 이름을 여기에 지정해야 합니다.
  • timeout : Redis 연결의 타임아웃은 Redis 서버와의 연결을 시도할 때, 연결이 수립되기까지 허용되는 최대 대기 시간을 의미합니다. 즉, Redis 클라이언트가 Redis 서버에 연결하기 위해 대기하는 시간입니다. 이 설정은 선택적이지만 대부분 권장한다.

2-3. 메인 어플리케이션 클래스에 @EnableCaching 을 선언

@EnableJpaAuditing //auditing 기능 위한 추가
@SpringBootApplication
@EnableCaching // 레디스 캐싱 하기위해 추가, @Cacheable 같은 어노테이션을 인식 하게 함 
public class WonyShopApplication {

  public static void main(String[] args) {
    SpringApplication.run(WonyShopApplication.class, args);
  }

}
  • 해당 어노테이션을 선언하면 스프링 부트에서는 @Cacheable과 같은 캐싱 어노테이션의 사용을 인식하게 됩니다.
  • 해당 애너테이션을 작성하면, 스프링은 AOP를 통해 비 침투적으로 캐싱 전략을 적용합니다.

2-4. Redis DataBase 이용

2-4-1. Redis 를 이용하기 위한 config

@Configuration
public class RedisConfig {

  @Value("${spring.data.redis.host}")
  private String host;
  @Value("${sping.data.redis.port}")
  private int port;

  @Bean
  public RedisConnectionFactory redisConnectionFactory(){
    return new LettuceConnectionFactory(host,port);
  }
  //RedisConnectionFactory는 Redis 서버와의 연결을 관리하는 팩토리입니다.
  // LettuceConnectionFactory는 Lettuce 클라이언트를 사용하여 Redis 연결을 설정합니다.

  /*
          RedisTemplate: Redis data access code를 간소화 하기 위해 제공되는 클래스이다.
                         주어진 객체들을 자동으로 직렬화/역직렬화 하며 binary 데이터를 Redis에 저장한다.
                         기본설정은 JdkSerializationRedisSerializer 이다.

          StringRedisSerializer: binary 데이터로 저장되기 때문에 이를 String 으로 변환시켜주며(반대로도 가능) UTF-8 인코딩 방식을 사용한다.
          GenericJackson2JsonRedisSerializer는 Redis에 저장되는 객체를 JSON 형식으로 직렬화하고, 역직렬화할 수 있도록 합니다.
       */
  @Bean
  public RedisTemplate<String,String> redisTemplate() {
    RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new StringRedisSerializer());
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    //redisConnectionFactory() 메서드를 사용하여 RedisConnectionFactory를 가져와서 RedisTemplate에 설정합니다.
    return redisTemplate;
  }

    @Bean
     public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer(){
      return new GenericJackson2JsonRedisSerializer();
    }
  }

2-4-2. RedisDao

어디서든 redisTemplate를 받아와서 set, get, delete.. 등을 사용할 수 있으나 그렇게 사용하지 않고 RedisDao를 작성하여 사용한다.

redisTemplate객체를 통해 redis 저장소에 Key-Value 쌍으로 데이터를 조회, 저장, 삭제를 한다.

@Component
@RequiredArgsConstructor
public class
RedisDao {

  private final RedisTemplate<String, String> redisTemplate;

  /**
   * 값을 저장
   * @param key : email
   * @param refreshToken
   * @param refreshTokenTime
   */
  public void setRefreshToken(String key, String refreshToken, long refreshTokenTime) {
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(refreshToken.getClass())); // 리프레쉬 토큰을 직렬화 하는 코드 ( 데이터 압축효과도 있음 ) 
    redisTemplate.opsForValue().set(email, refreshToken, minutes, TimeUnit.MINUTES);
    //opsForValue(): RedisTemplate에서 ValueOperations를 가져옵니다.
    //ValueOperations 객체를 통해 Redis의 값을 저장, 조회, 삭제하는 작업을 수행합니다.
  }


  /**
   * 키로 값을 조회
   * @param key : email
   * @return 해당 리프레쉬토큰
   */
    public String getRefreshToken(String key) {
    return  redisTemplate.opsForValue().get(key);
  }

  /**
   *키로 값을 삭제
   * @param key : email
   */
  public void deleteRefreshToken(String key) {
    redisTemplate.delete(key);
  }
  
  
    public boolean hasKey(String key) {
    return Boolean.TRUE.equals(redisTemplate.hasKey(key));
  }

  public void setBlackList(String accessToken, String msg, Long minutes) {
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(msg.getClass()));
    redisTemplate.opsForValue().set(accessToken, msg, minutes, TimeUnit.MINUTES);
  }

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

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

  /***
   * 레디스에 있는 모든 데이터를 삭제
   */
  public void flushAll(){
    redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll();
  }
  //serverCommands().flushAll()을 호출하여 Redis 서버의 flushAll() 명령을 실행하여 모든 데이터를 삭제합니다.
}

2-5. RedisCache 이용

2-5-1. RedisCache 이용하기 위한 Config

RedisCacheManager를 등록할 cacheConfig 설정
Spring CacheManager타입의 RedisCacheManager를 빈으로 등록해주면 Spring에서는 캐싱을 할 때 로컬 캐시에 저장하지 않고 redis에 저장하게 됩니다. 위와 같이 redisCacheManager를 등록해준다.
여기까지 끝났다면 스프링에서 레디스 캐시를 사용하기 위한 준비는 끝났다.

@Configuration
public class CacheConfig {


  @Bean
  public RedisCacheManager cacheManager(
      RedisConnectionFactory connectionFactory, ResourceLoader resourceLoader) {
    RedisCacheConfiguration defaultConfig
        = RedisCacheConfiguration.defaultCacheConfig()
        .disableCachingNullValues()
        .serializeValuesWith(
            RedisSerializationContext
                .SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer())// value Serializer 변경
        );

    //disableCachingNullValues()를 호출하여 null 값 캐싱을 비활성화합니다. 이는 Redis에 null 값을 저장하지 않도록 설정합니다.
    //serializeValuesWith()를 호출하여 값 직렬화를 구성합니다.
    // 위의 코드에서는 GenericJackson2JsonRedisSerializer를 사용하여 값 직렬화를 수행합니다.
    // 이는 값이 JSON 형식으로 직렬화되어 Redis에 저장되고 검색됩니다.
    //구성이 완료된 RedisCacheConfiguration을 사용하여 RedisCacheManager를 생성합니다.
    //이렇게 구성된 RedisCacheManager는 Spring Boot 애플리케이션에서 Redis 캐시를 관리하는 데 사용됩니다.
    // 캐시 설정과 Redis 연결을 제어하고 캐시 작업을 수행할 때 RedisCacheManager를 주입받아 사용할 수 있다.

     //redisCacheConfigMap은 Redis 캐시의 구성 정보를 담는 맵입니다.
    Map<String, RedisCacheConfiguration> redisCacheConfigMap
        = new HashMap<>();

    redisCacheConfigMap.put(
        CacheNames.USERBYUSERNAME,
        defaultConfig.entryTtl(Duration.ofHours(4)) //entryTtl()을 호출하여 캐시 항목의 만료 시간(TTL)을 설정합니다.  캐시 수명 4시간
    );

    // ALLUSERS에 대해서만 다른 Serializer 적용
    redisCacheConfigMap.put(
        CacheNames.ALLUSERS,
        defaultConfig.entryTtl(Duration.ofHours(4))
            .serializeValuesWith( //serializeValuesWith()를 호출하여 값을 직렬화하는 방식을 설정합니다. 
                RedisSerializationContext
                    .SerializationPair
                    .fromSerializer(new JdkSerializationRedisSerializer())
            )
    );
    redisCacheConfigMap.put(
        CacheNames.LOGINUSER,
        defaultConfig.entryTtl(Duration.ofHours(2))
    );


    return RedisCacheManager.builder(connectionFactory)
        .withInitialCacheConfigurations(redisCacheConfigMap)
        .build();
  }

}

//    //전체조회시
//    redisCacheConfigMap.put(
//        CacheNames.GETBOARD,
//        defaultConfig.entryTtl(Duration.ofHours(4))
//            .serializeValuesWith(
//                RedisSerializationContext
//                    .SerializationPair
//                    .fromSerializer(new JdkSerializationRedisSerializer(resourceLoader.getClassLoader()))
//            )
//    );

2-5-2. CacheNames 을 상수로 정의 하는 클래스 생성


/**
 * 위의 코드는 캐시 이름을 상수로 정의하는 클래스인 CacheNames를 나타냅니다. 각 상수는 특정 캐시 영역을 식별하는 문자열 값을 가지고 있습니다.
 * 이러한 캐시 이름 상수는 주로 Spring Cache와 같은 캐싱 기능을 사용할 때 캐시 이름을 지정하는 데 사용됩니다.
 * 예를 들어, @Cacheable 애노테이션에서 cacheNames 속성에 캐시 이름을 설정할 때 이러한 상수를 사용할 수 있습니다.
 *
 * 상수를 사용하여 캐시 이름을 정의하면, 캐시 영역을 구분하고 캐시에 저장된 데이터를 관리하기 쉬워집니다.
 * 또한, 캐시 이름을 상수로 정의함으로써 오타나 잘못된 캐시 이름 사용을 방지할 수 있습니다.
 */
public class CacheNames {
  public static final String USERBYUSERNAME = "CACHE_USERBYUSERNAME";
  public static final String ALLUSERS = "CACHE_ALLUSERS";
  public static final String LOGINUSER = "CACHE_LOGINUSER";

}

2-6. UserService 로그인 및 로그아웃 캐시 이용

  /**
   * 로그인 반환값으로 user를 userResponseDto 담아 반환하고  컨트롤러에서 반환된 객체를 이용하여 토큰 발행한다.
   */
  @Cacheable(cacheNames = CacheNames.LOGINUSER, key = "'login'+ #p0.getEmail()", unless = "#result== null")
  @Transactional
  public UserResponse login(LoginRequest loginRequest) {
    String email = loginRequest.getEmail();
    String password = loginRequest.getPassword();
    User user = userRepository.findByEmail(email).orElseThrow(
        () -> new CustomException(ExceptionStatus.WRONG_EMAIL)
    );
    if (!passwordEncoder.matches(password, user.getPassword())) {
      throw new CustomException(ExceptionStatus.WRONG_PASSWORD);
    }
    return new UserResponse().of(user);// user객체를 dto에 담아서 반환

  }
  
    /**
   * 로그아웃
   * @param accessToken
   * @param email
   * @return
   */
  @CacheEvict(cacheNames = CacheNames.USERBYEMAIL, key = "'login'+#p1")
  @Transactional
  public ResponseEntity logout(String accessToken, String email) {
    // 레디스에 accessToken 사용못하도록 등록
    Long expiration = jwtProvider.getExpiration(accessToken);
    redisDao.setBlackList(accessToken, "logout", expiration);
    if (redisDao.hasKey(email)) {
      redisDao.deleteRefreshToken(email);
    } else {
      throw new IllegalArgumentException("이미 로그아웃한 유저입니다.");
    }
    return ResponseEntity.ok("로그아웃 완료");
  }

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {

  @Cacheable(cacheNames = CacheNames.USERBYEMAIL, key = "'login'+#p0", unless = "#result==null")
  Optional<User> findByEmail(String email);

}
  • cacheNames 속성은 캐시 영역의 이름을 지정합니다. CacheNames.LOGINUSER로 정의된 캐시 영역에 해당하는 데이터를 캐시합니다.
  • key 속성은 캐시의 키를 지정합니다. "'login'+ #p0.getEmail()"는 메서드의 첫 번째 인자인 p0의 getEmail() 메서드를 사용하여 유니크한 캐시 키를 생성합니다. 이를 통해 이메일 주소별로 캐시를 구분합니다.
  • unless 속성은 캐시할 결과가 null인 경우 캐시를 하지 않도록 설정합니다. #result== null은 메서드의 결과가 null인 경우 캐시를 하지 않습니다.

2-7. 결과

2-7-1 . 로그인

로그인 후 레디스에 refreshToken 이 저장된 것을 확인 할 수 있다.

2-7-2 . 로그아웃

로그아웃 후 레디스에서 refreshToken이 삭제 된것을 확인 할 수 있다.

2-7-3. 로그인 후 캐싱 정보 확인하기

Redis는 redis-cli라는 인터페이스를 제공하여 캐시를 눈으로 확인할 수 있도록 지원한다.redis-cli를 설치하고,

  1. keys
    keys
    명령어를 커맨드 라인에 입력하면 현재 저장된 모든 캐시를 확인할 수 있다.
  @Cacheable(cacheNames = CacheNames.LOGINUSER, key = "'login'+ #p0.getEmail()", unless = "#result== null")
  @Transactional
  public UserResponse login(LoginRequest loginRequest) {
  }
  @Cacheable(cacheNames = CacheNames.USERBYEMAIL, key = "'login'+#p0", unless = "#result==null")
  Optional<User> findByEmail(String email);

이 두 코드에 의해
2) "CACHE_USERBYEMAIL::logincustomer123@naver.com"
3) "CACHE_LOGINUSER::logincustomer123@naver.com"
캐싱에 저장에 저장됨을 볼 수 있다.

  1. exists key
    해당키가 존재 하는지 유무를 확인 하기 위한 명령어로 반환값이 1이면 존재, 0이면 미존재를 뜻한다.
exists key


3. get key
해당 키에 대한 value 확인 하는 명령어
해당 키로 값을 조회하면 UserResponseDto 로 user 객체를 저장하고 있다.

get key

4.flushall
현재 저장되어 있는 모든key를 삭제 할 때에는 flushall 명령을 사용한다.

flushall

2-7-4. 로그아웃 후 캐싱 정보 확인하기

로그아웃 후 해당 키가 캐시에 존재 하지 않음을 확인 할 수 있다.

그리고 추후 레디스에서 accessToken을 사용하지 못하도록
redio.setBlackList(accessToken,"logout",expiration) 등록 했다. 시큐리티필터처리 과정에서 "logout" 문구가 있으면 에러메세지 처리 하도록 구현했다.

@Component
@RequiredArgsConstructor
public class
RedisDao {

  private final RedisTemplate<String, String> redisTemplate;


  public void setBlackList(String accessToken, String msg, Long minutes) {
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(msg.getClass()));
    redisTemplate.opsForValue().set(accessToken, msg, minutes, TimeUnit.MINUTES);
  }

Spring Cache 캐시 추상화 기본적인 사용법 @Cacheable @CachePut
Spring Data Redis
Redis-SpringBoot-Redis-를-활용하여-RefreshToken-성능-개선하기
[#2] Redis 캐시를 통해 읽기 성능 향상하기

profile
개발자꿈나무

5개의 댓글

comment-user-thumbnail
2023년 6월 27일

" ```java" 해주세요...

1개의 답글
comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/10/zizh-6-st-johns-wort-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/slendytubbies2-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/scp-goi-the-horizon-initiative-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/biochemical-products-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/scp-people-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/armored-giant-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mini-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/a-house-with-a-playground-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/wall-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/dollars-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/wha-2112-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mi-28-helicopter-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/zombie-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/cat-statue-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/pistol-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/sherman-tank-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/zabuza-momochinpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/borie-or-boreas-type-095-katyusha-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/el-d-215-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/11/german-soldiers-from-wwiisvproplayer-mod-for-melon-playground/

답글 달기
comment-user-thumbnail
2023년 7월 2일
답글 달기
comment-user-thumbnail
2024년 1월 20일

안녕하세요! 작성해주신 포스팅 덕분에 많은 도움이 되었습니다!

포스팅을 읽으며 생긴 질문이 있습니다!
CacheNames 중 ALLUSERS 는 @Cacheable 애노테이션에 사용된 적이 없는데,
CacheConfig 에서 유독 다른 시리얼라이저를 설정해주셨습니다.

ALLUSERS 는 사용하지 않는 CacheNames 인데도 불구하고 왜 저렇게 설정하신건지 여쭤보고 싶습니다!

답글 달기