[spring] (8) Redis Data Store

orca·2022년 12월 31일
0

Spring

목록 보기
8/13
post-thumbnail

Redis is an open-source in-memory datastore used as a database, cache, and even a message broker.
레디스는 DB, 캐시, 메세지 브로커로 쓰일 수 있는 인메모리 데이터 저장소임

-NoSQL DB
-Key-Value DB

Key-Value DataBase

Key-Value DB는 항상 고유한 단순 문자열(Key)과 임의의 큰 데이터 필드(Value)를 포함함
Value에 대한 포인터와 함께 고유한 Key를 저장하기 위해 해시 테이블을 구현한다
Value은 정수, JSON, 배열 etc...
Key는 해당 값을 참조하는 데 사용됨
오직 Key를 이용해서만 질의를 할 수 있다~

refresh token 같이 단시간 다량의 엑세스를 필요로 하는 도메인에 적용해보면 좋다

로컬에 Redis DB 구축하기

사전에 도커가 설치되어 있어야 한다

  1. redis 도커 설치하기
docker run --name some-redis -d -p 6379:6379 redis redis-server --requirepass 1234

컨테이너 이름은 some-redis 이고
베이스 OS의 6379 포트로 접근하면 접근할 수 있고
redis-server 비밀번호는 1234로 설정했다

실제 서비스한다면 패스워드와 더불어 포트도 변경해주는 게 좋음.. 구글링해본 결과 클라우드 환경에 레디스가 설치된 케이스에 대부분 해킹 시도가 있다고 한다

docker ps

항목 중 특히 PORTS 부분이 아래와 같이 설정한대로 잘 맵핑되었나 확인되어야 한다
0.0.0.0:6379->6379/tcp

레디스가 잘 설치된 걸 확인

  1. redis-cli 접속해보기
docker exec -it some-redis redis-cli

위와 같이 레디스 내부 데이터를 살펴보려고 하면
(error) NOAUTH Authentication required. 가 발생함
이때 아까 설정해주었던 password로 접속 가능함

AUTH 1234

Redis를 스프링 프로젝트와 연결

  1. bulid.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.7.7'
  1. application.yml
spring:
  redis:
    port: 6379
    host: localhost
    password: 1234
    timeout: 3000

이중 timeout은 password가 설정된 경우 레디스 DB가 액티브 상태가 아니면
서버가 에러 코드를 보내야 하는데, 이 시간이 굉장히 오래걸렸기 때문에 설정해둠

대부분 redis는 메인 DB로 사용하지 않으니까 timeout도 함께 설정해두는 것을 추천한다

  1. RedisConfig.java

SpringBoot 2.0이상이라면, 아래 코드이면 된다

@Configuration
@EnableCaching
public class RedisConfig {

}

@EnableCaching은 캐싱을 사용하겠다는 의미임

SpringBoot 2.0이하라면,
해당 파일 내부에 RedisConnectionFactory 정의해서 직접 빈 주입해줘야함
@EnableRedisRepositories 어노테이션도 추가로 붙여줘야함

@Configuration
@EnableCaching
@EnableRedisRepositories
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;

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

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
        configuration.setPassword(password);
        return new LettuceConnectionFactory(configuration);
    }

}
  1. RefreshToken.java
    JPA 레파지토리 생성할 때처럼, 도메인 파일을 먼저 만들자
    @RedisHash : Redis 도메인을 선언함
    @Id : Id는 해당 도메인의 기본 Key가 된다
    @Indexed : Id 외에 다른 변수를 Key로 설정하고 싶을 때 사용한다
@Getter
@Builder
@RedisHash("RefreshToken")
public class RefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Indexed
    private String username;

    private String tokenValue;

    private String updateAt;

    public static RefreshToken of(String username, String tokenValue){
        return RefreshToken.builder()
                .username(username)
                .tokenValue(tokenValue)
                .updateAt(LocalDateTime.now().toString())
                .build();
    }
}

Redis는 SQL과 달리 DATE or TIMESTAMP 자료형이 없어 @TimeStamp나 @CreatedDate 등등 사용이 불가했다
그래서 생성 시점 시간을 직접 String으로 변환해서 담아줌

  1. RefreshTokenRepository.java
    주의해야할 부분은 CrudRepository로 레포지토리를 생성해야 한다.
    도메인에서 명시한@Indexed@Id 변수를 이용해서 쿼리 작성해야함
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByUsername(String username);
}
  1. UserService.java
    기존에 있었던 로그인 로직에 RefreshToken 저장 로직을 추가한다
@Service
public class UserService {

    private final UserRepository userRepository;
    private final RefreshTokenRepository refreshTokenRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtUtil jwtUtil;



    public TokenDto loginUser(SigninForm form) {
        User user = userRepository.findByUsername(form.getUsername())
                .orElseThrow(()->new IllegalArgumentException("유저가 존재하지 않음"));
                
        if(passwordEncoder.matches(form.getPassword(), user.getPassword())){
                TokenDto dto = jwtUtil.generateToken(user.getUsername());
                refreshTokenRepository.save(RefreshToken.of(user.getUsername(),dto.getRefreshToken()));
                
                return dto;
        }else{
        	throw new IllegalArgumentException("패스워드가 다름");
    }
  ...
  }
    

Redis Cache를 활용하는 부분
@Cacheable : 캐싱을 활용할것임을 명시한다
-key : 레포지토리에서 사용했던 key 변수
-cacheNames : 캐싱의 대상이 되는 value, 즉 도메인

@Service
public class UserService {
...
@Cacheable(key = "#username", cacheNames = "RefreshToken")
    public String readToken(String username) {
        RefreshToken token = refreshTokenRepository.findByUsername(username)
                .orElseThrow(()->new IllegalArgumentException("해당 토큰이 존재하지 않음"));
        return token.getTokenValue();
    }
 ...
}

기타 Redis와 크게 상관은 없지만 참고용,,

UserController.java

@RestController
public class UserController {
    ...
    
@PostMapping("/auth/signin")
    public ResponseEntity loginUser(@RequestBody SigninForm form){
        return ResponseEntity.ok(userService.loginUser(form));
    }
    

@GetMapping("/auth/read")
    public ResponseEntity readToken(@RequestParam String username){
        return ResponseEntity.ok(userService.readToken(username));
    }
    ...
    }
 

SigninForm.java

@Getter
@Setter
public class SigninForm {
    private String username;
    private String password;
}

테스트

  1. POST localhost:8080/auth/signin

redis-cli 접속

keys *
hgetall Token:8502679460442746444

  1. GET localhost:8080/auth/read?username=haden
    저장된 토큰도 잘 가져온다

-첫번째 request

-두번째 request

Response 부분에 Time을 보면 반복적으로 read를 요청했을 때,
326ms > 25 ms로 시간이 확 줄어듦

캐싱도 잘 되고 있음을 확인!

0개의 댓글