LabHub

블로그

시스템 설계 기초 완전 가이드 — 확장성, 가용성, 캐싱, 메시지 큐

한국어English日本語

목차

  1. 시스템 설계란
  2. 확장성
  3. 로드 밸런싱
  4. 캐싱 전략
  5. CDN
  6. 데이터베이스
  7. CAP 정리
  8. 메시지 큐
  9. API 설계
  10. 모니터링과 로깅
  11. 실전 예제: URL 단축기 설계
  12. 실전 예제: 채팅 시스템 설계

시스템 설계란

시스템 설계(System Design)는 대규모 소프트웨어 시스템의 아키텍처를 구성하는 과정이다. 단순히 코드를 작성하는 것을 넘어, 수백만 사용자를 처리할 수 있는 확장 가능하고 안정적인 시스템을 만드는 방법을 다룬다.

왜 중요한가

면접에서의 접근법

시스템 설계 면접에서는 정답이 없다. 중요한 것은 사고 과정이다.

  1. 요구사항 명확화: 기능적/비기능적 요구사항을 정리한다
  2. 규모 추정: 사용자 수, QPS, 저장 용량 등을 계산한다
  3. 고수준 설계: 핵심 컴포넌트를 배치한다
  4. 상세 설계: 병목 지점을 파악하고 해결한다
  5. 트레이드오프 논의: 모든 결정에는 장단점이 있다

확장성

확장성(Scalability)은 시스템이 증가하는 부하를 처리하는 능력이다.

수직 확장 (Scale Up)

하나의 서버에 더 강력한 CPU, 더 많은 RAM, 더 큰 디스크를 추가하는 방식이다.

장점:

단점:

수평 확장 (Scale Out)

여러 대의 서버를 추가하여 부하를 분산하는 방식이다.

장점:

단점:

Stateless 설계

수평 확장의 핵심은 Stateless 설계다. 서버가 상태를 보유하지 않으면 어떤 서버든 동일한 요청을 처리할 수 있다.

Stateful 서버:
  사용자 A -> 서버 1 (세션 저장)
  사용자 A -> 서버 2 (세션 없음 -> 오류!)

Stateless 서버:
  사용자 A -> 서버 1 (외부 저장소에서 세션 조회)
  사용자 A -> 서버 2 (외부 저장소에서 세션 조회 -> 정상)

세션, 캐시 등의 상태는 Redis나 Memcached 같은 외부 저장소에 보관한다.


로드 밸런싱

로드 밸런서(Load Balancer)는 들어오는 트래픽을 여러 서버에 분배하는 역할을 한다.

L4 vs L7 로드 밸런싱

L4 (전송 계층):

L7 (애플리케이션 계층):

로드 밸런싱 알고리즘

라운드 로빈 (Round Robin): 요청을 순서대로 각 서버에 배분한다. 구현이 단순하지만 서버 성능 차이를 고려하지 못한다.

가중치 기반 라운드 로빈 (Weighted Round Robin): 서버마다 가중치를 부여하여, 성능이 좋은 서버에 더 많은 요청을 배분한다.

IP 해시 (IP Hash): 클라이언트의 IP를 해싱하여 항상 같은 서버로 라우팅한다. 세션 유지가 필요한 경우에 유용하지만, 서버 추가/제거 시 재분배가 필요하다.

최소 연결 (Least Connections): 현재 연결이 가장 적은 서버에 요청을 보낸다. 요청 처리 시간이 다양할 때 효과적이다.

라운드 로빈 예시:

  요청 1 -> 서버 A
  요청 2 -> 서버 B
  요청 3 -> 서버 C
  요청 4 -> 서버 A  (순환)
  요청 5 -> 서버 B  (순환)

헬스 체크

로드 밸런서는 주기적으로 서버의 상태를 확인한다. 응답이 없거나 비정상 응답을 보내는 서버는 로드 밸런싱 대상에서 제외한다.

