Redis를 활용한 성능 최적화 방안 - leonroars/slam GitHub Wiki

Redis를 활용한 성능 최적화 방안

목차

Part I : Redis 기반 Caching 활용 성능 최적화

  1. Caching & Cache : 소개

  2. Caching Design Patterns

  3. Server-side 에서의 Caching : 필요성과 발생 가능 문제

  4. Cache의 조건 : 왜 Redis는 Cache로서 적절한가?

  5. Cache 대상 선정 : 무엇을 Caching 할 것인가?

  6. 현 프로젝트 내 Caching 적용 지점 분석

Part II : Redis 자료구조를 활용한 성능 최적화

  1. Redis 자료 구조 소개.

  2. Redis 자료 구조 도입의 유효성 고찰

  3. Redis 자료 구조 활용 : 대기열 로직 이관




Part I : Redis 기반 Caching 활용 성능 최적화

1. Caching & Cache : 소개

캐시 디자인 패턴, 실제 프로젝트에의 적용과 같은 실용적인 내용을 다루기 전, 캐시의 개념과 작동 방식에 관해 간단히 짚고 넘어가보겠습니다.


Caching 과 Cache

캐싱(Caching) 은 어떤 요청에 대해 미리 계산 혹은 저장한 결과를 반환하여 해당 요청에 대한 응답 속도 향상 혹은 연산 속도 향상을 기대하는 성능 최적화 기술 혹은 전략 을 의미합니다.

즉, 캐싱은 크게 다음의 두 가지 방안을 통해 처리 효율 혹은 응답 효율을 증대시킨다고 할 수 있겠습니다.

  1. 연산 생략으로부터 발생하는 효율 : 명령어 혹은 연산 결과를 미리 저장하고, 같은 명령어/연산 호출 시 이를 반환함으로써 연산 횟수 자체를 줄일 수 있습니다.

  2. 읽기/쓰기의 시간 비용 절감으로부터 발생하는 효율 : 물리 저장 장치에서 불러와야하는 정보를 메모리에 저장해두고 이를 응답에 활용한다면 빠르게 작업 수행 가능.

그리고 이러한 전략의 수행 주체, 즉 미리 계산한 결과 혹은 데이터를 저장해두는 물리적/논리적 장치를 캐시(Cache) 라고 합니다.

캐시 역할을 수행하는 물리적 장치의 대표적인 예로는 많은 분들이 잘 알고 계시는 L1, L2 Cache 가 있습니다.

캐시로 활용되는 논리적 장치로 유명한 것들로는 Redis, memcached 가 있습니다.


Cache 과 Caching 의 효율성 평가

캐싱과 캐시의 정의만 두고 생각해보았을 때는 무조건 적용하는 쪽이 유리할 것만 같습니다.

하지만 엉뚱하게 사용해버린다면 여러가지 문제가 발생할 수 있습니다.

이 문제에 관한 고민은 나중에 다루어볼 것이므로, 우선 '캐싱/캐시를 잘 쓴다는 것이 무엇인가'에 대한 고민을 먼저 해보도록 하겠습니다.

Cache 의 효율성에 가장 중요한 점은 바로 캐시 역할을 하는 물리/논리 장치에 대한 읽기/쓰기가 영속성 저장장치 읽기/쓰기보다 빨라야 한다는 것입니다.

그렇지 않다면, 같거나 유사한 Round Trip을 감수하면서까지 열심히 캐시로 복사해오고 읽고 이를 갱신하는 것의 효용성이 유명무실해지기 때문입니다.

일반적으로 우리는 영속성 저장장치로 관계형 데이터베이스 서버를 활용합니다.

저장 공간의 크기가 중요하다는 특수성으로 인해 여전히 자기 저장 장치 기반으로 구성되는 경우가 흔한 현 상황에서 이보다 확정적으로 빠른 읽기 및 쓰기 연산 속도를 가진 것은 SSD, RAM과 같은 on-board chip 형태의 저장장치를 가장 먼저 생각해볼 수 있습니다.

그렇기 때문에 이러한 물리 장치에 근간을 두고 논리적으로 저장소의 개념을 구현하여 제공하는 memcached 혹은 Redis, 심지어 서버 애플리케이션이 구동하는 컴퓨팅 자원의 메모리(Java Spring Framwork 진영의 Caffeine Cache) 와 같은 것들이 논리적 장치 캐시로서 널리 활용되는 것입니다.


