강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

유희준님의 프로필 이미지
유희준

작성한 질문수

스프링부트로 직접 만들면서 배우는 대규모 시스템 설계 - 캐시 전략

질문이 있습니다!!

작성

·

23

·

수정됨

0

강의를 보다가 궁금한 부분들이 생겨 질문드립니다!!


1. RateLimit Boundary Burst 관련

RateLimit을 설명해주시는 부분을 보면서 한 가지 의문이 들었습니다.

예를 들어 1초에 100개로 제한을 두었을 때,

0.9초 시점에 100개 요청이 들어오고 1초가 되자마자 다시 100개가 들어온다면

실제로는 매우 짧은 시간 안에 200개의 요청이 처리될 수 있지 않을까 하는 생각이 들었습니다.

찾아보니 이러한 문제를 Boundary Burst라고 부른다는 것을 알게 되었습니다.

이를 해결하기 위해:

  • Sliding Window

  • Sliding Window Counter

  • Token Bucket

  • Leaky Bucket

등의 방식이 있다는 것을 확인했습니다.

실무 관점에서는 이런 경계 구간 Burst 문제를 어떤 방식으로 해결하는지 궁금합니다.


2. Request Collapsing 관련

Request Collapsing을 설명해주신 부분을 보면서,

여러 요청을 하나로 모아서 처리하는 방식으로 Golang의 SingleFlight 패턴을 응용하는 방법도 가능하지 않을까 생각해보았습니다.

이와 관련해서 아래와 같은 예제 코드도 작성해 보았습니다.

private final StringRedisTemplate redisTemplate;
private final Map<String, CompletableFuture<?>> singleFlightMap = new ConcurrentHashMap<>();

@Override
public <T> T fetch(String key, Duration ttl, Supplier<T> supplier, Class<T> clazz) {

  String cached = redisTemplate.opsForValue().get(key);
  if (cached != null) {
    return DataSerializer.deserializeOrNull(cached, clazz);
  }

  CompletableFuture<T> newFuture = new CompletableFuture<>();
  CompletableFuture<T> existing = (CompletableFuture<T>) singleFlightMap.putIfAbsent(key,
      newFuture);

  if (existing != null) {
    // 다른 스레드가 실행 중 → 기다림
    return existing.join();
  }

  // 내가 실행자 (락 없이 실행)
  try {
    T result = refresh(key, ttl, supplier);
    newFuture.complete(result);
    return result;
  } catch (Throwable t) {
    newFuture.completeExceptionally(t);
    throw t;
  } finally {
    singleFlightMap.remove(key, newFuture);
  }
}

private <T> T refresh(String key, Duration ttl, Supplier<T> dataSourceSupplier) {
  T result = dataSourceSupplier.get();
  put(key, ttl, result);
  return result;
}

이 방식은 분산 락이나 폴링 방식 없이도 동일 인스턴스 내 요청을 모을 수 있다는 장점이 있다고 생각했습니다.물론 이 방법은 모든 분산 서버 간 요청을 하나로 모으는 것은 아니기 때문에, 서버 인스턴스 수만큼은 요청이 발생할 수 있다는 한계가 있다고 생각합니다.

하지만 인스턴스 수가 많지 않다면, 분산 락으로 인한 오버헤드나 복잡성을 줄이면서도 어느 정도 트래픽을 제어할 수 있는 현실적인 선택지가 될 수 있지 않을까 하는 생각이 들었습니다.

실무 관점에서는 이런 방식에 대해 어떻게 평가하시는지 궁금합니다.


3. Write Through 방식에서의 장애 처리

Write Through 방식에서는 일반적으로:

  1. DB에 먼저 저장

  2. 동일 데이터를 Redis에도 저장

하는 구조로 이해하고 있습니다.

그런데 만약:

  • DB에는 쓰기 성공

  • Redis에는 쓰기 실패

하는 상황이 발생한다면,
이 경우를 트랜잭션 실패로 간주해야 하는지 궁금합니다.

Redis 장애로 인해 캐시 반영이 실패했을 때,

  • 핵심 트랜잭션(DB 쓰기)까지 롤백해야 하는지

  • 아니면 캐시는 보조 저장소로 보고 DB 성공을 기준으로 처리해야 하는지

  • 두 저장소 간 원자성을 반드시 보장해야 하는 설계인지

     

실무에서는 어떤 기준으로 판단하는지 궁금합니다.

답변 2

1

쿠케님의 프로필 이미지
쿠케
지식공유자

희준님, 안녕하세요!

 

1. RateLimit Boundary Burst 관련

말씀하신대로 경계 구간을 연달아 넘어서는 순간 요청 트래픽이 몰릴 수 있습니다.

이 부분은 먼저 정책적인 정리가 필요할 것 같은데요,

고정된 구간에 대해 N건, 최근 N건, 첫 요청 시점부터의 N건 등 정책에 따라서 구현 방식은 달라지게 됩니다.

현재 강의에서 취한 "첫 요청 시점부터의 N건 전략"은 구현이 간단하여 채택한 방식이지만,

rate limit의 다양한 구현 방식보단 활용 전략과 구간에 대한 이해를 위해 구성한 것이 의도였던 것이고요.

만약 다른 정책으로 가야 한다면 실무 관점에서는 구현 방식을 달리할 수도 있지만,

반드시 이러한 정책으로 가야하는지부터 고민해볼 수도 있고,

윈도우 구간을 더욱 세분화해서 이중/삼중으로 관리할 수도 있는 것이고(예를 들어, 분단위 정책과 초단위 정책 조합),

boundary burst 문제가 우려되지 않을 정도의 가용량을 미리 확보해두는 것도 간단하고 효율적인 방법일 것 같습니다.

 

