[System Design Lab] Prologue. 대규모 시스템 설계를 직접 구현해보려는 이유
시작하며
백엔드 개발을 공부하면서 캐시, 메시지 큐, 샤딩, 복제, 로드 밸런싱과 같은 개념을 자주 접했다.
각 기술이 어떤 역할을 하는지는 이해할 수 있었지만, 실제 시스템에 적용하려고 하면 쉽게 답하기 어려운 질문이 생겼다.
- 캐시는 어느 시점에 필요한가?
- 데이터베이스는 어떤 상황에서 병목이 되는가?
- 비동기 처리를 적용하면 실제 응답 시간은 얼마나 줄어드는가?
- 메시지 처리에 실패하면 어떻게 복구해야 하는가?
- 처리량이 증가하면 가장 먼저 한계에 도달하는 지점은 어디인가?
- 성능을 높이기 위해 추가한 구조가 운영 복잡도에는 어떤 영향을 주는가?
책이나 강의를 통해 설계안을 이해하는 것과 직접 구현한 시스템에서 병목을 발견하고 개선하는 것은 다른 경험이라고 생각했다.
그래서 『가상 면접 사례로 배우는 대규모 시스템 설계 기초』에서 다루는 시스템을 참고해, 주요 설계 문제를 직접 구현하고 검증하는 Backend System Design Lab을 시작하게 되었다.
아키텍처를 그리는 것만으로는 부족했다
시스템 설계 문제를 접하면 보통 다음과 같은 구조를 먼저 떠올리게 된다.
Client
→ Load Balancer
→ Application Server
→ Cache
→ Database
→ Message Queue
하지만 다이어그램에 구성 요소를 추가하는 것만으로는 해당 설계가 실제로 필요한지 판단하기 어렵다.
예를 들어 URL 단축 서비스를 설계하면서 Redis를 추가하는 것은 어렵지 않다. 그러나 더 중요한 것은 다음 질문이라고 생각한다.
Redis가 없을 때 어느 구간에서 병목이 발생하는가?
캐시를 적용하면 RPS와 응답 시간은 얼마나 달라지는가?
캐시 적중률이 낮을 때도 효과가 있는가?
캐시 데이터가 유실되면 어떻게 복구할 것인가?
DB와 캐시의 데이터가 달라질 가능성은 없는가?
이 질문에 답하려면 설계도를 그리는 것에서 끝나지 않고, 실제로 요청을 발생시키고 결과를 측정해야 한다.
따라서 이번 프로젝트에서는 특정 기술을 사용하는 것보다 기술이 필요한 이유와 적용 결과를 확인하는 과정에 집중하려 한다.
이번 프로젝트에서 만들고 싶은 흐름
Backend System Design Lab의 공통 진행 과정은 다음과 같다.
요구사항 정의
→ 용량 산정
→ 초기 아키텍처 설계
→ Baseline 구현
→ 부하 테스트
→ 병목 분석
→ 구조 개선
→ 동일한 조건으로 재측정
→ 결과와 트레이드오프 정리
처음부터 완성된 아키텍처를 구현하지 않는다.
우선 가장 단순한 구조를 구현하고 부하를 발생시킨다. 이후 Prometheus와 Grafana를 통해 응답 시간, 처리량, 오류율, CPU와 메모리 사용량 등을 확인한다.
병목이 확인되면 캐시, 비동기 처리, 메시지 큐, 데이터 분할과 같은 구조를 적용하고 동일한 조건에서 다시 측정한다.
핵심은 다음 흐름을 반복하는 것이다.
가설 → 구현 → 측정 → 분석 → 개선 → 재측정
기술을 먼저 선택한 뒤 이유를 붙이는 것이 아니라, 측정된 문제를 기준으로 기술을 선택하는 경험을 만들고 싶었다.
요구사항을 숫자로 바꾸는 연습
시스템 설계에서는 기능을 정의하는 것만큼 시스템의 규모를 추정하는 과정도 중요하다.
예를 들어 “많은 사용자가 URL을 조회한다”는 표현만으로는 저장 구조나 캐시 전략을 결정하기 어렵다.
다음과 같이 구체적인 수치로 바꾸어야 한다.
하루 활성 사용자 수
사용자당 평균 요청 수
평균 및 최대 RPS
읽기와 쓰기 요청의 비율
하루 생성되는 데이터 크기
예상 보관 기간
캐시가 필요한 데이터의 크기
계산한 값이 실제 서비스 규모를 정확하게 예측하지는 못하더라도, 설계 선택에 대한 기준은 만들 수 있다.
이번 프로젝트를 통해 막연하게 “트래픽이 많기 때문에 Redis를 사용했다”라고 설명하는 것이 아니라 다음과 같이 말할 수 있는 연습을 하려고 한다.
예상 읽기 요청은 최대 몇 RPS이고,
단일 DB 조회만으로 처리했을 때 어떤 병목이 발생했으며,
캐시 적용 후 지표가 어떻게 달라졌는가
성능뿐 아니라 실패 상황도 함께 다루기
처리량과 응답 시간을 높이는 것만으로 안정적인 시스템이 완성되는 것은 아니다.
메시지 큐를 도입한다면 메시지가 중복 처리되거나 유실될 수 있다. 캐시를 적용하면 데이터베이스와 값이 달라질 수 있다. 서버를 여러 대로 늘리면 동시성 제어와 상태 관리가 더 어려워질 수 있다.
따라서 각 프로젝트에서는 정상 흐름뿐 아니라 다음과 같은 실패 상황도 함께 확인하려 한다.
- 요청이 중복으로 전달되는 경우
- 메시지 처리에 실패하는 경우
- 캐시 데이터가 유실되는 경우
- 애플리케이션이 처리 도중 종료되는 경우
- 일부 외부 시스템의 응답이 느려지는 경우
- 여러 요청이 동일한 데이터를 동시에 변경하는 경우
새로운 구조가 성능을 얼마나 높이는지만 확인하지 않고, 그 구조로 인해 어떤 복잡도가 추가되는지도 기록할 예정이다.
공통 실험 환경
각 프로젝트의 결과를 비교하려면 실행 환경과 측정 기준이 달라지지 않아야 한다.
그래서 첫 번째 시스템 구현에 앞서 다음과 같은 공통 환경을 먼저 만들었다.
| 구분 | 역할 |
|---|---|
| Spring Boot | 시스템 API 구현 |
| Docker Compose | 동일한 로컬 실행 환경 제공 |
| k6 | Smoke, Load, Stress Test 수행 |
| Actuator·Micrometer | 애플리케이션 메트릭 노출 |
| Prometheus | 메트릭 수집 |
| Grafana | 성능과 자원 사용량 시각화 |
| GitHub Actions | 빌드, 테스트, 실행 검증 자동화 |
공통 환경은 GitHub의 Template Repository로 만들었다.
새로운 프로젝트를 시작할 때 템플릿을 복제하고 프로젝트명과 패키지를 변경하면, 같은 실행 환경과 부하 테스트 구조를 바로 사용할 수 있다.
이를 통해 매번 Docker, Prometheus, Grafana, k6 설정을 반복하기보다 각 프로젝트의 핵심 설계 문제에 집중하려 한다.
블로그에는 무엇을 기록할 것인가
각 프로젝트가 끝날 때마다 결과를 블로그에 정리할 예정이다.
단순히 구현한 기능과 사용한 기술을 나열하기보다 다음 질문에 답하는 글을 작성하려 한다.
어떤 요구사항을 정의했는가?
트래픽과 데이터 규모를 어떻게 산정했는가?
처음에는 어떤 구조로 구현했는가?
어디에서 병목이나 장애 가능성을 발견했는가?
왜 해당 기술과 구조를 선택했는가?
적용 전후의 수치는 어떻게 달라졌는가?
새로운 구조로 인해 어떤 복잡도가 추가됐는가?
실제 운영 환경이라면 무엇을 더 고려해야 하는가?
결과가 기대와 다르거나 성능이 개선되지 않은 실험도 숨기지 않고 기록하려 한다. 예상과 다른 결과에서 원인을 찾는 과정 역시 시스템을 이해하는 데 필요한 경험이라고 생각하기 때문이다.
이 프로젝트를 통해 얻고 싶은 것
이번 프로젝트의 목표는 유명한 시스템의 아키텍처를 그대로 따라 만드는 것이 아니다.
궁극적으로 얻고 싶은 것은 다음 세 가지다.
1. 설계 선택을 근거와 함께 설명하는 능력
“일반적으로 Redis를 사용한다”가 아니라, 현재 요구사항과 측정 결과에서 왜 Redis가 필요한지 설명하고 싶다.
2. 병목을 직접 발견하고 개선하는 경험
코드만 보고 성능을 추측하기보다 부하 테스트와 메트릭을 통해 문제를 확인하고 개선하고 싶다.
3. 설계의 트레이드오프를 판단하는 기준
성능이 높아졌다는 이유만으로 좋은 설계라고 판단하지 않고, 데이터 정합성, 장애 복구, 비용과 운영 복잡도를 함께 고려하고 싶다.
시스템 설계에는 하나의 정답이 없다는 말을 자주 듣는다. 그렇기 때문에 어떤 구조를 선택했는지보다 주어진 조건에서 왜 그 구조를 선택했는지 설명하는 과정이 더 중요하다고 생각한다.
마치며
지금까지는 프로젝트에서 발생한 성능 문제를 해결하거나 특정 기술을 적용한 경험을 개별적으로 쌓아왔다.
이번 Backend System Design Lab에서는 이를 조금 더 체계적인 과정으로 반복하려 한다.
문제를 수치로 정의하고,
가장 단순한 구조에서 시작하고,
부하를 통해 병목을 확인하고,
설계를 개선한 뒤,
같은 조건에서 결과를 비교한다.
모든 프로젝트가 처음부터 대규모 분산 시스템의 형태를 갖추지는 않을 것이다. 로컬 환경에서 진행하는 실험이 실제 운영 환경을 완전히 재현할 수도 없다.
대신 각 실험의 조건과 한계를 분명하게 기록하고, 작은 시스템에서 확인한 결과를 더 큰 환경에서는 어떻게 확장해야 하는지 고민해보려 한다.
다음 글에서는 본격적인 프로젝트를 시작하기 전에 구축한 공통 실험 환경을 정리한다.