로드 밸런서:
  /health -> 서버 A: 200 OK (정상)
  /health -> 서버 B: 503 Error (비정상 -> 제외)
  /health -> 서버 C: 200 OK (정상)

  이후 트래픽은 서버 A와 C에만 분배

캐싱 전략

캐싱(Caching)은 자주 조회되는 데이터를 빠른 저장소에 임시로 보관하여 응답 시간을 줄이는 기법이다.

Cache Aside (Lazy Loading)

가장 널리 사용되는 패턴이다.

  1. 애플리케이션이 캐시를 먼저 확인한다
  2. 캐시에 있으면(Cache Hit) 반환한다
  3. 캐시에 없으면(Cache Miss) DB에서 조회 후 캐시에 저장한다
읽기 흐름:
  클라이언트 -> 캐시 확인
    히트 -> 캐시에서 반환
    미스 -> DB 조회 -> 캐시 저장 -> 반환

장점: 실제로 요청되는 데이터만 캐싱한다. 캐시 장애 시 DB에서 직접 조회 가능하다. 단점: 첫 번째 요청은 항상 Cache Miss가 발생한다. 데이터 불일치가 발생할 수 있다.

Write Through

데이터를 쓸 때 캐시와 DB에 동시에 기록하는 패턴이다.

쓰기 흐름:
  클라이언트 -> 캐시 업데이트 -> DB 업데이트 -> 응답

장점: 캐시와 DB의 일관성이 보장된다. 단점: 쓰기 지연이 증가한다. 사용되지 않는 데이터도 캐싱된다.

Write Behind (Write Back)

데이터를 캐시에 먼저 쓰고, 일정 시간 후 비동기적으로 DB에 반영하는 패턴이다.

쓰기 흐름:
  클라이언트 -> 캐시 업데이트 -> 응답 (즉시)
  백그라운드: 캐시 -> DB 동기화 (지연)

장점: 쓰기 성능이 매우 빠르다. DB 부하를 줄일 수 있다. 단점: 캐시 장애 시 데이터 유실 가능성이 있다.

TTL과 캐시 무효화

TTL (Time To Live): 캐시 항목의 만료 시간을 설정한다. 너무 짧으면 캐시 효율이 떨어지고, 너무 길면 데이터 불일치가 발생한다.

캐시 무효화 전략:

TTL 예시:
  SET user:123 "Kim" EX 3600   (1시간 후 만료)
  SET product:456 "Laptop" EX 86400  (24시간 후 만료)

캐시 스탬피드 (Thundering Herd)

인기 있는 캐시 항목이 만료될 때, 수많은 요청이 동시에 DB로 몰리는 현상이다.

해결 방법:


CDN

CDN(Content Delivery Network)은 전 세계에 분산된 엣지 서버를 통해 사용자에게 가장 가까운 위치에서 콘텐츠를 제공하는 시스템이다.

동작 원리

사용자(서울) -> 서울 엣지 서버 (캐시 히트) -> 즉시 응답
사용자(서울) -> 서울 엣지 서버 (캐시 미스) -> 오리진 서버 -> 엣지에 캐싱 -> 응답

정적 자산 제공

CDN은 주로 정적 콘텐츠를 제공하는 데 사용된다.

캐시 히트율 최적화

캐시 히트율(Cache Hit Ratio)이 높을수록 CDN의 효율이 좋다.

최적화 방법:

Pull CDN vs Push CDN

Pull CDN: 첫 번째 요청 시 오리진에서 콘텐츠를 가져와 캐싱한다. 트래픽이 많은 사이트에 적합하다.

Push CDN: 콘텐츠를 미리 CDN에 업로드한다. 콘텐츠 변경이 드문 경우에 적합하다.


데이터베이스

SQL vs NoSQL

SQL (관계형 데이터베이스):

NoSQL (비관계형 데이터베이스):

선택 기준:

기준SQLNoSQL
데이터 구조정형화됨유연함
확장 방식수직 확장 위주수평 확장 위주
일관성강한 일관성최종 일관성 (보통)
트랜잭션ACIDBASE
쿼리복잡한 JOIN 가능단순 키-값 조회 최적화

