TheKoguryo's 기술 블로그

 Version 2023.11.20

10.1.2 Spring Boot에서 OCI Cache with Redis로 데이터 캐쉬하기

Spring Boot에서는 캐쉬를 설정할 수 있고, 캐쉬 서버로 Redis를 많이 사용합니다. OCI Cache with Redis로 만든 Redis 클러스터도 Spring Boot, Redis 코드에서 잘 연결되는 지 확인해 봅니다.

예제 Caching Data with Spring으로 기본 캐쉬 따라하기

Caching Data with Spring을 따라 순서대로 진행합니다. ‘OCI Cache with Redis를 캐쉬 서버로 연동’ 전까지는 해당 링크 내용과 동일합니다.

  • 소소를 Download 받아, 압축해제하거나, Git 명령(git clone https://github.com/spring-guides/gs-caching.git)을 사용하여 복제합니다.
  • cd gs-caching/initial
캐쉬설정하기 전
  1. 소스를 빌드하고 실행합니다.

    ./mvnw clean package
    java -jar target/caching-initial-0.0.1-SNAPSHOT.jar
    
  2. 애플리케이션을 실행합니다. 각 조회가 매번 3초 가량 걸리는 것을 볼 수 있습니다.

    2023-11-16T11:34:38.459+09:00  INFO 4132 --- [           main] com.example.caching.AppRunner            : .... Fetching books
    2023-11-16T11:34:41.463+09:00  INFO 4132 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T11:34:44.465+09:00  INFO 4132 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T11:34:47.468+09:00  INFO 4132 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    
Enable caching
  1. src/main/java/com/example/caching/SimpleBookRepository.java@Cacheable("books")를 추가하여, getByIsbn() 메서드의 결과를 books 캐쉬에 캐쉬하도록 설정합니다.

    package com.example.caching;
    
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SimpleBookRepository implements BookRepository {
    
      @Override
      @Cacheable("books")
      public Book getByIsbn(String isbn) {
        simulateSlowService();
        return new Book(isbn, "Some book");
      }
    
      // Don't do this at home
      private void simulateSlowService() {
        try {
          long time = 3000L;
          Thread.sleep(time);
        } catch (InterruptedException e) {
          throw new IllegalStateException(e);
        }
      }
    
    }
    
  2. src/main/java/com/example/caching/CachingApplication.java@EnableCaching annotation을 추가합니다.

    • 캐쉬 라이브러리를 별도로 지정하지 않으면, 디폴트로 ConcurrentHashMap을 사용합니다.
    package com.example.caching;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cache.annotation.EnableCaching;
    
    @SpringBootApplication
    @EnableCaching
    public class CachingApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(CachingApplication.class, args);
      }
    
    }
    
  3. 소스를 빌드하고 실행합니다.

    ./mvnw clean package
    java -jar target/caching-initial-0.0.1-SNAPSHOT.jar
    
  4. 처음 조회가 3초 가량 걸리고, 이후 2번은 캐쉬 데이터를 사용하여, 바로 응답한 것을 볼 수 있습니다.

    2023-11-16T11:53:55.346+09:00  INFO 5782 --- [           main] com.example.caching.AppRunner            : .... Fetching books
    2023-11-16T11:53:58.351+09:00  INFO 5782 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T11:53:58.355+09:00  INFO 5782 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T11:53:58.356+09:00  INFO 5782 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    

OCI Cache with Redis를 캐쉬 서버로 연동하기

  1. dependency에 redis를 추가합니다.

    • Maven 기준 pom.xml에 다음 추가
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-redis</artifactId>
    		</dependency>	
    
  2. src/main/application.properties 파일을 생성하고 다음 항목을 추가합니다.

    • 아래 host, port는 개발환경을 기준으로 앞선 실습에서의 Bastion 서비스를 통해 포트포워딩으로 연동하는 예시입니다.
    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.ssl=true
    
  3. Redis 설정을 위한 Config 클래스 파일( src/main/java/com/example/caching/RedisConfig.java)을 만듭니다.

    • lettuce 라이브러리를 사용하는 예시입니다.
      • redis-cli에서 --tls 옵션을 사용한 것 처럼 lettuceClientConfigurationBuilder.useSsl().disablePeerVerification()을 꼭 추가합니다.
      • application.properties에 값을 사용하여 host, port 값과 ssl 설정여부 설정하는 예시입니다.
    package com.example.caching;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Bean;
    
    import org.springframework.beans.factory.annotation.Value;
    
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
    
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
    
    import org.springframework.data.redis.connection.RedisNode;
    
    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port}")
        private int port;
    
        @Value("${spring.redis.ssl}")
        private boolean ssl;
    
        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
            final RedisNode redisNode = RedisNode.newRedisNode()
                    .listeningAt(host, port)
                    .build();
    
            // Connecting as a Standalone Redis server
            final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
            redisStandaloneConfiguration.setHostName(host);
            redisStandaloneConfiguration.setPort(port);
    
            final LettuceClientConfiguration.LettuceClientConfigurationBuilder lettuceClientConfigurationBuilder =
                    LettuceClientConfiguration.builder();
    
            if (ssl) {
                lettuceClientConfigurationBuilder.useSsl().disablePeerVerification();
            }
    
            final LettuceClientConfiguration lettuceClientConfiguration = lettuceClientConfigurationBuilder.build();
    
            return new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration);
        }    
    
    }
    
  4. 캐쉬되는 데이터 모델( src/main/java/com/example/caching/Book.java)을 Serializable 하도록 설정합니다.

    package com.example.caching;
    
    import java.io.Serializable;
    
    public class Book implements Serializable {
    
    	...
    
    }
    
  5. 소스를 빌드하고 실행합니다.

    ./mvnw clean package
    java -jar target/caching-initial-0.0.1-SNAPSHOT.jar
    
  6. 처음 조회가 3초 가량 걸리고, 이후 2번은 빠르게 실행되는 것을 볼 수 있습니다. 결과는 앞선 기본 ConcurrentHashMap를 저장소로 사용할 때랑 동일합니다.

    2023-11-16T12:53:16.367+09:00  INFO 12783 --- [           main] com.example.caching.AppRunner            : .... Fetching books
    2023-11-16T12:53:20.008+09:00  INFO 12783 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T12:53:20.058+09:00  INFO 12783 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    2023-11-16T12:53:20.100+09:00  INFO 12783 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    