그렇다면 캐싱의 효율성은 어떻게 평가할까요?

캐싱 업계(?)에는 두 가지 유명한 용어가 있습니다. 바로 Cache HitCache Miss 가 있습니다.

Cache Hit 은 요청에 대해 반환해줘야하는 데이터가 캐시에 존재하는 경우, Cache Miss 는 없는 경우를 지칭합니다.

만약 Cache Miss 가 난 경우엔 어떻게 해야할까요? 당연히 DB에서 해당 데이터를 가져와 반환해줘야합니다.

그러므로 Cache Miss 가 발생하는 상황을 최소화해야 합니다.

이를 Cache Hit Ratio가 높다 고 표현합니다.

Cache Hit Ratio 는 다음과 같이 산출되기 때문입니다.

$$ Cache Hit Ratio = \frac{Number Of Cache Hit}{Number Of Cache Hit + Number Of Cache Miss} $$

바로 이 Cache Hit Ratio 가 대표적인 캐싱의 효율성 평가 척도 입니다.

쉽게 말해 '자주 사용할 확률이 높은 데이터를 적절하게 선택하여 이를 미리 저장해두는 것'이 효율적인 캐싱이라는 것입니다.

그렇다면 이 '자주 사용할 확률이 높은 데이터'라는 것을 우리는 어떻게 알 수 있을까요?

바로 참조 지역성(Locality of Reference) 라는 개념을 활용합니다.


참조 지역성(Locality of Reference) & 캐시 교체 정책(Cache Replacement Policy) : 캐시에 담을 정보를 정해보자!

앞서 함께 살펴보았듯, '캐싱'의 효율은 참조 지역성과 깊은 관계를 갖고 있습니다.

참조 지역성

참조 지역성 이란, 애플리케이션에서 특정 데이터에 대한 접근이 시간적 또는 공간적으로 집중되어 발생하는 경향을 말합니다. 이때 이 참조 지역성은 크게 시간과 공간 관점에서 다시 구분하여 생각해볼 수 있습니다.

  • 시간적 지역성(Temporal Locality) : 최근에 접근한 데이터는 가까운 미래에도 다시 접근될 가능성이 높은 경향을 보임.
  • 공간적 지역성(Spatial Locality) : 한 위치의 데이터에 접근하면 그 인근의 데이터에도 접근할 가능성이 높은 경향을 보임.

이러한 참조 지역성을 고려한다면 우리는 '무엇이 자주 사용될 확률이 높은가?' 에 관한 질문에 보다 높은 정확도를 갖는 답을 찾을 수 있을 것이고, 그렇기 때문에 캐싱의 효율성과도 직결되는 것입니다.

하지만 이것만으로는 충분하지 않습니다. 참조 지역성은 관찰 시간 혹은 명령 실행 수와 같은 범위가 길어질 수록 조금씩 그 경향이 옅어지기 때문입니다.

가령, 방금 메모리 주소 A 위치의 데이터를 참조하였다면 근 시간 내에 A+1 위치 또한 참조할 확률이 높은 것은 사실이나 3 영업일 이후에도 A+1 위치를 참조할 확률이 같을 것을 의미하는 것이 아니라는 것입니다.

이는 어떤 데이터의 캐싱 가치가 '절대적이지 않음'을 의미합니다.

캐시 내 어떤 데이터는 점점 캐싱 가치가 떨어질 것이고, 어떤 것은 그 가치를 유지할 수도 있는 것입니다.

이를 관찰하여 우리는 '캐시 내 존재하는 데이터를 교체하는 방안' 또한 고려해야합니다.

이러한 관찰을 바탕으로 어떤 데이터를 캐시에 유지할 지, 만약 캐시에서 퇴출시킨다면 그 자리에 어떤 데이터를 캐싱하여 저장할 것인지 결정해야 합니다.

이 의사결정의 정확도가 곧 캐싱의 효율성으로 이어지게 되는 것입니다.

캐시 교체 정책(Cache Replacement Policy)

앞서 설명한 참조 지역성 이 어떤 데이터의 캐시 가치를 결정하는데에 주요한 개념이라면, 캐시 교체 정책은 반대로 '어떤 데이터를 캐시에서 제거할 것인가'에 보다 주요한 개념입니다. 다양한 캐시 교체 정책이 존재하지만 그 중 가장 대표적이고 널리 활용되는 것을 소개해보려합니다.

