🏗️

좋은 시스템 설계란 — 눈에 띄지 않는 설계가 최고의 설계다

상태 관리, DB 설계, 캐싱, 이벤트 처리, 장애 복구까지 — 검증된 단순함의 힘

Sean Goedecke의 글을 기반으로 정리한 시스템 설계 원칙이다.

좋은 설계는 눈에 안 띈다

소프트웨어 설계가 코드의 조립이라면, 시스템 설계는 서비스들의 조합이다. 앱 서버, 데이터베이스, 캐시, 큐, 이벤트 버스, 프록시 — 이런 컴포넌트를 어떻게 배치하느냐의 문제다.

좋은 설계를 만나면 "특별한 문제가 없다", "생각보다 쉽게 끝났다" 같은 반응이 나온다. 반대로 복잡하고 눈에 띄는 설계는 근본 문제를 감추거나 과도한 설계를 나타낸다. 처음부터 화려한 구조를 도입하기보다는, 최소한의 단순한 구조에서 시작해서 점진적으로 발전시키는 쪽이 유리하다.

상태(State)가 가장 어렵다

시스템 설계에서 가장 까다로운 부분이 상태 관리다. 정보를 저장하지 않고 결과만 반환하는 서비스(GitHub의 PDF 렌더링 같은)는 무상태적이다. DB에 쓰기를 하는 서비스는 상태를 관리한다.

상태를 저장하는 컴포넌트를 최대한 줄여야 한다. 시스템 복잡도와 장애 가능성이 동시에 내려간다. 상태 관리를 한 서비스에서만 하고, 나머지는 API 호출이나 이벤트 발생 같은 무상태 역할에 집중하는 구조가 좋다.

데이터베이스 — 스키마와 인덱스

사람이 읽기 쉬운 스키마가 좋다. 너무 유연한 스키마(전부 JSON 컬럼에 넣기)는 애플리케이션 코드에 부담을 준다. 인덱스는 빈번한 쿼리 컬럼에만 건다 — 모든 컬럼에 인덱스를 거는 건 쓰기 오버헤드만 늘린다.

DB가 병목이 되면 조인을 적극 활용한다. 애플리케이션에서 여러 번 쿼리해서 합치는 것보다 DB에서 조인하는 게 대부분 빠르다. ORM 사용 시 루프 안에서 쿼리가 발생하는 N+1 문제를 조심해야 한다.

읽기는 복제본(read-replica)으로 분산하고, 쓰기 연산은 스로틀링을 고려한다.

느린 작업과 빠른 작업의 분리

사용자 인터랙션은 수백 밀리초 내 응답이 필요하다. 시간이 오래 걸리는 작업(대용량 PDF 변환 등)은 최소한의 응답만 프론트에서 돌려주고, 나머지는 백그라운드로 넘긴다. 큐(Redis 등)와 잡 러너 조합이 일반적이다.

멀리 예약된 작업은 Redis보다 DB 테이블로 관리하고 스케줄러로 실행하는 게 실용적이다.

캐싱은 신중하게

캐싱은 비싼 연산 반복을 줄여준다. 근데 주니어 시절에는 모든 걸 캐시하고 싶어지고, 경험이 쌓이면 캐시 도입이 점점 신중해진다.

캐시는 새로운 상태를 도입한다. 동기화 이슈, 스테일 데이터, 무효화 버그 — 전부 캐시에서 나온다. 먼저 인덱스 추가 같은 쿼리 최적화를 시도하고, 그래도 안 되면 그때 캐시를 붙인다.

이벤트 처리

대부분의 기업이 Kafka 같은 이벤트 허브를 갖추고 있다. 근데 이벤트를 남발하면 추적이 어려워진다. 단순한 요청–응답 API가 로깅과 디버깅 면에서 더 낫다.

이벤트 기반 처리는 발신자가 수신자 동작에 신경 안 써도 될 때, 또는 고용량·지연 허용 시나리오에 적합하다.

Push vs Pull

Pull은 단순하지만 반복 요청이 문제가 된다. Push는 변경 시 즉시 전달하므로 효율적이고 최신 데이터 유지에 유리하다. 대량 클라이언트를 처리하려면 각 방식에 맞는 인프라 확장이 필요하다.

핫패스에 집중

시스템에서 가장 많이 트래픽이 흐르는 경로가 핫패스다. 설계 실패 시 서비스 전체에 영향이 가므로, 마이너 기능보다 핫패스에 설계·테스트 자원을 집중해야 한다.

로깅과 관측성

비정상 경로(unhappy path)에 상세 로그를 적극적으로 남긴다. 평균값만 보지 말고 p95, p99 지연 시간도 반드시 관찰한다. 상위 소수의 느린 요청이 핵심 사용자의 문제일 수 있다.

킬스위치와 장애 복구

무작정 재시도는 다른 서비스에 부담만 준다. 회로 차단기(circuit breaker)로 요청을 제어하고, 멱등키(idempotency key)로 중복 처리를 방지한다.

장애 시 fail open(허용)과 fail closed(차단) 중 선택이 필요하다. Rate limiting은 fail open이 사용자 영향이 적다. 인증은 fail closed가 필수다.

결국 지루할 정도로 단순한 설계가 실무에서 살아남는다

기술적으로 특별한 설계는 매우 드물다. 검증된 컴포넌트를 적재적소에 배치하는 게 장기적으로 가장 안정적이다. 눈에 띄지 않고, 충분히 입증된 방법론을 안전하게 조합하는 것 — 그게 좋은 시스템 설계다.

동작 흐름

1

상태(state)를 저장하는 컴포넌트 수를 최소화한다

2

DB 스키마는 읽기 쉽게, 인덱스는 필요한 곳에만

3

느린 작업은 백그라운드로 분리한다

4

캐시는 쿼리 최적화 후에 마지막 수단으로 도입

5

핫패스에 설계·테스트 자원을 집중한다

6

circuit breaker + 멱등키로 장애에 대비한다

사용 사례

웹 서비스 아키텍처 — 앱 서버 + DB + 캐시 + 큐 조합의 기본 설계 마이크로서비스 간 통신 — 이벤트 기반 vs 요청-응답 선택 장애 대응 설계 — fail open/closed, circuit breaker, 멱등키 적용