ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redis, SpringBoot 연동하기
    Spring Framework 2021. 7. 16. 17:08
    반응형

    서론 

    포트폴리오용 백엔드 서버를 구성 후 문득 생각이 들었습니다.

     

    "만약에 사람들이 몰려서 DB의 조회가 많이 일어나면 어떡하지?" 물론 아무도 사용하지 않는 사이트지만요ㅎㅎ

     

    검색을 해본 결과 Redis 오픈소스를 사용하면 캐싱을 통해서, 다수의 처리를 쉽게 할 수 있을 것 같았습니다. Spring Data Redis 공식 문서를 참고하여, 기본적인 캐싱을 해보겠습니다.

     

    이 글은 아주 기본적인 Hello World 수준이므로, 더 깊은 내용을 원하시는 분들은 뒤로 가기 누르셔도 됩니다. 

    Redis란?

    Redis는 in-memory 방식의 데이터 저장소 입니다. 여기서 in-memory 방식이라는 것은 주 메모리(RAM)를 데이터 저장소로 사용하는 데이터베이스 관리 시스템입니다. 즉 SSD, HDD 같은 DISK를 활용하는 것이 아닌, 램을 저장소로 사용합니다. 이때의 이점은 DISK에 비해 접근 속도가 훨씬 뛰어납니다. 

     

    그렇다면 빠르게 접근 가능한 데이터 저장소에 무엇을 저장할까요?

     

    저장소는 database, cache, message broker로 사용이 됩니다. Cache는 것은 데이터를 임시로 복사해 두는 것을 표현합니다. 똑같은 정보를 DB에 100번 조회하는 것보다, 이미 조회한 것은 캐시로 저장해 두고 꺼내 쓰면 DB 조회를 줄일 수 있고, 접근 속도도 높일 수 있습니다. Message Broker는 송신자의 메시지 프로토콜을 수신자의 메세지 프로토콜로 변환해주는 역할을 합니다. 

     

    그리고 이 저장소에 저장할 수 있는 자료구조는 string, hash, list, set, sorted set이 있습니다. 그래서 좀 더 유연한 데이터 저장 구조를 사용할 수 있습니다.

     

    마지막으로 Redis 서버에는 데이터가 Key - Value 형식으로 저장됩니다. 


    설정

    1. Redis 설치 (MacOS)

    brew install redis

    2. Redis 서버 구동

    brew servcies start redis
    
     // 반드시 redis 설치된, redis.conf 파일이 있는 곳에서 실행 해주세요.
    redis-cli -c
    
    // 기본 6379 포트가 실행되며, 이를 모니터 합니다.
    127.0.0.1:6379> monitor

    Redis는 Master - Replica 구조로서 Cluster를 구성할 수 있습니다. 하지만 이 글에서는 한 개의 포트를 이용해서 마스터 1개만을 이용합니다. 


    RedisConnection 

    스프링 IoC 컨테이너를 통해 Redis 저장소에 접근하기 위해서는 RedisConnection 인터페이스가 필요합니다. MySQL Connector과 같은 역할을 한다고 생각하시면 됩니다. 

     

    RedisConnection은 RedisConnectionFactory 인터페이스에 의해 구현됩니다. 

    public interface RedisConnectionFactory extends PersistenceExceptionTranslator {
    
    	/**
    	 * Provides a suitable connection for interacting with Redis.
    	 *
    	 * @return connection for interacting with Redis.
    	 * @throws IllegalStateException if the connection factory requires initialization and the factory was not yet
    	 *           initialized.
    	 */
    	RedisConnection getConnection();
        
    }

     

    하지만 우리는 RedisConnectionFactory를 바로 사용하지 않습니다. 이것을 상속받은 Lettuce 라이브러리를 사용하려고 합니다.

    public class LettuceConnectionFactory
    	implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {}

    3. Spring Config

    // 의존성 (Gradle)
    implementation 'org.springframework.data:spring-data-redis:2.5.2'
    implementation 'io.lettuce:lettuce-core:6.1.3.RELEASE'
    // .yml 설정 파일
    spring:
      cache:
          type: redis
      redis:
          host: 127.0.0.1
          port: 6379

    RedisConfiguration을 활용해서 자바 코드로 직접 작성해도 괜찮다고 생각합니다. 하지만 깃허브 같은 공개 저장소에 올릴 때는 .yml 설정 파일에 작성 후 공개하지 않으므로서, 좀 더 보안적으로 괜찮은 방법이라고 생각합니다.

    @Configuration
    @AllArgsConstructor
    public class CacheConfig {
    
        RedisConnectionFactory connectionFactory;
    
        @Bean // 스프링 로컬 캐시가 아닌, Redis 서버에 저장하기 위한 설정 입니다.
        public CacheManager redisCacheManager() {
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            return RedisCacheManager.RedisCacheManagerBuilder
                    .fromConnectionFactory(connectionFactory)
                    .cacheDefaults(redisCacheConfiguration)
                    .build();
        }
    }

     

     

    4. @Cacheable 

    본인이 캐싱을 하고자 하는 서비스 로직에 추가해주시면 됩니다. 

     

    저는 기존의 애플리케이션을 그대로 활용했으나, 이 글을 보시는 분들은 Test 코드를 작성하셔야 합니다.

    @Cacheable(cacheNames = "findPost", value = "findPost", key = "#boardName") 
    @Transactional
    public List<ListPostDTO> findPosts(String boardName) { // 게시글 목록 보여줍니다. 
    	// 코드 생략
    }
    • @cacheable: 캐싱된 데이터가 있다면 반환, 없다면 DB 조회 후 캐싱
    • cacheNames: namespace, 다른 namespace에 같은 key 값 저장 가능
    • value: 캐시의 값
    • key: 캐시의 key

    자바 코드 실행 결과

    최초 실행했을 때 아래와 같이 SQL문이 실행된 것을 볼 수 있습니다.

    Hibernate: select board0_.boardNo as boardno1_0_, board0_.name as name2_0_ from board board0_ where board0_.name=?
    
    Hibernate: select post0_.postNo as postno1_1_, post0_.author as author2_1_, post0_.boardNo as boardno5_1_, post0_.content as content3_1_, post0_.title as title4_1_, post0_.userNo as userno6_1_ from post post0_ where post0_.boardNo=?
    
    Hibernate: select user0_.userNo as userno1_4_0_, user0_.provider as provider2_4_0_, user0_.userId as userid3_4_0_, user0_.userName as username4_4_0_, user0_.userPwd as userpwd5_4_0_, roles1_.userNo as userno1_5_1_, role2_.roleNo as roleno2_5_1_, role2_.roleNo as roleno1_3_2_, role2_.roleName as rolename2_3_2_ from user user0_ left outer join users_roles roles1_ on user0_.userNo=roles1_.userNo left outer join role role2_ on roles1_.roleNo=role2_.roleNo where user0_.userNo=?

     

    캐싱된 Key 값 확인

    127.0.0.1:6379> keys *
    1) "findPost::free"

    다시 호출하면 SQL문은 실행되지 않습니다. 왜냐하면 이미 Redis 서버에 저장된 캐시를 반환하기 때문입니다. 

     

    아래와 같이 모든 캐시 삭제 후 다시 호출하면, SQL문이 실행되는 것을 볼 수 있습니다. 즉 DB를 조회합니다.

    127.0.0.1:6379> flushall // 모든 캐시 삭제
    OK

    문제점

    Redis 캐시의 문제점은 자동으로 업데이트가 안됩니다. 즉 이미 조회한 글 목록이 캐시에 남아있다면, 새 글을 작성해도 이전의 글 목록만 보입니다. 

     

    Redis 서버는 캐시가 자동으로 Expire 기간을 정할 수 있지만, 만약에 해당 기간 전에 또 조회를 한다면 문제가 생깁니다. 

     

    아래의 코드는 CacheManager를 이용해서 직접 캐시를 다룹니다. 새 글을 작성 후 기존의 "글 목록 캐시를 삭제" 합니다. 다시 글 목록 로직을 실행하면, 캐시는 없고 다시 DB 조회 후 캐싱합니다. 

    private final CacheManager cacheManager;
    
    // 새 글 작성 후 전체글 목록 캐시 삭제
    cacheManager.getCache("findPost").clear();

     

    사실 저 또한 초보기 때문에 이렇게 캐시를 다루는 것이 맞는 것인지는 모르겠습니다. 직접 캐시를 삭제하는 코드를 넣는 것은 왠지 모르게 위험해 보이고, 불안해 보입니다.


    더 나아가기

    현재 이 글에서는 Redis 설치 시 기본으로 설정되어 있는 6379 포트를 이용합니다. 하지만 Cluster(Master-Replica), Sentinel 등 여러 가지 설정을 이용해서 더 견고한 Redis 서버를 구성할 수도 있습니다. 


    참고 문헌

    https://redis.io

    https://docs.spring.io/spring-data/redis/docs/2.5.2/reference/html/#reference

    반응형

    댓글

Designed by Tistory.