본문 바로가기
Etc/Memo

대기열 Redis 이관 및 Cache Service 도입

by 코젼 2024. 8. 1.
728x90
반응형

목차

    대기열 Redis 이관

    Redis 설정

    @Configuration
    public class RedissonConfig {
    
        @Value("${spring.data.redis.host}")
        private String redisHost;
    
        @Value("${spring.data.redis.port}")
        private int redisPort;
    
        private static final String REDISSON_HOST_PREFIX = "redis://";
    
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
            return Redisson.create(config);
        }
    }
    

    Waiting Tokens

    • Sorted Set(value들의 순서 관리 가능) 자료구조로 저장되며, Key는 토큰을 Score는 요청시간을 Member에는 유저정보를 저장한다. 
    • 신규 대기열 추가, 대기 인원 계산, 활성화 할 토큰 목록
    public String getNewToken() {
    
        long timestamp = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        String token = UUID.randomUUID().toString();
    
        if (getWaitingTokens().contains(token)) {
            throw new CustomBadRequestException(WAITING_TOKEN_ALREADY_EXISTS, "이미 존재하는 토큰입니다.");
        }
    
        getWaitingTokens().add(timestamp, token);
        return token;
    }

    대기열 토큰 발급

    4000명의 테스트는 통과했지만 4500명의 테스트를 통과하지 못한 경우

    • 테스트 통과: 스레드들은 동일한 프로세스에서 실행되거나 다양한 프로세스에 걸쳐 실행될 수 있다. 시스템의 자원이 충분하고, 스레드의 생성 및 실행이 원활하게 이루어졌다.
    • 테스트 실패: 시스템의 자원 제한(메모리, CPU, 스레드 수 등)이 원인일 수 있습니다. 프로세스 수 제한이 영향을 줄 수 있다.

    현재 사용자로 실행할 수 있는 최대 프로세스 수가 2666개로 제한되어 있다.

    일시적/영구적으로 limit 를 변경할 수 있지만 변경하지 않고 처리 가능한 프로세스 내에서 테스트를 진행

     

    평균 소요 시간 계산 (10회 측정)

    • 유저 3,000명 일 경우
      • 소요 시간(ms) = 150229, 136956, 137193, 161753, 125391, 153194, 148325, 170981, 114695, 125471
      • 평균 소요 시간(s) = 138,808.9
    • 유저 4,000명 일 경우
      • 소요 시간(ms) = 147884, 116934, 281365, 171363, 136395, 229341, 187371, 83359, 118434, 160012
      • 평균 소요 시간(s) = 142.909.3

    second 단위로, 유저 1,000명 차이가 3초 차이나는 것을 볼 수 있다.

    • 3초 차이라면 1,000명을 더 대기열에 유입시키는 것이 좋은 방안이라고 생각한다.
    • 따라서, 토큰 발급에 대해서는 유저 4,000명으로 테스트를 진행

    3000명 통합 테스트 가능
    4000명 통합 테스트 가능

    EAGAIN 오류 발생: 시스템 자원(메모리 또는 스레드 수)가 부족하여 스레드를 생성하지 못한다.

    4500명 통합 테스트 실패

     

    현재 시스템에서 사용할 수 있는 메모리와 스레드 수를 확인한다.

    -u: processes: 현재 사용자에게 허용된 최대 프로세스 수

     

    프로세스와 스레드 수의 관계

    프로세스

    • 프로세스는 실행 중인 프로그램을 의미하며, 독립적인 메모리 공간과 자원을 가지고 있다.

    스레드

    • 프로세스 내에서 실행되는 작은 단위의 실행 흐름입니다. 하나의 프로세스는 여러 스레드를 가질 수 있다.
    • 스레드는 같은 프로세스 내에서 메모리와 자원을 공유한다.
      • 따라서 스레드가 생성될 때 프로세스와 관련된 메모리와 자원을 소모하지만, 독립적인 프로세스를 생성하는 것보다 훨씬 적은 자원을 소모한다.

    Active Tokens

    • Sets 자료구조로 저장되며, Key는 토큰을 “Member에는 유저정보와 만료일시 등 메타정보를 포함”한다.
    • 활성 토큰 대기열 추가, 예약 시 토큰이 있어야 하고, 예약 완료 시 토큰을 삭제한다.

    TTL

    활성화 시 10분 뒤 토큰이 만료되도록 TTL 을 설정한다.

    추후 MQ 를 활용해서 redis 가 TTL 처리를 통해 부하가 걸리지 않도록 고도화 할 예정이다.

     

    Active Tokens 전환 방식

    1. Active Tokens 에서 만료된 토큰의 수만큼 Waiting Tokens에서 전환
      • 서비스를 이용할 수 있는 유저를 항상 일정 수 이하로 유지할 수 있다.
      • 서비스를 이용하는 유저의 액션하는 속도에 따라 대기열의 전환시간이 불규칙하다.
    2. N초마다 M개의 토큰을 Active Tokens 으로 전환
      • 대기열 고객에게 서비스 진입 가능 시간을 대체로 보장할 수 있다.
      • 서비스를 이용하는 유저의 수가 보장될 수 없다.

    서비스에 먼저 접근했기에 절대적인 우선권을 주는 것이 아닌, 동시 접속자에 의한 시스템 부하를 막기 위해 트래픽을 제한하는 것이 목적이므로 2번을 채택한다.

     

    적절한 동시 접속자를 유지하기 위해서 N초마다 M개의 Active Tokens 으로 전환할 건지 고민할 필요가 있다.

    이전 로직에서는 임시 테스트를 위해 1분당 3명씩 입장이 가능하도록 테스트를 진행했었는데, 리팩토링을 진행하며 정확한 N, M의 값을 구하기 위해 여러 사항을 고려한다.

     

    1. 한 유저가 콘서트 조회를 시작한 이후에 하나의 예약을 완료할 때까지 걸리는 시간을 파악

    • 유스케이스
      • 유저 토큰 발급 및 대기열 진입 (스케줄러를 통해 입장 가능한 경우, 참가열(Active Tokens) 진입)
      • 콘서트 조회 및 날짜 조회
      • 예약 가능한 좌석 조회
      • 좌석 예약 시도
      • 대기열 제외, 유저가 콘서트 조회부터 예약까지 평균 1분이 소요된다.

    2. DB에 동시에 접근할 수 있는 트래픽의 최대치를 계산

    • 트랜잭션이 필요한 비즈니스 로직(콘서트 조회 ~ 예약)
      • 좌석 예약 시도
    • 토큰 발급 통합 테스트를 통해 4,500명부터 시스템의 자원 제한때문에 테스트에 실패하는 것을 확인할 수 있다.
    • 현재는 네트워크를 고려하지 않지만 추후 부하 테스트를 통해 정확한 트래픽의 최대치를 계산해야 한다.
    • 한 좌석에 동시에 4,000명이 예약을 시도한 경우 4,000 TPS 가 발생하지만, 평균적인 좌석 예약을 고려하여 초당 약 2,000 TPS 가 발생한다.
    • 2,000 * 60 = 1분당 12,000 TPS 를 수용할 수 있다.
    • 그 이상의 TPS 가 진행될 경우 병목 현상이 발생하고 서버가 종료되는 위험이 있다.

    3. 1분간 유저가 호출하는 API

    • 4개의 API 를 호출합니다.
      • 콘서트 목록 조회
      • 콘서트 날짜 조회
      • 예약 가능한 좌석 조회
      • 좌석 예약
    • 동시성 이슈에 의해 예약에 실패하는 경우, 유저가 예약 가능한 좌석 조회 API 재시도를 하기 때문에 1.5 번을 곱한다.
    • (4개의 API) * (재시도 1.5회) = 6

    4. 분당 처리할 수 있는 동시 접속자 수

    • 12,000(1분당 TPS) / 6(유저가 호출하는 API 개수) = 2,000 명씩 처리할 수 있다.
      • 따라서 M의 값은 2,000으로 설정할 수 있습니다.
    • 예약까지 평균 1분이 소요되므로, N의 값은 1/6 시간인 10초를 설정한다.
    • N = 10초마다 M = 2,000명씩 유효한 토큰으로 전환한다.
    @Value("${waitingToken.entryAmount:2000}")
    private int entryAmount;
    
    @Value("${waitingToken.processTime:10}")
    private long processTime;

    스케줄러에서 10초마다 토큰을 활성화 할 수 있도록 설정했다.

    @Component
    @RequiredArgsConstructor
    public class WaitingTokenScheduler {
    
        private final QueueService queueService;
    
        //매 10초마다 토큰 활성화
        @Scheduled(fixedRate = 10000)
        public void activateToken() {
            queueService.activateToken();
        }
    }
    • 나의 대기열 순번이 32,184 번이라면 잔여 예상 대기 시간은 2분 40초
      • 1 ~ 2000 : 입장
      • 2001 ~ 4000 : 10초 대기
      • 4001 ~ 6000 : 20초 대기
      • 6001 ~ 8000 : 30초 대기
      • 8001 ~ 10000 : 40초 대기
      • 10001 ~ 12000 : 50초 대기
      • 12001 ~ 14000 : 60초 대기
      • 14001 ~ 16000 : 70초 대기
      • ...{생략}
      • 32001 ~ 34000 : 2분 40초 대기 (순번이 32,184인 유저가 2분 40초 대기하고 들어갈 수 있다)
    • 방정식으로 잔여 예상 대기 시간을 구할 수 있다.
      • (대기열 순번 / 초당 처리량) * 대기열 처리 시간
      • ((32,184 / 2,000) * 10) = 160.92
      • 160초 = 2분 40초

    Lock

    Lock + Transaction 을 적용해 보았다.

    DB Transaction 과 Lock 의 범위에 따른 처리 고려를 할 경우, 이러한 순서로 진행된다.

    락 획득 -> 트랜잭션 시작 -> 트랜잭션 종료 -> 락 해제

    동일한 클래스에서 @Transactional 이 있는 메서드를 호출할 경우 트랜잭션이 적용되지 않는데,

    이는 AOP, 빈을 통해 호출되면 그 메서드가 동작하기 전에 빈이 클래스를 상속해서 프록시를 만들기 때문이다.

    프록시가 런타임에 트랜잭션을 읽어서 메서드를 오버라이드 해서 만든다.

    따라서 트랜잭션을 동작시키기 위해서는 프록시 빈을 사용해야 한다.

    간단하게 사용하기 위해서는 TransactionHandler 를 만들어서 컨슈머를 받고, 메서드를 호출한다.

     

    추후 AOP를 공부하고 적용해 볼 예정이다!

    LockHandler.interface - domain

    public interface LockHandler {
    
        Object runWithLock(String lockName, long waitTime, long leaseTime, TimeUnit timeUnit, Supplier<String> supplier) throws InterruptedException;
        void releaseLock(RLock lock);
    }

    RedisLockRepository.class - infrastructure

    @Component
    @RequiredArgsConstructor
    @Slf4j
    public class RedisLockRepository implements LockHandler {
    
        private final RedissonClient redissonClient;
        private final TransactionHandler transactionHandler;
    
        @Override
        public Object runWithLock(
                String lockName, long waitTime, long leaseTime, TimeUnit timeUnit, Supplier<String> supplier
        ) throws InterruptedException {
    
            RLock lock = redissonClient.getLock(lockName);
    
            try {
                boolean available = lock.tryLock(waitTime, leaseTime, timeUnit);
                if (!available) return false;
                return transactionHandler.runWithTransaction(supplier);
            } catch (InterruptedException e) {
                throw e;
            } finally {
                releaseLock(lock);
            }
        }
    
        @Override
        public void releaseLock(RLock lock) {
            if (lock != null && lock.isHeldByCurrentThread()) {
                try {
                    lock.unlock();
                } catch (IllegalMonitorStateException e) {
                    throw new IllegalMonitorStateException("Redis Lock Already UnLock");
                }
            }
        }
    }

    TransactionHandler.class - support

    @Component
    @RequiredArgsConstructor
    public class TransactionHandler {
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public <T> T runWithTransaction(Supplier<T> supplier) {
            return supplier.get();
        }
    }
    

     


    Cache

    DB 일관성을 유지하고 DB 부하(ex. 집계 함수)를 줄이기 위해 사용한다.

    Hit rate 를 높일 수 있는 방법을 목적으로 둔다.

     

    요구사항에 따라 효율적인 시스템을 구성하기 위해 캐싱 전략을 선택하는 경우도 있다.

    쿼리 파라미터를 기반으로 쿼리 결과를 캐싱할 수도 있다.

     

    DB 데이터 업데이트 시 꼭 초기화가 필수인가 고민할 필요가 있다.

    -> Eviction 전략, MQ, 적절한 TTL(expiration 전략) 등 도입 고려

     

    캐싱 적용 대상

    • 빈번한 접근
    • 데이터를 처리하기 위해 복잡한 로직이 필요한 데이터
    • 자주 변경되지 않는 데이터

    Server Caching

    • application level: 메모리 캐시
      • 애플리케이션의 메모리에 데이터를 저장해두고 같은 요청에 대해 데이터를 빠르게 접근해 반환함으로서 API 성능 향상 달성
      • 신속성 - 인스턴스의 메모리에 캐시 데이터를 저장하므로 속도가 가장 빠름
      • 저비용 - 인스턴스의 메모리에 캐시 데이터를 저장하므로 별도의 네트워크 비용이 발생하지 않음
      • 휘발성 - 애플리케이션이 종료될 때, 캐시 데이터는 삭제됨
      • 메모리 부족 - 활성화된 애플리케이션 인스턴스에 데이터를 올려 캐싱하는 방법이므로 메모리 부족으로 인해 비정상 종료로 이어질 수 있음
      • 분산 환경 문제 - 분산 환경에서 서로 다른 서버 인스턴스 간에 데이터 불일치 문제가 발생할 수 있음
    •  external level: 별도의 캐시 서비스
      • 별도의 캐시 Storage 혹은 이를 담당하는 API 서버를 통해 캐싱 환경 제공
      • e.g. Redis, Nginx 캐시, CDN, ..
      • 일관성 - 별도의 담당 서비스를 둠으로서 분산 환경 ( Multi - Instance ) 에서도 동일한 캐시 기능을 제공할 수 있음
      • 안정성 - 외부 캐시 서비스의 Disk 에 스냅샷을 저장하여 장애 발생 시 복구가 용이함
      • 고가용성 - 각 인스턴스에 의존하지 않으므로 분산 환경을 위한 HA 구성이 용이함
      • 고비용 - 네트워크 통신을 통해 외부의 캐시 서비스와 소통해야 하므로 네트워크 비용 또한 고려해야 함

    Cache Strategy

    DB 의 Connection 과 I/O 는 매우 높은 비용을 요구하며, 이는 트래픽이 많아질 수록 기하급수적으로 그 부하가 증가하는 특성을 가지고 있다.

    데이터 정합성을 유지하기 위해 각 트랜잭션은 원자적으로 수행되어야 하며 이는 요청이 증가할 수록 더 많은 딜레이가 생긴다는 것을 의미하기 때문이다.

    Termination Type

    • Expiration
      • 캐시 데이터의 유통기한을 두는 방법
      • Lifetime 이 지난 캐시 데이터의 경우, 삭제시키고 새로운 데이터를 사용가능하게 함
    • Eviction
      • 캐시 메모리 확보를 위해 캐시 데이터를 삭제
      • 명시적으로 캐시를 삭제시키는 기능
      • 특정 데이터가 Stale 해진 경우 ( 상한 경우 ) 기존 캐시를 삭제할 때도 사용

    콘서트 조회에 캐싱을 걸 경우, 콘서트를 새로 등록하면 캐시를 모두 refresh 해주어야 한다.

    이 부분에서 만료 전략이 중요해진다.

    변경 사항이 있을 경우 이전 캐시는 삭제해야 하기 때문에 Eviction 전략을 선택한다.

     

    Cache Configuration

    @Configuration
    @EnableCaching
    @RequiredArgsConstructor
    public class RedisCacheConfig {
    
        private final ObjectMapper objectMapper;
    
        @Value("${spring.data.redis.host}")
        private String redisHost;
    
        @Value("${spring.data.redis.port}")
        private int redisPort;
    
        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
            return new LettuceConnectionFactory(redisHost, redisPort);
        }
    
        @Bean
        public CacheManager redisCacheManager() {
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
                    .defaultCacheConfig()
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
                    .entryTtl(Duration.ofMinutes(3L));
    
            return RedisCacheManager.RedisCacheManagerBuilder
                    .fromConnectionFactory(redisConnectionFactory())
                    .cacheDefaults(redisCacheConfiguration)
                    .build();
        }
    }

     

    성능 개선 로직 분석

    API 목록

    유저가 가장 많이 조회하는 콘서트 목록 조회, 콘서트 날짜 조회 API 가 DB 조회 부하를 많이 일으킬 것으로 예상했다.

    Grafana K6 을 사용해서 캐싱 사용 전 후 성능 테스트를 진행했다.

    • /token
      • POST 대기열 토큰 등록
      • GET 토큰 사용 가능 여부 조회
    • /concert
      • GET /list 콘서트 목록 조회
      • GET /schedules/{concertId} 콘서트 날짜 조회
      • GET /seats/{concertScheduleId} 콘서트 좌석 조회
      • POST /seats/booking 콘서트 좌석 예약
      • POST /payment 결제
    • /user/amount
      • PATCH /charge/{userId} 유저 잔액 충전
      • GET /{userId} 유저 잔액 조회

    콘서트 목록 조회

    캐싱 적용 전

    1000명의 유저가 30초 동안 API 를 요청한 경우

    • 클라이언트가 서버로부터 받은 데이터 총량: 5.9MB, 평균 속도: 191 kB/s
    • 클라이언트가 서버로 전송한 데이터 총량: 2.7MB, 평균 속도: 87 kB/s
    • HTTP 요청의 전체 처리 시간(평균): 20.68 ms
    • 요청 실패 비율: 5%, 실패 횟수: 28484
    • 서버가 응답을 클라이언트에 전송하는 데 걸린 시간(평균): 165.77 µs
    • 클라이언트가 요청을 서버에 전송하는 데 걸린 시간(평균): 15.56 µs
    • 서버가 응답을 준비하는 데 걸린 시간(HTTP 요청 후 대기 시간): 20.46 ms
    • 전체 HTTP 요청 수: 29984 (초당 967,248896 건)
    • vus - 테스트 동안 활성화된 가상 사용자의 수(동시 테스트 수행): 59~1000명

     

    캐싱 적용 후

    1000명의 유저가 30초 동안 API 를 요청한 경우

    • 클라이언트가 서버로부터 받은 데이터 총량: 8.0MB, 평균 속도: 195 kB/s
    • 클라이언트가 서버로 전송한 데이터 총량: 2.3MB, 평균 속도: 57 kB/s
    • HTTP 요청의 전체 처리 시간(평균): 8.07 ms
    • 요청 실패 비율: 0%, 실패 횟수: 0
    • 서버가 응답을 클라이언트에 전송하는 데 걸린 시간(평균): 300 µs
    • 클라이언트가 요청을 서버에 전송하는 데 걸린 시간(평균): 282.81 µs
    • 서버가 응답을 준비하는 데 걸린 시간(HTTP 요청 후 대기 시간): 7.49 ms
    • 전체 HTTP 요청 수: 18481 (초당 450.291956 건)
    • vus - 테스트 동안 활성화된 가상 사용자의 수(동시 테스트 수행): 10~1000명

     

    비교

    캐싱 도입 후 CPU 부하가 감소했다.

    HTTP 요청의 평균 전체 처리 시간이 더 빨라졌고, 요청 실패 비율과 횟수가 현저히 감소했다.

    캐싱 사용 유무 X O
    클라이언트가 서버로부터 받은
    데이터 총량 및 평균 속도
    5.9MB
    191 kB/s
    8.0MB
    195 kB/s
    클라이언트가 서버로 전송한
    데이터 총량 및 평균 속도
    2.7MB
    87 kB/s
    2.3MB
    57 kB/s
    HTTP 요청의 전체 처리 시간(평균) 20.68 ms 8.07 ms
    요청 실패 비율 5% 0%
    실패 횟수 28484 0
    서버가 응답을 클라이언트에 
    전송하는 데 걸린 시간(평균)
    165.77 µs 300 µs
    클라이언트가 요청을 서버에 
    전송하는 데 걸린 시간(평균)
    15.56 µs 282.81 µs
    서버가 응답을 준비하는 데 
    걸린 시간(HTTP 요청 후 대기 시간)
    20.46 ms 7.49 ms
    전체 HTTP 요청 수 29984
    (초당 967,248896 건)
    18481
    (초당 450.291956 건)
    vus - 테스트 동안 활성화된 
    가상 사용자의 수(동시 테스트 수행)
    59~1000명 10~1000명

     


    콘서트 날짜 조회

    캐싱 적용 전

    1000명의 유저가 30초 동안 API 를 요청한 경우

    • 클라이언트가 서버로부터 받은 데이터 총량: 5.7MB, 평균 속도: 185 kB/s
    • 클라이언트가 서버로 전송한 데이터 총량: 3.9MB, 평균 속도: 127 kB/s
    • HTTP 요청의 전체 처리 시간(평균): 25.92 ms
    • 요청 실패 비율: 2.36%, 실패 횟수: 28911
    • 서버가 응답을 클라이언트에 전송하는 데 걸린 시간(평균): 168.14 µs
    • 클라이언트가 요청을 서버에 전송하는 데 걸린 시간(평균): 29.58 µs
    • 서버가 응답을 준비하는 데 걸린 시간(HTTP 요청 후 대기 시간): 25.72 ms
    • 전체 HTTP 요청 수: 29611 (초당 955.078662 건)
    • vus: 테스트 동안 활성화된 가상 사용자의 수(동시 테스트 수행): 57~1000명

     

    캐싱 적용 후

    1000명의 유저가 30초 동안 API 를 요청한 경우

    • 클라이언트가 서버로부터 받은 데이터 총량: 7.5MB, 평균 속도: 183 kB/s
    • 클라이언트가 서버로 전송한 데이터 총량: 2.5MB, 평균 속도: 61 kB/s
    • HTTP 요청의 전체 처리 시간(평균): 12.17 ms
    • 요청 실패 비율: 0%, 실패 횟수: 0
    • 서버가 응답을 클라이언트에 전송하는 데 걸린 시간(평균): 115.57 µs
    • 클라이언트가 요청을 서버에 전송하는 데 걸린 시간(평균): 105.24 µs
    • 서버가 응답을 준비하는 데 걸린 시간(HTTP 요청 후 대기 시간): 11.95 ms
    • 전체 HTTP 요청 수: 18883 (초당 457.70371 건)
    • vus: 테스트 동안 활성화된 가상 사용자의 수(동시 테스트 수행): 8~1000명

     

    비교

    캐싱 도입 후 CPU 부하가 감소했다.

    HTTP 요청의 평균 전체 처리 시간이 더 빨라졌고, 요청 실패 비율과 횟수가 현저히 감소했다.

    캐싱 사용 유무 X O
    클라이언트가 서버로부터 받은
    데이터 총량 및 평균 속도
    5.7MB
    185 kB/s
    7.5MB
    183 kB/s
    클라이언트가 서버로 전송한
    데이터 총량 및 평균 속도
    3.9MB
    127 kB/s
    2.5MB
    61 kB/s
    HTTP 요청의 전체 처리 시간(평균) 25.92 ms 12.17 ms
    요청 실패 비율 2.36% 0%
    실패 횟수 28911 0
    서버가 응답을 클라이언트에 
    전송하는 데 걸린 시간(평균)
    168.14 µs 115.57 µs
    클라이언트가 요청을 서버에 
    전송하는 데 걸린 시간(평균)
    29.58 µs 105.24 µs
    서버가 응답을 준비하는 데 
    걸린 시간(HTTP 요청 후 대기 시간)
    25.72 ms 11.95 ms
    전체 HTTP 요청 수 29611 
    (초당 955.078662 건)
    18883 
    (초당 457.70371 건)
    vus - 테스트 동안 활성화된 
    가상 사용자의 수(동시 테스트 수행)
    57~1000명 8~1000명


    More

    비즈니스 요구사항이 추가될 경우, 부하가 예상되는 경우를 생각해보았다.

     

    예시로, 최근 가장 인기 많은 콘서트를 조회하는 API 가 있다고 가정해보자.

    최근 3일 동안 진행된 콘서트 예약 건 수를 모두 뽑아서 콘서트 ID 를 group by 해서 예약이 취소되지 않은 데이터에 대해서 필터를 걸고... 등등 몇 가지만 적어도 벌써 쿼리가 굉장히 오래 걸릴 것으로 예상된다.

     

    캐싱을 도입하기 전에 쿼리가 잘 수행되기 위해서 인덱스를 먼저 건다.

    • 인덱스: 콘서트 ID, createdAt, status...

    만약 인덱스를 걸었음에도 불구하고 DB 부하가 계속 있고, 쿼리를 계속 날려야 할 때 캐싱 도입을 고려해볼 수 있다.

     

     

     

    728x90
    반응형

    댓글