FIFO(First-In-First-Out)

  • 캐시에 삽입된 순서를 기준으로 퇴출 대상을 결정합니다. 가장 먼저 저장된 데이터를 가장 먼제 제거합니다.

LFU(Least Frequently Used)

  • 가장 참조 빈도 수가 낮은 데이터를 제거합니다. 지금까지 많이 참조되지 않았기 때문에 앞으로도 참조될 가능성이 적다고 판단합니다.
  • 다만, Cache Hit 빈도를 모든 데이터에 대해 측정하고 관리해야 한다는 점에서 FIFO 에 비해 추가적인 저장공간 혹은 연산이 필요할 수 있습니다.

LRU(Least Recently Used)

  • 마지막 참조 시점으로부터 가장 시간이 많이 지난 데이터를 제거합니다. 이는 _시간 지역성_을 고려한 것이라고 이해할 수 있습니다.
  • 다만, 마지막 Cache Hit 시점으로부터 지금까지 흐른 시간을 모든 데이터에 대해 측정하고 관리해야 한다는 점에서 FIFO 에 비해 추가적인 저장공간 혹은 연산이 필요할 수 있습니다.



2. Cache Design Pattern

캐시 디자인 패턴은 '시스템이 캐시를 활용하는 방법'을 패턴화하고 정의한 것입니다. 아래에선 대표적인 4가지 캐시 디자인 패턴을 소개할 예정이며, 각 디자인 패턴 별 구조적 특성이 존재하고 이에 따른 장/단점과 적합한 Use-case가 존재하므로 고려하여 사용하는 것이 좋습니다.

Cache Aside Pattern:

  1. 애플리케이션이 먼저 캐시를 조회합니다.
  2. Cache Hit인 경우 바로 캐시에서 응답을 반환하고, Cache Miss이면 DB에서 데이터를 가져와 캐시에 저장한 후 응답합니다.
  • 그렇기 때문에 조회 요청이 많은 데이터를 잘 선정하여 캐싱할 경우 유의미한 응답 속도 개선과 부하 절감을 실현할 수 있습니다.

Read-Through Pattern:

  • 애플리케이션이 캐시를 조회하는데, 캐시가 없다면 캐시가 자동으로 DB에서 데이터를 로드하고 저장하여 응답합니다.
  • Application 수준 에서의 구현과 Infrastructure 수준에서의 구현이 함께 이루어져야 합니다.
  • 데이터 정합성 유지에 유리한 디자인 패턴 입니다.

Write-Through Pattern:

  1. 애플리케이션이 업데이트 요청을 하면, 캐시에 먼저 데이터를 기록한 후 동기적으로 DB에도 업데이트를 수행합니다.
  2. 모든 업데이트가 완료된 후 응답을 반환합니다.
  • Write-Behind Pattern:
  1. 애플리케이션이 업데이트 요청을 하면, 캐시에 즉시 업데이트를 적용하고 곧바로 응답을 반환합니다.
  2. 동시에 업데이트 작업이 비동기적으로 DB 업데이트 큐에 저장되고, 백그라운드에서 DB에 반영됩니다.
  3. Read-through 와 마찬가지로 Application 수준 에서의 구현과 Infrastructure 수준에서의 구현이 함께 이루어져야 합니다.
flowchart TD
    %% Cache Aside Pattern
    subgraph "Cache Aside Pattern"
        A1[애플리케이션 요청]
        A1 --> B1[캐시 조회]
        B1 -- Hit --> C1[캐시 응답 반환]
        B1 -- Miss --> D1[DB 조회]
        D1 --> E1[DB 결과 캐시에 저장]
        E1 --> C1
    end

    %% Read-Through Pattern
    subgraph "Read-Through Pattern"
        A2[애플리케이션 요청]
        A2 --> B2[캐시 조회]
        B2 -- Miss --> C2[캐시 자동 DB 조회 & 저장]
        C2 --> D2[캐시 응답 반환]
        B2 -- Hit --> D2
    end

    %% Write-Through Pattern
    subgraph "Write-Through Pattern"
        A3[애플리케이션 업데이트 요청]
        A3 --> B3[캐시에 업데이트]
        B3 --> C3[동기적으로 DB 업데이트]
        C3 --> D3[업데이트 완료 후 응답 반환]
    end

    %% Write-Behind Pattern
    subgraph "Write-Behind Pattern"
        A4[애플리케이션 업데이트 요청]
        A4 --> B4[캐시에 업데이트]
        B4 --> C4[즉시 응답 반환]
        B4 --> D4[비동기 DB 업데이트 큐에 저장]
        D4 --> E4[백그라운드에서 DB 업데이트 수행]
    end