샤딩 (Sharding)

데이터를 여러 데이터베이스 인스턴스에 분산 저장하는 기법이다.

해시 기반 샤딩: 키를 해싱하여 샤드를 결정한다. 균등 분배가 가능하지만, 샤드 추가/제거 시 재분배 비용이 크다.

user_id % 4 = 0 -> 샤드 A
user_id % 4 = 1 -> 샤드 B
user_id % 4 = 2 -> 샤드 C
user_id % 4 = 3 -> 샤드 D

범위 기반 샤딩: 키의 범위에 따라 샤드를 결정한다. 범위 쿼리에 유리하지만, 핫스팟이 발생할 수 있다.

일관된 해싱 (Consistent Hashing): 해시 링을 사용하여 노드 추가/제거 시 최소한의 키만 재배치한다. 분산 캐시나 분산 DB에서 널리 사용된다.

레플리케이션 (Replication)

데이터를 여러 노드에 복제하여 가용성과 읽기 성능을 높이는 기법이다.

마스터-슬레이브 (Primary-Replica):

마스터-마스터 (Multi-Primary):

마스터-슬레이브 구조:

  쓰기 -> [마스터]
            |
     +------+------+
     |              |
  [슬레이브1]   [슬레이브2]
     |              |
  읽기 요청     읽기 요청

파티셔닝

데이터를 논리적으로 분할하는 기법이다.

수평 파티셔닝: 행 단위로 분할. 샤딩과 유사하다. 수직 파티셔닝: 열 단위로 분할. 자주 조회되는 열과 그렇지 않은 열을 분리한다.


CAP 정리

분산 시스템에서 다음 세 가지 속성을 동시에 모두 만족시킬 수 없다는 정리다.

세 가지 속성

Consistency (일관성): 모든 노드가 동일한 데이터를 보유한다. 읽기 요청은 항상 가장 최신 데이터를 반환한다.

Availability (가용성): 모든 요청에 대해 응답을 반환한다. 일부 노드가 장애 상태더라도 시스템은 정상 응답한다.

Partition Tolerance (분할 허용성): 네트워크 분할이 발생해도 시스템이 계속 동작한다.

실제 선택

네트워크 분할(Partition)은 분산 시스템에서 불가피하므로, 실질적으로는 CP 또는 AP 중 하나를 선택해야 한다.

CP 시스템 (일관성 + 분할 허용성):

AP 시스템 (가용성 + 분할 허용성):

CP 시스템 (분할 발생 시):
  노드 A: 최신 데이터 = "v2"
  노드 B: 오래된 데이터 = "v1" -> 요청 거부 (일관성 우선)

AP 시스템 (분할 발생 시):
  노드 A: 최신 데이터 = "v2"
  노드 B: 오래된 데이터 = "v1" -> "v1" 반환 (가용성 우선)

PACELC

CAP 정리의 확장으로, 분할이 없을 때(E: Else)의 지연 시간(L: Latency)과 일관성(C: Consistency) 간의 트레이드오프도 고려한다.


메시지 큐

메시지 큐(Message Queue)는 비동기 통신을 가능하게 하는 미들웨어다. 생산자(Producer)와 소비자(Consumer)를 분리하여 시스템의 결합도를 낮추고 확장성을 높인다.

왜 필요한가

동기 통신의 문제점:

동기 (문제):
  주문 API -> 결제 서비스 -> 재고 서비스 -> 알림 서비스 -> 응답
  (전체 지연 = 각 서비스 지연의 합)

비동기 (해결):
  주문 API -> 메시지 큐에 이벤트 발행 -> 즉시 응답
  결제 서비스 <- 큐에서 소비
  재고 서비스 <- 큐에서 소비
  알림 서비스 <- 큐에서 소비
  (각 서비스가 독립적으로 처리)

Apache Kafka

분산 이벤트 스트리밍 플랫폼이다.

핵심 특징:

