인생을 코딩하다.

[Spring, DB] Redis 연동하기 및 Spring Boot Redis Docs 살펴보기 본문

Spring

[Spring, DB] Redis 연동하기 및 Spring Boot Redis Docs 살펴보기

Hyung1 2021. 6. 26. 23:48
728x90
반응형

안녕하세요. 이번에는 프로젝트에서 Redis를 적용해야 하는 이슈가 생겼는데요. Redis를 적용해야 하는 이유는 여기서 확인 하실 수 있습니다.

우선 Spring Boot Redis Docs와 함께 외부 Redis에 간단히 알아볼까요?

https://docs.spring.io/spring-boot/docs/2.0.0.M7/reference/htmlsingle/#boot-features-connecting-to-redis

 

Spring Boot Reference Guide

This section dives into the details of Spring Boot. Here you can learn about the key features that you may want to use and customize. If you have not already done so, you might want to read the "Part II, “Getting Started”" and "Part III, “Using Spr

docs.spring.io

위의 docs를 기반으로 글을 작성해보도록 하겠습니다.

 

Spring Data는 다양한 NoSQL 기술, 예를들면 MongoDb, Elasticsearch 등의 기술을 사용할때 도움을 준답니다.

이에반해 Spring Boot는 자동설정을 제공해 준답니다. 어떠한 기술을 연동해서 쓸 때 필요한 빈 설정들을 Spring Boot가 자동으로 설정해주고, 우리가 해야할 일은 클래스패스를 추가하고 가져다가 쓰는일 이것이 스프링 부트가 해주는 일이라고 설명되어 있네요.

그리고 spring.io/spring-data를 참조하라고 써있습니다. 참조해볼까요?

  • Redis는 캐쉬, 메시지 브로커 등 다양한 기술을 가지고 있는 키와 벨류로 이루어진 저장소 입니다..
  • Spring Boot는 기본적인 자동설정을 제공하는데 Lettuce와 Jedis라는 클라이언트 라이브러리를 제공합니다.
  • 그리고 Lettuce와 Jedis 기반으로 Spring Data Redis가 추상화 된 것(RedisTemplate)을 제공합니다.
spring-boot-starter-data-redis

라는 의존성이 있으니 이것을 추가해서 편리하게 사용할 수 있습니다. 기본적으로 Lettuce(Redis Client)를 사용하고 starter는 전통적인 방법과 리액티브한 방법들을 제공해줍니다.

리액티브한 프로그래밍을 만들때는 위의 의존성에 -reactive만 더 붙여서 사용하면 되겠죠?

RedisConnectionFactory, StringRedisTemplate RedisTemplate를 빈으로 주입받아서 사용할 수 있고, 기본적으로 localhost는 6379를 이용합니다.

LettuceClientConfigurationBuilderCustomizer을 구현한 빈을 등록해서 커스터마이징 할 수 있다고 합니다.
Jedis를 사용하고 싶다면 JedisClientConfigurationBuilderCustomizer를 사용해야 한다고 하네요.

또 DB의 Connection-poll을 쓰는 것처럼 Redis도 commons-pool2라는 것이 있다고 하네요.

직접 이제 Redis를 연동해볼까요?

 implementation("org.springframework.boot:spring-boot-starter-redis") implementation("org.springframework.session:spring-session-data-redis")

 

  • Session data를 redis에 저장하기 위해서는 spring-session-data-redis 의존성을 추가해주어야 합니다.
  • 그리고 redis client는 lettuce를 이용할 것입니다. lettuce모듈을 별도로 추가하지 않아도 spring-boot-starter-data-redis을 통해 Lettuce 관련 모듈을 사용할 수 있으므로 spring-boot-starter-data-redis를 추가해줍니다.

Lettuce vs Jedis

위에서 Redis Docs를 읽어볼 때 spring-boot-starter-data-rediss는 Lettuce와 Jedis라는 클라이언트 라이브러리를 제공한다고 했죠?

전 Lettuce를 선택했는데 왜 Lettuce를 선택했을까요? 한번 비교해보도록 하겠습니다.

Jedis는 파이프라인을 제외하고 모두 동기식으로 되어있습니다. 이에비해 Lettuce는 동기, 비동기를 모두 지원합니다.