Loading

3. Server-side 에서의 Caching : 필요성과 발생 가능 문제

서버 측 캐싱은 애플리케이션의 성능 향상에 큰 도움이 됩니다.

필요성:

  • 데이터 조회 속도를 향상시키고, DB 부하를 줄입니다. 이는 서비스 안정성과 직결됩니다.
  • Cache Hit Ratio 가 높도록 캐싱을 구현할 경우, 응답 시간 단축과 함께 시스템의 전체 처리량이 늘어납니다. 즉 Capacity 향상이 가능합니다.

발생 가능 문제:

  • Cache Invalidation: 데이터 업데이트 시 캐시의 일관성을 유지하기 어려울 수 있습니다. 캐시 일관성 유지를 위해 필연적으로 DB Hit이 필요하고 이러한 로직이 또 다른 부하가 되지 않도록 세밀하게 구현할 필요가 있습니다.
  • Stale Data: 오래된 데이터가 캐시에 남아 있을 경우, 사용자에게 부정확한 정보를 제공할 수 있습니다.
  • Cache Miss 증가: 잘못된 캐시 전략은 오히려 DB 부하를 증가시킬 수 있습니다.

마지막 부분에서 기술한 'Cache Miss 증가'의 극단적인 예로서, 많은 요청에 의해 읽기가 발생하는 특정 데이터들의 만료 시점이 모두 동일하여 만료가 되어버리고 캐싱이 되지 않았을 때, 이 모든 요청들로부터 발생한 부하가 곧장 DB로 가해지게 되는 상황을 생각해볼 수 있습니다. 이를 캐시 스탬피드(Cache Stamped) 혹은 Thundering Herds 라고 합니다.

이를 해결하기 위해, 캐시에 적재되는 데이터들의 만료가 동시에 이루어지지 않도록 만료 시점 등록 시 이들이 설정한 범위 이내에서 무작위로 설정될 수 있도록(이를 Jitter 라고 합니다.) 하는 방안이 있습니다.

4. Cache 의 조건 : 왜 Redis는 Cache로서 적절한가?

Redis가 캐시로 적합한 이유는 다음과 같습니다.

  • 메모리 기반 저장소: 빠른 읽기/쓰기가 가능하며, 낮은 지연시간을 보장합니다. 이는 앞서 기술했던 '캐시의 효율성 평가' 내용과 부합합니다.
  • 다양한 데이터 구조 지원: Strings, Hashes, Lists, Sets, Sorted Sets 등 여러 자료구조를 제공하여 다양한 요구사항을 효율적으로 만족시킬 수 있습니다.
  • TTL 지원: Redis는 키별로 TTL(Time-To-Live)을 설정할 수 있어, 자동으로 만료 처리할 수 있습니다.
  • 확장성: 분산 환경에서 Redis 클러스터 구성이 용이하며, 고가용성을 보장합니다. CAP Theorem 과 같은 법칙이 성립하는 데이터베이스보다 상대적으로 확장이 수월하므로 장애 상황 혹은 서비스 운영 환경의 다양성에 대응하기 용이합니다.

5. Cache 대상 선정 : 무엇을 Caching 할 것인가?

캐싱 대상 선정 시엔 캐시 히트율과 데이터 변경 빈도를 고려하는 것이 일반적입니다. 이는 앞서 기술한 '캐시의 효율성 평가' 와 '캐싱의 효율성 평가'에서 다루었던 내용과 상통합니다.

하지만 이를 보다 실용적으로, 실제 프로젝트를 떠올리며 캐싱 가치가 충분한 데이터를 선정해본다면 다음과 같은 특징을 갖는 데이터로 묶어 생각해볼 수 있습니다.

  • 자주 조회되지만 자주 변경되지 않는 데이터: 예를 들어, 읽기 전용 설정 값, 참조 데이터 등
  • 연산 비용이 큰 결과: 복잡한 연산 결과나 DB 집계 결과 등을 캐싱하여 재계산 비용을 줄일 수 있음
  • 사용자별 맞춤 데이터: 세션, 프로파일 등의 데이터는 캐시를 통해 빠른 응답이 가능

