ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 서비스 속도를 높이는 redis의 캐시레이어
    대용량 데이터 & 트래픽/대용량 처리를 위한 redis 2024. 3. 10. 21:42

    데이터를 계속 db에 접근해서 가져오게 된다면 많은 부하가 걸리기 때문에 서비스의 성능이 안 좋을 것이다.

     

    Redis의 Caching은 CPU의 메모리에 저장을 해뒀다가 사용하는 기술이며 대표적인 구현 방식은 캐시에 데이터가 없으면 DB에서 데이터를 가져오고, 데이터가 있다면 그냥 사용하는 Cache-Aside(Lazy Loading)이다.

    사진 : 패스트캠퍼스 - 백엔드 개발자를 위한 한 번에 끝내는 대용량 데이터 & 트래픽 처리

     

     

     

    UserService

    @Service
    public class UserService {
    
        @Autowired
        private ExternalApiService externalApiService;
        // 이게 외부 서비스라 가정 (여기서는 외부 db)
    
        public UserProfile getUserProfile(String userId) {
    
            String userName = externalApiService.getUserName(userId);
            int userAge = externalApiService.getUserAge(userId);
    
            return new UserProfile(userName, userAge);
        }
    }

     

     

    ExternalApiService

    @Service
    public class ExternalApiService {
    
        public String getUserName(String userId) {
            // 여기서 외부 서비스나 DB를 호출한다고 가정
    
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
    
            System.out.println("Getting user name from other service!");
    
            if (userId.equals("A")) {
                return "Adam";
            }
            if (userId.equals("B")) {
                return "Bob";
            }
    
            return "";
        }
    
        public int getUserAge(String userId) {
            // 여기서 외부 서비스나 DB를 호출한다고 가정
    
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
    
            System.out.println("Getting user age from other service!");
    
            if (userId.equals("A")) {
                return 28;
            }
            if (userId.equals("B")) {
                return 32;
            }
    
            return 0;
        }
    }

    실제로 0.5초가 걸리지는 않지만 500ms로 가정을 했다.

     

     

    캐시를 사용하지 않는다면 위와 같이 500ms씩 걸리게 된다.

     

     

     

     

    postman으로 요청을 해본 결과 1042ms가 나왔다.

     

    실제로는 정확히 0.5초씩 소모가 되지는 않지만 db에서 데이터를 매번 가져오는 것은 확실히 resource가 소모되는 일이다.

     

     

     

    UserService 수정

    @Service
    public class UserService {
    
        @Autowired
        private ExternalApiService externalApiService;
        // 이게 외부 서비스라 가정 (여기서는 외부 db)
    
        @Autowired
        StringRedisTemplate redisTemplate;
    
        public UserProfile getUserProfile(String userId) {
    
            String userName = null;
    
            ValueOperations<String, String> ops = redisTemplate.opsForValue();
            String cachedName = ops.get("nameKey:" + userId);
            if (cachedName != null) {
                userName = cachedName;
            }
            else {
                userName = externalApiService.getUserName(userId);
                // redis의 cache에 없으면 외부 db에서 데이터를 가져오기
                ops.set("nameKey:" + userId, userName, 5, TimeUnit.SECONDS);
                // 그리고 5초동안 저장
            }
    
            // String userName = externalApiService.getUserName(userId);
            int userAge = externalApiService.getUserAge(userId);
    
            return new UserProfile(userName, userAge);
        }
    }

    이번에는 userName을 externalApiService에서 가져오는 것이 아니라 redisTemplate을 통해서 cahedName이 있다면 userName에 cachedName을 사용하고, 만약에 없다면 외부 db인 externalApiService에서 가져온 다음에 ops.set을 통해 redis에 저장하는 방식으로 수정했다.

     

     

    처음에는 캐시에 저장된 것이 없기 때문에 똑같이 시간이 좀 걸렸지만 이후에 5초 안에 요청을 또 했을 때에는 531ms가 소요되었다. 즉, userName과 userAge 중에서 userName은 캐시에 저장되었기 때문에 0.5초 동안의 sleep이 걸리지 않은 것이다.

     

    콘솔을 봐도 처음에는 name과 age를 externalApiService에서 호출해서 로그가 찍혔지만 두 번째 호출에서는 name을 redis의 cache에서 가져왔기 때문에 age만 externalApiService에서 호출했다는 로그가 찍혔다.

     

     

    5초 이후에 캐시가 없어지고 다시 api 호출을 하면 1초가 넘게 걸린다.

     

     

     

    Spring의 캐시 추상화

    스프링에서는 CacheManager를 통해 일반적인 캐시 인터페이스를 구현 가능하다.

     

    @Cacheable 어노테이션을 사용하면 메서드에 캐시를 적용해서 Cache-Aside 패턴을 수행한다.

    spring:
      cache:
        type: redis
      data:
        redis:
          host: localhost
          port: 6379

    application.yml에 spring-cache-type : redis를 추가해준다.

     

     

    @EnableCaching
    @SpringBootApplication
    public class RedisCachingApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(RedisCachingApplication.class, args);
    	}
    }

    Main 클래스에도 @EnableCaching 어노테이션을 추가해서 캐시를 사용한다는 것을 알린다.

     

     

     

    @Cacheable(cacheNames = "userAgeCache", key = "#userId")
    public int getUserAge(String userId) {
        // 여기서 외부 서비스나 DB를 호출한다고 가정
    
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
    
        System.out.println("Getting user age from other service!");
    
        if (userId.equals("A")) {
            return 28;
        }
        if (userId.equals("B")) {
            return 32;
        }
    
            return 0;
    }

    다른건 변경하지 않고 ExternalApiService에서 getUserAge 메서드에 @Cacheable 어노테이션을 붙였다. cacheNames는 임의로 설정하면 되고, key는 userId를 사용한다.

     

     

     

    실행을 해보면 첫 번째 호출에서는 캐시가 없기 때문에 1초가 넘게 걸리지만 이후에는 name과 age 둘 다 캐시가 있기 때문에 15ms라는 아주 적은 시간이 소요되었다.

     

     

     

    RedisCacheConfig

    @Configuration
    public class RedisCacheConfig {
    
        @Bean
        public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
            RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                    .disableCachingNullValues()
                    .entryTtl(Duration.ofSeconds(10))   // 기본 TTL
                    .computePrefixWith(CacheKeyPrefix.simple())
                    .serializeKeysWith(
                            RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
                    );
    
            HashMap<String, RedisCacheConfiguration> configMap = new HashMap<>();
            configMap.put("userAgeCache", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(5)));  // 특정 캐시에 대한 TTL
    
            return RedisCacheManager
                    .RedisCacheManagerBuilder
                    .fromConnectionFactory(connectionFactory)
                    .cacheDefaults(configuration)
                    .withInitialCacheConfigurations(configMap)
                    .build();
        }
    }

    위와 같이 어노테이션을 사용했을 때에도 Redis의 설정을 바꿀 수 있다. 중요한 것들을 살펴보면 RedisCacheConfiguration을 만들고 entryTtl을 사용해서 기본 TTL을 지정한다. 또한 HashMap에서 userAgeCache의 TTL을 5초로 지정할 수 있다.

     

    RedisCacheManager를 반환할 때 cacheDefaults에 default인 configuration 객체를 넣었고, configMap으로 원하는 캐시값들을 custom했다.

     

     

    위의 configuration에서의 설정값이 적용되어서 age의 캐시도 5초의 TTL을 가진다.

     

     

    1. username -> redis를 직접 구현
    2. userage -> spring의 @Cacheable 어노테이션을 이용해서 구현

     

     

    위의 2가지 방법에서 2번째를 사용하는 것이 좋다.

     

    그 이유는 redisTemplate을 직접 사용해서 null을 검사하고 set, get을 하는 로직은 getUserProfile의 온전한 기능에 redis가 추가되었기 때문에 응집도가 떨어진다고 할 수 있다.

     

    그렇기 때문에 spring에서 제공하는 @Cacheable 어노테이션을 이용해서 구현하는 것이 좋다.

Designed by Tistory.