1). 첫째 멀티쓰레드 환경에서

여러 쓰레드에서 단일 jedis 인스턴스를 공유하려 할때 jedis는 쓰레드에 안전하지 않습니다. 멀티쓰레드 환경에서 고려해야할 상황이 따릅니다. pooling(Thread-pool)과 같은 jedis-pool을 사용하여 안전하게 쓰는 법이 있지만 connection할 인스턴스를 미리 만들어놓고 대기하는 연결비용의 증가가 따릅니다.

반면 lettuce는 netty 라이브러리 위에서 구축되었고, connection 인스턴스((StatefulRedisConnection))를 여러 쓰레드에서 공유가 가능하기 때문에 Thread-safe합니다.

Netty란?

Netty는 프로토콜 서버 및 클라이언트와 같은 네트워크 어플리케이션을 빠르고 쉽게 개발하는 것을 가능하게 해주는 NIO 클라이언트 서버 프레임워크입니다. Netty는 TCP, UDP 소켓 서버 개발과 같은 네트워크 프로그래밍을 매우 간단하고 능률적으로 만들어줍니다.

Netty는 NIO 클라이언트 서버 프레임워크로써 싱글 쓰레드로 이벤트 루프 기반위에서 실행됩니다. 이벤트 루프는 채널과 큐를 이용해서 작업을 처리합니다. 여기서 이벤트 루프에 관해 글을 작성하기엔 너무 길어지고, Jedis와 lettuce에 관해 쓴 글의 목적이 불문명해져 여기를 참고하면 좋을 것 같습니다.

즉 간단하게 말해서 이벤트 루프 기반이기 때문에 Non-Blocking I/O를 이용해서 싱글 쓰레드, 및 비동기 기반인 lettuce가 효율적으로 작업을 처리할 수 있습니다. 비동기 방식을 사용하면 네트워크,디스크 I/O를 기다리는 스레드를 낭비하지 않고 다른 작업을 수행하여 자원을 효율적으로 사용하기 때문에 성능이 더 빠른 장점이 있습니다.

2). 속도 비교

lettuce는 netty(비동기 이벤트 기반 고성능 네트워크 프레임워크) 라이브러리 위에서 구축되었기 때문에 Jedis보다 고성능입니다.

https://jojoldu.tistory.com/418


벤치마크를 비교한 결과 TPS, CPU, 커넥션수, 응답 속도 모든 분야에서 lettuce가 앞섭니다. 이외에도 공식 문서가 잘 만들어져있고 Issue제기시 피드백이 훨씬 빠르다고 합니다.. jedis는 릴리즈가 중단되었다가 최근에서야 다시 올라오기 시작했답니다.

자 이제 계속 연동해볼까요?

우선 https://github.com/microsoftarchive/redis/releases에서 Redis를 설치해줍니다. 설치 후 redis-server.exe를 실행하면 redis 서버가 구동됩니다.

 

그리고 paalication-local.yml에 아래와 같이 작성해줍니다.

spring:
  redis:
    host: localhost
    port: 6379

host는 현재 로컬 환경의 서버니깐 localhost로 적어주고, port는 6379를 적어줍니다. 6379로 적어준 이유는 레디스 서버의 기본 포트 번호가 6379이기 때문에 포트 번호를 따로 바꾸지 않아서 기본 포트인 6379에 연결해주었습니다.

 

이제 Config파일을 작성해주어야 하는데요,

@Configuration
@EnableRedisHttpSession
class RedisConfig(

    @Value("\${spring.redis.host}")
    private val redisHost: String,

    @Value("\${spring.redis.port}")
    private val redisPort: Int

) {

    @Bean
    fun redisConnectionFactory() = LettuceConnectionFactory(redisHost, redisPort)
    
}

 우리는 Lettuce 라이브러리를 사용할 것이기 때문에, 

https://docs.spring.io/spring-data/data-redis/docs/current/api/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.html
LettuceConnectionFactory 내부구조

RedisStandaloneConfiguration(host, port) 생성자를 이용하여 새로운 인스턴스를 생성하고 스프링 세션을 레디스 서버로 연결시킵니다.

    @Bean
    fun redisConnectionFactory() = LettuceConnectionFactory(redisHost, redisPort)