6. 현 프로젝트 내 Caching 적용 지점 분석

우리 프로젝트에서는 다음과 같은 부분에 캐싱을 적용할 수 있습니다.

  • 콘서트 및 공연 일정 조회: 자주 변하지 않는 콘서트 정보나 일정 정보를 캐시하여 DB 부하를 줄일 수 있습니다.
  • 좌석 예약 상태: 실시간으로 변화하는 좌석 예약 상태는 캐시 갱신 주기가 짧아야 하므로, 주의가 필요합니다. (예: 빠른 invalidation 또는 실시간 동기화)
  • 대기열 토큰: 대기열의 토큰 정보 또한 자주 읽히는 데이터로, Redis의 빠른 읽기 성능을 활용할 수 있습니다.

Part II : Redis 자료구조를 활용한 성능 최적화

1. Redis 자료 구조 소개.

Redis는 다양한 자료구조를 지원하여, 상황에 맞게 최적의 성능을 발휘할 수 있습니다. 대표적인 자료구조는 다음과 같습니다.

  • String: 단일 값 저장. 간단한 캐시나 카운터에 적합.
  • Hash: 키-값 쌍의 집합으로, 객체 형태의 데이터를 저장하는 데 유용.
  • List: 순서가 있는 값들의 모음으로, 큐나 스택 구현에 적합.
  • Set: 중복 없는 값들의 집합으로, 멤버십 검사나 집합 연산에 적합.
  • Sorted Set (ZSet): 각 요소에 점수를 부여하여 정렬된 상태로 저장할 수 있으며, 랭킹, 시간순 정렬 등에 적합합니다.
  • 내부적으로 확률적 Skip List 방식으로 구현됩니다.
  • 삽입과 탐색 모두 O(log N) 시간 복잡도를 가집니다.

2. Redis 자료 구조 도입의 유효성 고찰

Redis가 효율적인 자료 구조를 도입하지만 활용을 결정하기에 앞서 Redis 와의 통신으로부터 발생하는 비용 또한 반드시 고려해야 합니다. 이러한 관점에서 생각해볼 때 현행 프로젝트에서 Redis 의 자료구조 활용하는 것이 효율적인 것은 바로 '대기열' 구현 로직입니다.

그 근거를 크게 세 가지 관점에서 기술해보도록 하겠습니다.

서비스 및 설계 안정성 측면 DB는 영속성 저장 장치/계층 입니다. 비즈니스적으로 재사용가치가 있는 정보를 안정적으로 저장하고 재활용할 수 있도록 비휘발성 저장 매체 기반으로 구동된다는 특징이 있습니다.

하지만 대기열이라는 개념은 핵심 도메인와 어느 정도 거리가 있다고 판단했습니다.

대기열은 핵심 도메인이 '안정적'으로 이루어질 수 있도록 돕는 보조적인 설계라고 생각했기 때문입니다.

이러한 관점에서 본다면, 보조적인 설계 요소를 활용하기 위해 핵심 비즈니스 로직이 안정적으로 활용해야 하는 영속성 계층에 부하를 가하는 구조가 안전하지 않아 보였습니다.

현재 우리 비즈니스의 특성 상 실제 서비스에 진입하여 이용(대기열로부터 차례가 되어 벗어나 예약을 위해 애플리케이션에 요청을 보내는 단계)하는 사람과 대기하는 사람의 비율이 상당히 클 수 밖에 없습니다.

가령 인터파크 티켓팅에서 잠실 주경기장에서 열리는 브루노 마스 공연(약 7만 석) 당시 13만 명에 가까운 인원이 대기열을 이용했습니다.

서비스를 이용할지, 안할 지도 모르는 사람들의 비율이 실제 서비스를 이용하는 사람의 200%에 가까운 수치입니다.

이와 같은 불확실하고 핵심 서비스 로직과도 일차적인 관련이 없는 트래픽 부하가 영속성 계층에 어떠한 보호 장치 없이 가해지는 것은 가급적 피하는 것이 좋다고 생각합니다.

그렇기 때문에 대기열이라는 개념이 영속성 계층이 아닌 다른 저장소에 근거하여 구현되는 것이 좋다고 판단했습니다.

