이 글은 안정적이고 실용적인 시스템 설계에 대한 저자의 경험과 통찰을 시간 순서대로 소개합니다. 복잡함보다는 예측 가능하고 '지루한' 구조가 좋은 설계임을 강조하며, 실제로 시스템을 만드는 과정에서 알아두면 좋은 핵심 원칙들, 도구, 그리고 실전을 담고 있습니다. 각 주요 개념은 일화와 구체적 예시, 그리고 실감나는 조언과 함께 알기 쉽게 설명합니다.
1. 시스템 설계가 무엇인가요?
시스템 설계는 소프트웨어 설계(프로그램 코드 구조 설계)와 구분되며, 여러 서비스를 어떻게 조립해서 전체 시스템을 완성할지를 고민하는 영역입니다. 여기서 말하는 서비스란 앱 서버, 데이터베이스, 캐시, 큐, 이벤트 버스, 프록시 등 다양한 구성 요소를 의미합니다.
"소프트웨어 설계가 코드 한 줄 한 줄을 어떻게 조립하는지에 대한 것이라면, 시스템 설계는 서비스를 어떻게 조립하는지에 대한 것이다."
저자는 경험에서 우러나온, 쉽게 오류를 만나지 않는, 관리가 쉬운 시스템을 만들기 위한 원칙들을 정리해 보려고 합니다.
2. 좋은 시스템 설계란 무엇인가?
좋은 설계는 놀랄 만큼 단순하고, 눈에 띄지 않습니다. 시스템에서 문제가 오래도록 일어나지 않고, 개발자가 그 일부분을 신경 쓰지 않아도 잘 돌아간다면 그것이 바로 잘 설계된 시스템입니다. 오히려 복잡하고 인상적인 구조일수록 문제를 감추거나 과도하게 설계됐을 가능성이 높습니다.
"실제로 좋은 설계는 그 존재감 자체가 희미하다. 뭔가 인상적인 시스템이 있다면 항상 의심부터 든다."
경험 많은 개발자라면, 어렵게 보이는 트릭이나 패턴보다는 단순함에서 안정성을 찾습니다. 복잡함이 반드시 나쁘다는 건 아니지만, 반드시 단순한 시스템에서 점진적으로 복잡도를 추가해 나가야 한다고 강조합니다.
3. 상태(State) 다루기의 어려움과 원칙
시스템 설계에서 가장 까다로운 부분은 '상태'를 다루는 일입니다. 앱이 어떤 형태로든 정보를 저장할 때, 그 상태를 어디에, 어떻게 저장하고 조작할지 수많은 고민이 필요합니다.
반대로, 정보를 저장하지 않으면(즉, stateless), 설계는 훨씬 단순해집니다. 예를 들어, Github의 PDF → HTML 변환 API처럼 상태가 없는 서비스는 오류가 나더라도 컨테이너 재시작만으로 복구가 가능합니다.
상태 있는 컴포넌트는 최소로 줄여야 하며, 가능하다면 한 서비스만 데이터베이스와 직접 통신하도록 만들고, 나머지는 API 요청이나 이벤트로 연결하세요.
"다섯 개의 서비스가 하나의 테이블에 직접 쓰지 말고, 하나의 서비스만 쓰게 하세요. 나머지는 그 서비스에 요청을 보내세요."
4. 데이터베이스: 핵심 설계 원칙
시스템의 상태를 다루는 핵심은 역시 데이터베이스입니다. 저자는 실제로 MySQL과 PostgreSQL 경험을 바탕으로 몇 가지 실전 조언을 남깁니다.
(1) 스키마와 인덱스
- 테이블 설계는 유연성을 고려해야 하지만, 너무 느슨해서도 안 됩니다.
- 사람이 읽기 쉬운 스키마를 유지하세요.
- 인덱스는 자주 사용하는 조건에 맞춰 만들고, 카디널리티(다양성)가 가장 높은 필드를 우선하세요.
- 필요 이상으로 많은 인덱스는 오히려 성능 저해 요소입니다.
"테이블이 몇 개만 넘는다면 꼭 인덱스를 걸어둬라."
(2) 병목과 확장성
- DB 접근은 종종 대규모 트래픽의 병목입니다.
- 테이블 간 데이터를 묶어올 때는
JOIN을 활용하세요. 쿼리를 쪼개서 처리하는 것이 항상 좋지는 않지만, 간혹 필요할 때가 있습니다. - 대부분의 시스템은 쓰기노드 한 대 + 읽기 복제본 구조로 운영합니다. 읽기는 복제본에서, 반드시 실시간 동기화가 필요할 때만 마스터에서 처리하세요.
- 쓰기 트랜잭션이나 대량 쿼리 요청에 대비해 제한/스로틀링도 고려해야 합니다.
5. 느린 작업, 빠른 작업: 백그라운드 잡
시스템 인터페이스는 빠른 응답이 필요하지만, 몇몇 작업은 불가피하게 오래 걸릴 수 있습니다. 이런 작업은 필요 최소한만 즉각 처리하고, 나머지는 백그라운드 잡으로 넘어갑니다.
"PDF → HTML 변환의 경우, 첫 페이지만 바로 결과를 보여주고 나머지는 백그라운드에서 처리하면 좋다."
백그라운드 잡은 보통 큐(예: Redis), 잡 실행 서비스로 구성돼 있습니다. 예약 작업(예: 한 달 뒤 실행)처럼 큐에 오래 담기 어려운 작업은 DB 테이블에 기록해 두고, 정기적으로 꺼내 처리하는 방식을 쓰기도 합니다.
6. 캐싱, 반드시 필요한 경우에만!
캐시는 공통 데이터 접근이 느릴 때만 도입해야 합니다. 초보 엔지니어일수록 무조건 캐시를 적용하길 원하지만, 실제로는 DB 인덱스나 설계 최적화로 충분히 해결 가능한 경우가 많습니다.
"잘못된 캐시는 시스템의 '이상한 상태'가 만들어지는 출발점이다."
대규모 데이터 캐시가 필요하다면, 스케줄링+문서 스토리지(예: S3에 결과 저장) 방식으로 해결하는 등 다양한 전략을 적용합니다.
7. 이벤트 처리와 적절한 활용
대부분 기술 회사는 이벤트 허브(예시: Kafka)를 갖추고 있습니다. 이벤트 허브는 여러 서비스에 "이런 일이 일어났다"를 알려주는 역할을 합니다.
- 과도하게 활용하기보다는, API 연동이 더 적합한 경우도 많음을 기억하세요.
- 이벤트는 후속 처리의 성공 여부가 즉각적으로 중요하지 않거나, 대량이면서 실시간성이 떨어질 때 적합합니다.
8. 데이터 흐름: Pull vs Push
많은 곳으로 데이터를 전달할 때는 Pull(요청 기반)과 Push(서버가 능동적으로 전달) 중 상황에 따라 선택하면 됩니다.
- 적은 수의 서비스에 바뀐 데이터를 제공할 때는 Push가 관리가 쉽습니다.
- 다수의 클라이언트(예: Gmail)에는 필요에 따라 Read-replica 서버나, 이벤트 큐 등으로 확장하게 됩니다.
9. '핫 패스'에 집중하세요
복잡한 시스템에서도 '핫 패스(핵심 경로)'만큼은 특별히 신경써서 설계해야 합니다. 여기서는 실수 한 번에 전체 서비스가 완전히 마비될 수 있습니다.
"설정 페이지는 천 가지 방식으로 만들어도 다들 비슷하게 동작한다. 반면, 사용자 모든 행동을 집계하는 코드에는 선택지 자체가 훨씬 좁다."
10. 로깅과 모니터링: 확실하게
문제를 빠르게 파악하려면 '불행 경로'에서 공격적으로 로그를 남기되, 결국 아름다운 코드보다 실용성이 더 중요합니다.
- 리소스(CPU, 메모리), 큐 크기, 요청/잡별 처리 시간 등 기본적인 운영 메트릭도 필수입니다.
- 평균이 아니라 p95, p99와 같은 상위 퍼센타일 지표도 놓치면 안 됩니다.
"느린 요청 한두 건이 심각한 불만을 가진 주요 고객에서 발생할 수 있다."
11. 실패에 대비하는 자세: 킬스위치와 안전한 실패
불가피하게 시스템 일부가 장애를 겪을 때 어떻게 대처할지 미리 설계해야 합니다.
- 무작정 재시도(retry)는 오히려 부하만 키울 수 있으니, '회로차단기(circuit breaker)' 패턴을 활용한다.
- 중복 실행을 막으려면 idempotency key(불변성 키)를 요청에 포함해, 같은 요청이 재시도돼도 안전하도록 설계합니다.
- 장애 시 '열린 실패(기능을 허용)'와 '닫힌 실패(차단)' 중 선택이 중요합니다.
- 예: 레이트 리미팅은 열린 실패(차단 기능 실패 시 잠깐 허용)가 바람직.
- 인증은 닫힌 실패(차라리 거부)가 안전.
12. 마치며
이 글은 똑똑한 트릭 대신 지루하지만 검증된 패턴과 컴포넌트를 올바른 자리에 사용하는 것이야말로 좋은 시스템 설계임을 여러 번 강조합니다. 대기업일수록 이런 인프라가 이미 다 갖춰져 있기에, 신박한 설계보다 평범한 시스템 설계가 실질적으로 팀과 제품을 지켜줌을 잊지 말라고 당부합니다.
"너무 새롭고 재밌는 걸 하다가는, 결국 엉망진창이 될 수도 있다. 진짜 좋은 시스템 설계는 눈에 띄지 않는다."
결론
요약하자면, 좋은 시스템 설계란 최대한 단순하고, 신뢰할 수 있고, 자기 역할에 충실한 컴포넌트로 이루어진 구조를 의미합니다. 경험이 쌓일수록 화려한 설계보다 '지루하고 평범한 조합의 신뢰성'이 가장 빛난다는 사실, 잊지 마세요. 🚀