redis-cli로 Redis 클러스터에서 결과확인
  1. redis-cli로 포트 포워딩된 localhost, 6379 포트로 접속합니다.

  2. keys, get 명령으로 데이터가 들어간 것을 확인 할 수 있습니다.

    $ redis-cli --tls -h localhost
    localhost:6379> keys *
    1) "books::isbn-1234"
    localhost:6379> get books::isbn-1234
    "\xac\xed\x00\x05sr\x00\x18com.example.caching.Book\x81\xa5yv\x90|\xa3\xe8\x02\x00\x02L\x00\x04isbnt\x00\x12Ljava/lang/String;L\x00\x05titleq\x00~\x00\x01xpt\x00\tisbn-1234t\x00\tSome book"
    localhost:6379>
    
추가 테스트
  1. 추가 테스트를 위해 src/main/java/com/example/caching/AppRunner.java 파일을 수정합니다.

    package com.example.caching;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class AppRunner implements CommandLineRunner {
    
      private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);
    
      private final BookRepository bookRepository;
    
      public AppRunner(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
      }
    
      @Override
      public void run(String... args) throws Exception {
        logger.info(".... Fetching books");
        logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
        logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
        logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
        logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
      }
    
    }
    
  2. src/main/application.properties 파일에 다음 항목을 추가합니다.

    logging.level.org.springframework.cache=TRACE
    
  3. 다시 빌드하고 실행합니다.

    ./mvnw clean package
    java -jar target/caching-initial-0.0.1-SNAPSHOT.jar
    
  4. 실행 로그 확인

    • isbn-1234 조회: 두 번 모두 Cache entry for key 'isbn-1234' found in cache 'books' 와 같이 Cache Hit 되었습니다.
    • isbn-4567 조회:
      1. No cache entry for key 'isbn-4567' in cache(s) [books] - 첫번째는 Cache Miss
      2. Cache entry for key 'isbn-4567' found in cache 'books' - 두번째는 Cache Hit
    2023-11-16T13:06:42.373+09:00  INFO 34105 --- [           main] com.example.caching.AppRunner            : .... Fetching books
    2023-11-16T13:06:42.375+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Computed cache key 'isbn-1234' for operation Builder[public com.example.caching.Book com.example.caching.SimpleBookRepository.getByIsbn(java.lang.String)] caches=[books] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    2023-11-16T13:06:42.890+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Cache entry for key 'isbn-1234' found in cache 'books'
    2023-11-16T13:06:42.890+09:00  INFO 34105 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    
    2023-11-16T13:06:42.890+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Computed cache key 'isbn-4567' for operation Builder[public com.example.caching.Book com.example.caching.SimpleBookRepository.getByIsbn(java.lang.String)] caches=[books] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    2023-11-16T13:06:42.932+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : No cache entry for key 'isbn-4567' in cache(s) [books]
    2023-11-16T13:06:42.932+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Computed cache key 'isbn-4567' for operation Builder[public com.example.caching.Book com.example.caching.SimpleBookRepository.getByIsbn(java.lang.String)] caches=[books] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    2023-11-16T13:06:45.996+09:00  INFO 34105 --- [           main] com.example.caching.AppRunner            : isbn-4567 -->Book{isbn='isbn-4567', title='Some book'}
    
    2023-11-16T13:06:45.997+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Computed cache key 'isbn-1234' for operation Builder[public com.example.caching.Book com.example.caching.SimpleBookRepository.getByIsbn(java.lang.String)] caches=[books] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    2023-11-16T13:06:46.042+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Cache entry for key 'isbn-1234' found in cache 'books'
    2023-11-16T13:06:46.043+09:00  INFO 34105 --- [           main] com.example.caching.AppRunner            : isbn-1234 -->Book{isbn='isbn-1234', title='Some book'}
    
    2023-11-16T13:06:46.043+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Computed cache key 'isbn-4567' for operation Builder[public com.example.caching.Book com.example.caching.SimpleBookRepository.getByIsbn(java.lang.String)] caches=[books] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
    2023-11-16T13:06:46.088+09:00 TRACE 34105 --- [           main] o.s.cache.interceptor.CacheInterceptor   : Cache entry for key 'isbn-4567' found in cache 'books'
    2023-11-16T13:06:46.088+09:00  INFO 34105 --- [           main] com.example.caching.AppRunner            : isbn-4567 -->Book{isbn='isbn-4567', title='Some book'}
    
  5. redis-cli로 다시 조회해 보면 새로 추가한 isbn-4567도 들어가 있는 것을 볼 수 있습니다.

    $ redis-cli --tls -h localhost
    localhost:6379> keys *
    1) "books::isbn-4567"
    2) "books::isbn-1234"
    localhost:6379>
    
참고


이 글은 개인으로서, 개인의 시간을 할애하여 작성된 글입니다. 글의 내용에 오류가 있을 수 있으며, 글 속의 의견은 개인적인 의견입니다.

Last updated on 16 Nov 2023