시간 복잡도 관점에서의 효율성 현행 프로젝트는 하루에 공연 예매가 한 건만 발생하도록 하였습니다만, 실제로는 하루에 여러 공연의 예약이 동시에 진행될 수 있습니다.

당연히 각 공연 일정 별로 별도의 대기열 관리가 필요할 것입니다.

이때 각 공연 일정 별로 별도의 DB 테이블을 활용하는 것이 아닌 이상, 하나의 테이블에 공연 일정 ID로 구분하는 상당히 많은 Token 이 행으로서 존재하게 될 것입니다.

이런 구조에서 '어떤 공연 일정의 대기열에서, 앞의 K 명의 토큰 상태를 변경한다' 라는 내용을 구현한다면 어떤 방식으로 이루어질까요?

각 공연 일정 별로 섞여있을 것이기 때문에 LIMIT 와 같은 쿼리로 편리하게 K 명을 추출할 수 없을 것입니다.

공연 일정 별로 구분도 해야합니다.

이후 해당하는 K 개의 토큰에 대해 각각 상태 변경이 수행되어야 할 것입니다.

이러한 연산의 시간복잡도를 개략적으로 산출해본다면 O(K) 수준이 될 것임을 생각해볼 수 있습니다.

만약 Redis 에서 제공하는 Scored-Sorted Set 을 활용한다면 어떨까요?

비록 삽입에 O(log N)이 걸리기는 하나, 실제로 조회 시점에는 상수 시간 복잡도에 가까운 성능을 내어줄 것임을 짐작해볼 수 있습니다.

따라서 효율성 측면에서도 DB 기반의 대기열 구현보단 Redis 기반 구현이 우월하다고 할 수 있는 것입니다.

3. 3. Redis 자료 구조를 활용한 대기열 로직 이관 : N 초에 M 개의 토큰을 활성화한다!

일반적인 관계형 DB의 경우 읽기에 대해서는 4000 TPS, 튜닝이 잘 된 DB 라면 그 이상도 가능하지만 쓰기 TPS 는 그보다 낮은 200~300 TPS 정도의 처리가 가능하다고 합니다. 이때 우리의 예약 시스템에서는 좌석 예약에 분산 락이 적용되어 있으므로, 동시 사용자의 규모와 활성화 주기는 쓰기 트랜잭션이 결정하는 것임을 생각해볼 수 있습니다.

  • 활성화 인원 결정 공식
    활성화 인원 ( M )은 다음 공식으로 산출할 수 있습니다.

$$ M \leq TPS \times N \times \text{안전계수} $$

예를 들어,

  • DB의 쓰기 TPS를 200건으로 가정하고,
  • 안전계수를 0.2로 설정하며,
  • 활성화 주기 ( N )을 10초로 잡는다면,

$$ M \leq 200 \times 10 \times 0.2 = 400 $$

이론적으로는 10초 동안 최대 400건의 쓰기가 가능하지만, 우리 시스템은 최대 좌석 수가 50석으로 제한되어 있으므로
동시에 활성화하는 사용자는 실제로 10명 정도로 제한하는 것이 안정적입니다.

  • 시스템 안정성 및 단계적 예약 처리
    실제 예매 시스템에서는 전체 대기자는 많을 수 있으나, 동시에 DB에 쓰기 작업을 발생시키는 활성화 인원 ( M )은
    시스템 부하와 동시성 충돌을 고려하여 보수적으로 설정해야 합니다.
    예를 들어, 50석 전체 예약을 10명씩 5단계로 나누어 진행하면,
    각 배치에서 DB에 가해지는 쓰기 부하를 완화할 수 있고,
    동시 업데이트로 인한 충돌을 효과적으로 제어할 수 있습니다.

  • 최종 결론
    DB의 TPS와 시스템 안정성을 고려할 때,

    • 활성화 주기 ( N = 10 )초,
    • 동시에 활성화하는 사용자 수 ( M = 10 )명
      은 예약 프로세스의 단계적 진행과 안정성을 보장하는 합리적인 설정으로 판단됩니다.

    이러한 설정은 DB의 쓰기 부하를 적절히 분산시키고, 동시성 충돌을 최소화하며, 전체 예매 시스템의 응답시간과 안정성을 향상시키는 데 기여할 것으로 기대됩니다.

⚠️ **GitHub.com Fallback** ⚠️