이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • MAU, DAU, 동시 접속자, QPS, TPS를 어떤 순서로 연결하는가?
  • 평균 QPS와 피크 QPS를 나누면 어떤 설계 결정이 달라지는가?
  • 쓰기 TPS가 작아도 정합성과 운영 복잡도를 키우는 경우는 언제인가?

개요

트래픽 추정은 큰 사용자 숫자를 초당 처리해야 할 일로 바꾸는 과정이다. MAU 500만이라는 문장은 서비스 규모를 말하지만 서버가 감당할 초당 요청을 바로 말해 주지는 않는다. 서버 입장에서 필요한 질문은 하루에 몇 명이 오고, 언제 몰리고, 한 명이 어떤 행동을 몇 번 하며, 그 행동이 읽기인지 쓰기인지다. 이 문서는 숫자를 멋지게 계산하는 문서가 아니다. 계산값을 보고 cache, queue, DB connection, thread pool, external API quota 같은 설계 경계를 어디에 둘지 판단하는 문서다.

이 문서가 바꾸는 설계 축

바뀌는 내용
성능평균 QPS가 아니라 피크 QPS와 latency budget을 기준으로 용량을 잡는다.
정합성TPS가 상태 변경을 뜻할 때 transaction, idempotency, unique constraint가 필요해진다.
장애 격리피크나 retry가 특정 API에 몰리면 queue, rate limit, bulkhead를 검토한다.
운영 복잡도숫자가 불확실할수록 dashboard, load test, capacity review가 필요해진다.

용어를 먼저 분리한다

용어의미설계에서 쓰는 방식
MAU한 달에 한 번 이상 온 사용자시장 규모와 장기 저장량 추정에 쓴다.
DAU하루에 한 번 이상 온 사용자하루 요청량과 피크 시간대 계산의 출발점이다.
Concurrent Users같은 시점에 활성인 사용자WebSocket, session, connection, memory 계산에 쓴다.
QPS초당 요청 수API 서버, cache, DB read, gateway 용량 기준이다.
TPS초당 transaction 수결제, 주문, 포인트, 재고처럼 상태 변경 비용을 본다.
QPSTPS를 같은 말처럼 쓰면 설계가 흐려진다.
상품 상세 조회 3,000 QPS는 cache hit ratio를 먼저 본다.
주문 생성 300 TPS는 lock, index write, payment timeout, idempotency를 먼저 본다.
숫자는 작아도 TPS는 실패 비용이 크다.

대표 시나리오

음원 서비스의 홈 화면과 재생 이벤트를 추정한다고 하자. 요구사항은 다음과 같다.

항목
MAU5,000,000
DAU1,200,000
주요 활성 시간하루 6시간
사용자당 홈 조회18회/일
사용자당 검색4회/일
사용자당 재생 이벤트40회/일
사용자당 플레이리스트 수정0.4회/일
이 숫자를 하나의 QPS로 합치면 중요한 차이가 사라진다.
홈 조회와 검색은 읽기 경로다.
재생 이벤트는 쓰기처럼 들어오지만 사용자 응답과 분리할 수 있다.
플레이리스트 수정은 적은 TPS라도 사용자에게 즉시 반영되어야 하는 상태 변경이다.

평균 QPS 계산

먼저 하루 전체로 나누면 낮은 숫자가 나온다.

home_read_avg_qps = 1,200,000 * 18 / 86,400
                  = 250 QPS
search_avg_qps = 1,200,000 * 4 / 86,400
               = 56 QPS
play_event_avg_tps = 1,200,000 * 40 / 86,400
                   = 556 TPS
playlist_update_avg_tps = 1,200,000 * 0.4 / 86,400
                        = 5.6 TPS

이 계산만 보면 홈 조회보다 재생 이벤트가 더 커 보인다. 하지만 홈 조회는 특정 시간에 몰리고, 재생 이벤트는 비동기 수집으로 흘릴 수 있다. 그래서 평균은 설계 결론이 아니라 대화의 시작점이다.

활성 시간대 보정

대부분의 서비스는 24시간 균등하게 쓰이지 않는다. 주요 활성 시간이 6시간이라면 활성 구간 평균은 달라진다.

active_window_seconds = 6 * 60 * 60 = 21,600
home_read_active_qps = 1,200,000 * 18 / 21,600
                     = 1,000 QPS
search_active_qps = 1,200,000 * 4 / 21,600
                  = 222 QPS
play_event_active_tps = 1,200,000 * 40 / 21,600
                      = 2,222 TPS

피크 계수를 3으로 잡으면 홈 조회는 3,000 QPS가 된다. 재생 이벤트는 6,666 TPS가 되지만, 이 값이 사용자 응답 latency와 같은 의미는 아니다. 비동기 수집이면 producer는 빠르게 ack를 받고 consumer와 storage가 backlog를 처리한다. 동기 저장이면 사용자의 재생 시작 latency가 곧 저장소 쓰기 latency에 묶인다.

QPS와 TPS를 다르게 해석한다

경로계산값우선 판단
홈 조회peak 3,000 QPScache, read model, API latency budget
검색peak 666 QPS검색 엔진 quota, indexing freshness
재생 이벤트 수집peak 6,666 TPSqueue, batch insert, 유실 허용 여부
플레이리스트 수정peak 17 TPStransaction, conflict, read-after-write
홈 조회는 성능 문제로 시작한다.
캐시를 넣으면 성능은 좋아지지만 stale data와 invalidation 정책이 생긴다.
재생 이벤트는 운영 복잡도 문제로 이어진다.
queue를 넣으면 사용자 latency는 줄지만 중복 처리, 순서, 재처리 절차가 필요하다.
플레이리스트 수정은 정합성 문제다.
TPS가 낮아도 같은 플레이리스트를 여러 기기에서 수정하면 충돌 기준이 필요하다.