Redis에 세션 데이터를 저장하기 위해  @EnableRedisHttpSession 어노테이션을 추가해주어야 합니다. 

 

@EnableRedisHttpSession란 뭘까요?

 

  • 서블릿의 Filter 인터페이스를 구현한 springSessionRepositoryFilter을 스프링 빈으로 등록합니다.
  • 해당 애노테이션으로 springSessionRepositoryFilter가 빈으로 등록됩니다.
  • Spring Session 모듈에 있는 AbstractHttpSessionApplicationInitializer가 Filter를 서블릿 컨텍스트에 추가하여 모든 서블릿 요청에 Filter가 작동하도록 합니다.
  • 이 필터는 WAS에서 제공하는 HttpSession 구현체 인스턴스를 Spring Session기반 인스턴스로 변환시켜 Redis에 저장하는 세션으로 사용될 수 있도록 해줍니다.

이제 데이터를 레디스에 저장하는 것 까진 완료 하였습니다. redis-cli를 통해 데이터를 조회해보면 값이 잘 저장된 것을 볼 수 있습니다.

127.0.0.1:6379> hget "spring:session:sessions:9a398c47-36c6-4d33-95d1-80f51447f7eb" "sessionAttr:email"
"\xac\xed\x00\x05t\x00\x0fadmin@gmail.com"

하지면 여기서 문제가 하나 있습니다.

 

스프링에서 로그인 정보(val email:String)를 Redis 서버에 저장할 때, Redis의 데이터 저장 형식은 byte array이기 떄문에 값이 String으로 저장되는 것이 아닌 byte형식으로 저장됩니다.

따라서 RedisTemplate을 이용해서 직렬화하여 직렬화를 통해서 해결을 해야하는데요,

 @Bean