Kafka 구조:
  Producer -> Topic (Partition 0, 1, 2) -> Consumer Group
                                            Consumer A (P0)
                                            Consumer B (P1)
                                            Consumer C (P2)

사용 사례: 로그 수집, 이벤트 소싱, 실시간 스트리밍 처리, 마이크로서비스 간 통신

RabbitMQ

전통적인 메시지 브로커로, AMQP 프로토콜을 사용한다.

핵심 특징:

사용 사례: 작업 큐, RPC 패턴, 복잡한 라우팅이 필요한 경우

Amazon SQS

AWS의 완전 관리형 메시지 큐 서비스다.

핵심 특징:

Kafka vs RabbitMQ vs SQS 비교

특성KafkaRabbitMQSQS
처리량매우 높음중간높음
메시지 순서파티션 내 보장보장 가능FIFO만 보장
메시지 보존설정 기간 동안 보존소비 후 삭제최대 14일
운영 복잡도높음중간낮음 (관리형)
적합한 용도이벤트 스트리밍작업 큐서버리스 워크로드

이벤트 드리븐 아키텍처

메시지 큐를 활용한 이벤트 드리븐 아키텍처는 마이크로서비스 환경에서 핵심 패턴이다.

이벤트 소싱: 상태 변경을 이벤트로 기록한다. 현재 상태는 이벤트를 재생하여 재구성한다.

CQRS: 명령(Command)과 조회(Query)를 분리하여 각각 최적화한다.

이벤트 소싱 예시:
  이벤트 1: 주문 생성 (OrderCreated)
  이벤트 2: 결제 완료 (PaymentCompleted)
  이벤트 3: 배송 시작 (ShippingStarted)
  이벤트 4: 배송 완료 (DeliveryCompleted)

  현재 상태 = 이벤트 1 + 2 + 3 + 4 적용 결과

API 설계

REST vs GraphQL vs gRPC

REST:

GET /api/users/123
POST /api/orders
PUT /api/users/123
DELETE /api/orders/456

GraphQL:

query {
  user(id: "123") {
    name
    email
    orders {
      id
      total
    }
  }
}

gRPC:

service UserService {
  rpc GetUser (GetUserRequest) returns (UserResponse);
  rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
}

API 비교

특성RESTGraphQLgRPC
프로토콜HTTP/1.1HTTP/1.1HTTP/2
데이터 형식JSONJSONProtocol Buffers
타입 시스템없음 (OpenAPI로 보완)내장내장
스트리밍제한적Subscription양방향 스트리밍
적합한 용도공개 API프론트엔드 BFF마이크로서비스

API 버저닝

API 변경 시 하위 호환성을 유지하는 전략이다.

Rate Limiting

API 남용을 방지하고 시스템을 보호하기 위해 요청 횟수를 제한한다.

알고리즘:

토큰 버킷:
  버킷 용량: 10
  충전 속도: 1개/초

  시간 0: 10개 토큰 (요청 5개 -> 5개 남음)
  시간 5: 10개 토큰 (5개 충전, 상한 10)

모니터링과 로깅

메트릭

시스템의 건강 상태를 수치로 파악한다.

핵심 메트릭:

도구: Prometheus, Grafana, Datadog, CloudWatch

알림 (Alerting)

메트릭이 임계값을 초과하면 담당자에게 알림을 보낸다.

좋은 알림의 조건:

분산 트레이싱

마이크로서비스 환경에서 요청의 전체 경로를 추적한다.

사용자 요청 (trace-id: abc123)
  -> API Gateway (span-1, 5ms)
    -> 인증 서비스 (span-2, 10ms)
    -> 주문 서비스 (span-3, 50ms)
      -> 결제 서비스 (span-4, 200ms)
      -> 재고 서비스 (span-5, 30ms)
    -> 알림 서비스 (span-6, 15ms)
  총 소요 시간: 310ms

도구: Jaeger, Zipkin, AWS X-Ray, OpenTelemetry

로깅 전략

구조화된 로그: JSON 형식으로 로그를 기록하면 검색과 분석이 용이하다.