피크 계수는 근거를 남긴다

피크 계수는 감으로 정하면 위험하다. 근거가 없는 10배는 비용을 부풀리고, 근거가 없는 2배는 장애를 부른다. 처음에는 다음 기준으로 보수적으로 둔다.

상황피크 계수 예시확인할 증거
업무 시간에 고르게 사용2~3시간대별 request log
출퇴근, 점심, 저녁에 몰림3~6active user histogram
이벤트 오픈, 티켓팅, 쿠폰10 이상캠페인 일정, 대기열 여부
외부 장애 후 재시도retry factor 별도client retry 정책
피크 계수와 retry factor를 같은 값에 섞지 않는다.
피크는 사용자가 몰리는 현상이고 retry는 실패가 부하를 다시 만드는 현상이다.
둘을 합치면 장애 상황에서 실제 부하를 낮게 볼 수 있다.

Spring 경계 예시

계산식을 코드로 숨기기보다, 설계 문서와 테스트에서 같은 용어를 쓰게 만든다.

public record TrafficEstimate(
        long dau,
        double actionsPerUserPerDay,
        int activeWindowHours,
        double peakFactor,
        double retryFactor
) {
    public double activeAveragePerSecond() {
        return dau * actionsPerUserPerDay / (activeWindowHours * 3600.0);
    }
    public double plannedPeakPerSecond() {
        return activeAveragePerSecond() * peakFactor * retryFactor;
    }
}

이 코드는 운영 로직의 핵심이 아니라 리뷰 도구다. 중요한 것은 activeWindowHours, peakFactor, retryFactor를 설계 문서에 노출하는 것이다. 숫자의 출처가 없으면 load test 결과가 나와도 무엇을 보정해야 하는지 알기 어렵다.

API 계약에 남길 것

QPS 추정은 API 응답 계약에도 영향을 준다. 홈 조회가 peak 3,000 QPS이고 cache를 쓴다면 응답에 freshness 기준이 필요할 수 있다.

GET /home
X-Data-Freshness: 2026-07-01T09:00:00+09:00

재생 이벤트 수집이 queue 기반이면 201 Created보다 202 Accepted가 더 정확할 수 있다.

POST /play-events
Idempotency-Key: event-98324
202 Accepted
{
  "status": "accepted",
  "eventId": "event-98324"
}

202는 처리 완료가 아니라 접수 완료다. 이 차이를 적지 않으면 사용자는 성공으로 보는데 내부 집계는 아직 반영되지 않는 상태가 된다.

개인 프로젝트 기준

  • DAU, 행동 빈도, 활성 시간, 피크 계수를 최소 한 번은 숫자로 계산한다.
  • QPS와 TPS를 분리하고, 가장 큰 값 하나만 설계 기준으로 삼지 않는다.
  • cache나 queue를 넣지 않았다면 현재 추정값에서 왜 없어도 되는지 적는다.
  • load test가 없더라도 예상 peak QPS와 timeout 기준을 README나 설계 문서에 남긴다.
  • 포트폴리오에서는 MAU 100만보다 peak read 3,000 QPS, write 17 TPS처럼 설계 가능한 숫자를 말한다.

기업 운영 기준

  • 추정값은 dashboard 지표와 같은 이름을 써야 한다.
  • api_home_qps, play_event_ingest_tps, playlist_update_latency_p95처럼 경로별 지표를 둔다.
  • 피크 계수는 실제 트래픽 로그로 주기적으로 보정한다.
  • 대규모 이벤트 전에는 synthetic load test와 rollback 조건을 문서화한다.
  • QPS 증가가 DB connection, thread pool, external quota 중 어디를 먼저 포화시키는지 capacity review에서 확인한다.

위험 신호

  • MAU만 말하고 DAU, 활성 시간, 행동 빈도를 말하지 않는다.
  • 평균 QPS만 계산하고 피크 계수와 retry factor를 빼먹는다.
  • 읽기 QPS와 쓰기 TPS를 하나의 숫자로 합친다.
  • TPS가 작다는 이유로 idempotency와 transaction 충돌을 무시한다.
  • 사용자에게 성공으로 보이는 시점내부 처리가 완료되는 시점을 구분하지 않는다.
  • 계산값을 cache, queue, DB, 외부 API quota 중 어느 결정에도 연결하지 못한다.

확인 질문

확인 질문

  • 확인 질문: MAU에서 바로 QPS를 계산하면 왜 위험한가?
    • 답변: 한 달 방문자는 하루 활성 사용자, 활성 시간대, 행동 빈도를 숨기므로 피크와 병목을 볼 수 없기 때문이다.
  • 확인 질문: QPS와 TPS를 나누는 핵심 이유는 무엇인가?
    • 답변: 조회 부하는 주로 성능과 비용을 바꾸지만 상태 변경 부하는 정합성, 중복 방지, 복구 절차를 함께 바꾸기 때문이다.
  • 확인 질문: 이 문서에서 가장 중요한 운영 지표는 무엇인가?
    • 답변: 평균값이 아니라 경로별 peak QPS, peak TPS, p95 latency, retry rate, saturation 지표다.