fun redisTemplate(): RedisTemplate<String, Any> {
	val redisTemplate = RedisTemplate<String, Any>()
    
	return redisTemplate.apply {
  		 setConnectionFactory(redisConnectionFactory())
	   	 keySerializer = StringRedisSerializer()
  	     	 valueSerializer = GenericJackson2JsonRedisSerializer()
}

아래 코드는 Jackson2RedisSerializer을 사용했습니다.

  • Jackson2RedisSerializer 클래스는 Jackson을 활용해 객체 등의 데이터를 JSON의 형태로 바꿔서, Redis 내에 데이터를 읽고 쓸 수 있게 해주는 역할을 합니다.
valueSerializer = GenericJackson2JsonRedisSerializer()

왜 자바 직렬화가 아닌 JSON 형태로 직렬화해서 저장했을까요?

 

자바 직렬화 방식에는 문제점이 있습니다. 물론 장점도 있습니다. 장점먼저 살펴볼까요?

  • 자바 직렬화의 장점
    자바 직렬화는 자바 시스템에서 개발에 최적화되어 있습니다. 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화를 가능합니다.
  • 물론 역직렬화도 마찬가지입니다. 당연하게 보이는 장점 중에 하나지만 데이터 타입이 자동으로 맞춰지기 때문에 관련 부분을 큰 신경을 쓰지 않아도 됩니다. 그렇게 역직렬화가 되면 기존 객체처럼 바로 사용할 수 있게 됩니다. 개발자 입장에서 상당히 편한 부분인거죠. (java.io.Serializable 인터페이스만 구현해주면)

동작 방식에는 문제가 없지만 자바 직렬화 방식에는 큰 문제점이 존재합니다. 그럼 이번엔 문제점을 살펴볼까요?

 

역직렬화시 클래스 구조 변경 문제

  • 외부(DB, 캐시 서버, NoSQL 서버 등)에 장기간 저장될 정보는 자바 직렬화 사용을 지양해야 합니다. 역직렬화 대상의 클래스가 언제 변경이 일어날지 모르는 환경에서 긴 시간 동안 외부에 존재했던 직렬화된 데이터는 쓰레기(Garbage)가 될 가능성이 높습니다. 언제 예외가 발생할지 모르는 지뢰 시스템이 될 수도 있습니다.

메모리 문제

  • 자바 직렬화시에 기본적으로 타입에 대한 정보 등 클래스의 메타 정보도 가지고 있기 때문에 상대적으로 다른 포맷에 비해서 용량이 큰 문제가 있습니다. 특히 클래스의 구조가 거대해지게 되면 용량 차이가 커지게 됩니다.
  • 예를 들면 클래스 안에 클래스 또 리스트 등 이런 형태의 객체를 직렬화 하게 되면 내부에 참조하고 있는 모든 클래스에 대한 메타정보를 가지고 있기 때문에 용량이 비대해지게 됩니다. 그래서 JSON 같은 최소의 메타정보만 가지고 있으면 되는 테스트로 된 포맷보다 같은 데이터에서 최소 2배 최대 10배 이상의 크기를 가질 수 있습니다.

용량 문제는 생각보다 많은 곳에서 나타나는 문제입니다. 특히 직렬화된 데이터를 메모리 서버(Redis, Memcached)에 저장하는 형태를 가진 시스템에서 두드러집니다. 메모리 서버 특성상 메모리 용량이 크지 않기 때문에 핵심만 요약해서 기록하는 형태가 효율적입니다.

 

 보안

  • java에서 제공하는 직렬화는 취약한 라이브러리와 확인 되지 않은 바이트 코드를 삽입하는 클래스를 악용하는 페이로드로 인한 원격 코드 실행을 허용합니다. 조작된 입력은 역 직렬화 단계 중에 애플리케이션에서 원치 않은 코드 실행으로 이어질 수 있어 신뢰 할 수 없는 환경에서는 직렬화를 사용하면 안됩니다. 그래서 json 직렬화를 권장하게 됩니다.

자바 직렬화는 장점이 많은 기술입니다만 단점도 많습니다. 문제는 이 기술의 단점은 보완하기 힘든 형태로 되어 있기 때문에 사용 시 제약이 많습니다. 그래서 항시 고민해 봐야하는 규칙들이 있습니다.

  1. 외부 저장소로 저장되는 데이터는 짧은 만료시간의 데이터를 제외하고 자바 직렬화를 사용을 지양합니다.
  2. 역직렬화시 반드시 예외가 생긴다는 것을 생각하고 개발합니다.
  3. 자주 변경되는 비즈니스적인 데이터를 자바 직렬화을 사용하지 않습니다.
  4. 긴 만료 시간을 가지는 데이터는 JSON 등 다른 포맷을 사용하여 저장합니다.

위의 코드엣 JSON 직렬화를 해주지 않는다면 코드를 훨씬 간결하게 짤 수 있는 방법도 존재합니다.

 

StringRedisTemplate입니다.

 @Bean 
 fun stringRedisTemplate() = StringRedisTemplate(redisConnectionFactory())
    

 

"RedisTemplate의 문자열 중심 확장입니다. Redis에 대한 대부분의 작업은 문자열 기반이므로이 클래스는 template특히 serializer 측면에서 보다 일반적인 구성을 최소화하는 전용 클래스를 제공합니다 " 라고 적혀있네요.

 

StringRedisTemplate를 이용하면 위의 코드처럼 훨씬 간소화 할 수 있겠죠?

 

만약 위의 코드에서 Json 직렬화를 해준다고 하면, 아래 코드와 같이 변경할 수 있는데요,

 @Bean 
 fun stringRedisTemplate() {
 	val stringRedisTemplate = StringRedisTemplate()
    
 	return stringRedisTemplate.apply {
    	setConnectionFactory(redisConnectionFactory())
    	valueSerializer = GenericJackson2JsonRedisSerializer()
}
    

하지만 String으로 직렬화 한 후 한 번더 Json 직렬화를 하는 비용이 발생하기 떄문에, RedisTemplate을 이용한 직렬화가 훨씬 낫겠죠?

 

이렇게 Redis Docs를 읽어보며 Redis에 관해 알아가는 시간을 가졌습니다.

 

또 이를 바탕으로 스프링에서 Redis를 연동하는 과정 및 이슈 들에 관해 글을 써보았는데요, 저는 코틀린으로 했지만 자바로 구현하셔도 크게 별다를 건 없으실 겁니다. 

 

혹시나 자바로 구현하는 중에 어려운게 있으시면 인텔리제이에서 자바 언어로 변환해서 보셔도 괜찮을 것 같습니다~

728x90
반응형
Comments