ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SpringBoot] URL 이미지 반환하기(2) - Redis Caching
    개발/Java 2022. 5. 16. 15:55
     

    [SpringBoot] URL 이미지 반환하기

    스프링부트를 활용해서 API 서버를 만들면서 이미지가 담긴 URL을 반환해주는 기능이 필요했다. 클라이언트가 서버의 URL로 접근해서 이미지를 요청하면 외부 API의 정적 이미지를 담은 URL을 내 쪽

    www.floodnut.com

    앞서 구현한 URL 이미지를 캐싱하는 작업을 진행하고자 한다.

    우선 캐싱을 진행하고자 한 이유는...

    1. 이미지는 외부 API를 통해 요청된다. API 호출 횟수를 줄이기 위해서다.
    2. 반복 요청에 대해서 외부 API로의 이미지 요청의 응답 속도를 줄이기 위해서다.

    그렇다면 NoSQL인 Redis를 선택한 이유는?

    Key-Value를 통해 원하는 이미지만 빠르게 반환하기 위해서다.

    여러 이미지를 반환한다면 RDBMS가 더 유리할 수도 있겠지만 URL 당 하나의 이미지만을 반환하기에 Redis로 결정했다.

     

    사실 급하게 공부하면서 짜본거라 Redis에 대한 이해도가 그렇게 높지 않다.

    하지만 내가 경험한 것에 대해서 짧게 정리해보겠다.

     

    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port}")
        private int port;
    
        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
            return new LettuceConnectionFactory(host, port);
        }
    
    	/*
        @Bean
        public RedisTemplate<String, Object> redisTemplate() {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); 
            redisTemplate.setConnectionFactory(redisConnectionFactory());  
            redisTemplate.setKeySerializer(new StringRedisSerializer());   // Key: String 
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            return redisTemplate;
        }
        */
    }

    Redis 설정을 Bean에 등록해준다. 나는 Redis를 도커를 통해 구동했고 서버가 이에 접근할 포트와 호스트를 지정해줘야한다.

    Redis 설치 후 application.properties에 @Value 어노테이션을 통해 가져올 호스트:포트를 등록해준다.

     

    RedisTemplate을 사용할 경우 Redis에서 가져온 값을 직렬화/역직렬화하는 과정을 추가한다.

    나는 중간에 값을 확인할 필요 없이 바로 이미지만을 보여주기 때문에 RedisRepository로 바로 접근하도록 하였다.

     

    @RedisHash(value = "smsgeo", timeToLive = 3600)
    public class SMSImage {
        
        @Id
        private String key;
        private byte[] cachedImage;
    
        public SMSImage(String key, byte[] cachedImage){
            this.key = key;
            this.cachedImage = cachedImage;
        }
    
        public byte[] getCachedimage(){
            return this.cachedImage;
        }
    
        public String getKey(){
            return this.key;
        }
    }

    우선 DB에 접근할 자료구조 클래스인 SMSImage 모델이다.

    JPA의 Entity처럼 Redis에서는 RedisHash를 통해 엔터티를 지정해준다.

    이때, timeToLive를 통해 캐싱되는 값의 만료 시간을 초 단위로 지정해줄 수 있다.

     

    여기서 사실 조금 삽질을 했다. 이전 포스팅을 보면 반환되는 이미지는 ByteArray 타입이다.

    하지만 최초에 내가 지정한 캐싱 이미지의 타입은 Image 였다.

    Redis를 통해 가져온 캐싱 이미지는 BufferedImage 로 반환되는데 여기서 생성자 문제가 터져서 어떻게 처리할까 고민하다가 ByteArray로 아예 캐싱되는 값의 형태를 바꿔버렸다.

     

    @Repository
    public interface SMSImageRedisRepository extends CrudRepository<SMSImage, String>{
    
    }
    
    // 메소드 구현
    @Repository
    public class SMSImageRedisImpl extends SMSImageRedisRepository {
    	/* 메소드 구현 */
    }

    CrudRepository를 상속받는 레포지토리를 생성한다. 특정 메소드를 오버라이딩하여 사용하고 싶다면 별도의 구현체를 지정하면 된다.

     

        @Autowired
        private SMSImageRedisRepository smsRedisRepo;
        
        
        // ...
        
        
        public @ResponseBody byte[] getImage(/* 파라미터 */){
            try{
                String key = hashing.encrypt(__string__);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
    
                /* Redis Retrieve */
                Optional<SMSImage> opt = smsRedisRepo.findById(key);
                SMSImage cachedImage = opt.get();
    
                return cachedImage.getCachedimage();
            }catch(NullPointerException ne){
                String key = hashing.encrypt(__string__);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                SmsImageAPI sms = new SmsImageAPI();
                BufferedImage bi = sms.getAPIImage();
                Image image = bi;
                ImageIO.write(bi, "png", baos);
    
                /* Redis Caching */
                SMSImage cache = new SMSImage(key,  baos.toByteArray());
                smsRedisRepo.save(cache);
    
                /* Redis Value Return */
                return baos.toByteArray();
            }
            
            //...

    위 구조를 보자. 조금 난잡하게 짜긴 했지만 간단하게 설명하도록 하겠다.

     

    우선 키로 사용하기 위한 sha256 해싱 문자열을 생성한다.

    이 키를 가지고 Redis에 접근하여 이미 캐싱되어 있는지를 확인한다.

    여기서 Optional이 사용된다. 캐싱되어 있지 않는 키:값 이라면 Null을 반환할 것이고 이에 접근한다면 Null 참조 오류를 뿜을 것이다.

     

    만약 Null 참조 오류가 발생한다면 외부 API에 접근하여 이미지를 전달받고 이를 ByteArray로 Redis에 저장한다.

    이미 캐싱되어 있다면 Optional 객체에서 Image 객체를 가져와 저장된 ByteArray를 반환한다.

     

    자바와 스프링부트를 공부하면서 느꼈던 점은 정말 이 생태계가 대단하다는 점 이었다.

    물론 부족한 부분을 메꾸기 위한 점도 있겠지만 모든 언어와 프레임워크는 단점이 있기 마련이다.

    자바 스트림과 같이 람다식을 활용한 기술도 필수적이라고 느꼈지만 이번 Redis를 통해서 Optional도 새롭게 알게 되었다.

     

    아직 내부적인 원리를 완벽하게 이해하진 못했지만 차근차근 알아가보고 싶다.

     

    댓글

Designed by Tistory.