2. Request Collapsing 관련

Request Collapsing은 전략일 뿐이지, 반드시 모든 요청을 병합할 필요는 없습니다.

인스턴스 수가 많지 않거나 또는 많더라도 데이터 소스가 충분히 트래픽을 받쳐줄 수 있다면,

별도 리모트 서버에 분산락을 구성하여 인프라, 네트워크 비용 등의 오버헤드를 높이는 것보단 말씀하신 전략으로 충분합니다. 실제로도 그렇게 많이 구성하고 있고요.

가벼운 요청의 경우 인스턴스 단위로 처리해도 충분할 수 있으나,

데이터 소스가 반드시 1회만 처리해야하는 경우 또는 무거운 요청은,

여전히 전체 요청에 대해 1회만 처리하는게 좋을지 고민해볼 수 있을 것 같습니다.

 

3. Write Through 방식에서의 장애 처리

  • DB에는 쓰기 성공

  • Redis에는 쓰기 실패

위 상황을 트랜잭션 실패로 다루어야하는지는 시스템과 데이터에 대한 정책에 따라 다릅니다.

어떤 데이터는 정합성이 깨져도 문제가 없을 수도 있고, 지연 처리되어도 문제 없을 수도 있고, 반드시 동시에 처리되어야할 수도 있습니다.

구현에 대한 고민보단 정책적인 고민이 먼저 필요할 것으로 보이고,

DB와 Redis 간에는 기본적으로 트랜잭션 통합을 지원하지 않기 때문에,

반드시 통합시켜야한다면 saga, outbox 등의 분산 트랜잭션을 위한 패턴을 고민하고 구현 방식을 달리해야 합니다.

  • 핵심 트랜잭션(DB 쓰기)까지 롤백해야 하는지

  • 아니면 캐시는 보조 저장소로 보고 DB 성공을 기준으로 처리해야 하는지

  • 두 저장소 간 원자성을 반드시 보장해야 하는 설계인지

그래서 위 방법들 모두 필요할 수 있습니다.

핵심 트랜잭션(DB 쓰기)까지 롤백해야 하는지 이 부분은 데이터 캐싱에 실패했다고 해서 원본 데이터에 대한 비즈니스까지 실패 처리하는건 대부분의 상황에 애매할 수 있겠네요.(물론, 반드시 데이터가 일치되어야 한다면 필요)

추가적으로 아래 전략들도 함께 고민해볼 수 있을 것 같습니다.

  • Redis 복구까지 DB 접근

  • Redis 복구 완료 시에 실패 로그 또는 DB 원본 데이터를 기준으로 정합성 관리

정책에 따른 적절한 전략과 해결책은 그저 찾아서 구현하면 된다고 봅니다!

유희준님의 프로필 이미지
유희준
질문자

강사님, 상세한 답변 감사드립니다!!

결국에는 현재 도메인에 맞는 정책 결정구현 선택 순서로 접근 하는 것이 선점되어야 하는 과제라는 말씀이신 것이군요.

단순히 구현 방법을 찾는 것보다는

  • 이 데이터가 얼마나 일치해야 하는가

  • 이 트래픽을 어느 수준까지 허용할 것인가

처럼 정책 기준을 세우는 것이 더 근본적인 고민이라는 것을 답변을 통해 다시금 느꼈습니다. 앞으로 설계할 때 이 순서를 잊지 않겠습니다.

감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다.
유희준님, 질문에 대해 하나씩 답변드리겠습니다.

  1. RateLimit Boundary Burst 관련
    Boundary Burst 문제는 매우 짧은 시간내에 너무 많은 요청이 한꺼번에 처리되는 상황을 말합니다. 이를 해결하기 위해 주로 Token BucketLeaky Bucket 방법을 많이 사용합니다. Token Bucket은 요청을 처리할 수 있는 토큰을 미리 부여하여 관리하는 방식이고, Leaky Bucket은 정해진 속도로 처리하는 방식입니다. 실무에서는 트래픽 특성에 맞춰 Sliding Window 방식도 조합하여 사용하는 경우가 많습니다.

  2. Request Collapsing 관련
    작성해주신 코드는 SingleFlight 패턴을 응용한 것으로 보입니다. 이 방법은 동일 인스턴스에서 요청을 최소화하기 위해 효율적입니다. 다만, 분산 환경에서는 각 인스턴스의 요청이 합쳐지지 않는 한계가 있어, 분산 락을 통해 각 인스턴스간 요청을 합치는 방식이 필요할 수 있습니다. 인스턴스 수가 적거나 집중적인 락 관리가 불필요한 경우에는 작성하신 방식이 실용적일 수 있습니다.

  3. Write Through 방식에서의 장애 처리
    Redis에 쓰기 실패가 발생할 경우, 일반적으로는 DB 저장이 완료되었기 때문에 트랜잭션이 실패했다고 보진 않습니다. 보조 저장소인 캐시와의 원자성을 보장하기보다, 주 저장소인 DB의 성공을 기준으로 처리하는 것이 통상적입니다. 이는 Redis 캐시 작동이 일시적 실패라면 빠르게 복구될 수 있기 때문입니다. 그러나, 중요 데이터의 경우 트랜잭션 실패로 간주하고, 전체 롤백을 고려할 수 있습니다. 이 결정은 시스템의 아키텍처 및 요구사항에 따라 달라질 수 있습니다.

관련된 추가적인 정보는 아래 링크들을 참고해 주세요:

다른 궁금하신 사항 있으시면 언제든지 말씀해 주세요!

유희준님의 프로필 이미지
유희준

작성한 질문수

질문하기