{
  "timestamp": "2026-04-12T10:30:00Z",
  "level": "ERROR",
  "service": "order-service",
  "trace_id": "abc123",
  "message": "Payment failed",
  "user_id": "user-789",
  "order_id": "order-456",
  "error_code": "INSUFFICIENT_FUNDS"
}

ELK 스택 (Elasticsearch + Logstash + Kibana) 또는 Loki + Grafana 조합으로 로그를 수집, 저장, 시각화한다.


실전 예제: URL 단축기 설계

면접에서 자주 출제되는 시스템 설계 문제다.

요구사항

기능적 요구사항:

비기능적 요구사항:

규모 추정

고수준 설계

클라이언트 -> 로드밸런서 -> API 서버 -> DB
                              |
                            캐시 (Redis)

단축 키 생성

방법 1: 해시 + 자르기

방법 2: 카운터 기반

방법 3: 사전 생성

상세 설계

리다이렉트 흐름:

1. 클라이언트가 short.url/abc123 요청
2. 로드밸런서가 API 서버로 전달
3. API 서버가 Redis 캐시 확인
4. 캐시 히트 -> 301 Redirect 응답
5. 캐시 미스 -> DB 조회 -> 캐시 저장 -> 301 Redirect

301 vs 302 리다이렉트:


실전 예제: 채팅 시스템 설계

요구사항

기능적 요구사항:

비기능적 요구사항:

통신 프로토콜

HTTP 폴링: 클라이언트가 주기적으로 서버에 새 메시지를 확인한다. 구현이 단순하지만 비효율적이다.

롱 폴링: 서버가 새 메시지가 있을 때까지 연결을 유지한다. 폴링보다 효율적이지만, 타임아웃 관리가 필요하다.

WebSocket: 양방향 실시간 통신이 가능하다. 초기 HTTP 핸드셰이크 후 지속적 연결을 유지한다. 실시간 채팅에 가장 적합하다.

HTTP 폴링:
  클라이언트 -> 서버: 새 메시지? (매 1초)
  서버 -> 클라이언트: 없음
  클라이언트 -> 서버: 새 메시지? (매 1초)
  서버 -> 클라이언트: 있음! (메시지 전달)

WebSocket:
  클라이언트 <-> 서버: 상시 연결 유지
  서버 -> 클라이언트: 새 메시지 즉시 push

고수준 설계

발신자 -> WebSocket 서버 -> 메시지 큐 -> WebSocket 서버 -> 수신자
                |                           |
              메시지 DB              온라인 상태 서비스
                |
            메시지 검색 (Elasticsearch)

메시지 저장소 선택

채팅 메시지의 특성:

적합한 DB:

메시지 ID 설계

메시지의 순서를 보장하기 위해 ID가 시간 순으로 정렬 가능해야 한다.

Snowflake ID 방식:

| 타임스탬프 (41비트) | 데이터센터 (5비트) | 머신 (5비트) | 시퀀스 (12비트) |

온라인 상태 관리

하트비트 방식: 클라이언트가 주기적으로(예: 5초 간격) 서버에 하트비트를 전송한다. 일정 시간 동안 하트비트가 없으면 오프라인으로 판정한다.

사용자 A: 하트비트 전송 (5초 간격)
  서버: 마지막 하트비트 시간 기록
  30초간 하트비트 없음 -> 상태를 "오프라인"으로 변경

그룹 채팅

그룹 채팅에서는 메시지를 그룹 내 모든 멤버에게 전달해야 한다.

소규모 그룹 (수십 명):

대규모 그룹 (수천 명):


마무리

시스템 설계의 핵심은 트레이드오프를 이해하는 것이다. 완벽한 시스템은 없으며, 항상 요구사항에 맞는 최적의 선택을 해야 한다.

핵심 원칙 정리:

이 글에서 다룬 개념들은 시스템 설계의 기초에 해당한다. 실제 프로젝트에서 이 개념들을 적용하고, 다양한 시스템 설계 사례를 학습하면서 실력을 키워나가길 바란다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다