개요

좋은 시스템 설계는 복잡해 보이지 않고 긴 시간 동안 별문제 없이 돌아가는 상태를 말함 핵심은 상태를 최소화하고, 검증된 단순한 컴포넌트를 필요할 때만 조합하는 방향 과설계나 과도한 신기술 도입은 문제를 감추거나 유지보수 비용을 키우는 경향 최소 기능의 단순 구조에서 시작 후 관측 기반으로 점진 개선 권장

시스템 설계의 정의와 접근

소프트웨어 설계가 코드 조립이라면 시스템 설계는 서비스를 조합하는 일이라는 관점 주요 구성 요소 팔레트

  • 앱 서버
  • 데이터베이스
  • 캐시
  • 큐와 잡 러너
  • 이벤트 버스
  • 프록시와 게이트웨이

좋은 설계의 징후

  • 특별히 문제 삼을 부분 없음
  • 생각보다 쉽게 끝남
  • 신경 쓸 필요가 없는 영역이 존재함

복잡함 그 자체는 신호가 아님

  • 근본 문제를 가리는 장식일 수 있음
  • 초기부터 복잡한 분산 구조 도입 지양
  • 단순한 구성으로 작동을 보장한 뒤 단계적 확장

상태와 무상태

가장 어려운 지점은 상태 관리

  • 상태 저장 컴포넌트 수를 줄일수록 복잡도와 장애 가능성 하락
  • 상태는 가능한 한 한 곳에서만 관리
  • 나머지는 무상태 API 호출이나 이벤트 발행 역할에 집중

무상태 예시

  • 입력을 받아 즉시 결과만 반환하는 렌더링 서비스 같은 경량 계산 경로

상태 예시

  • 데이터베이스에 쓰기 트랜잭션 수행

데이터베이스 설계와 병목

데이터베이스는 상태의 중심이므로 스키마와 인덱스 설계, 병목 해소 전략이 핵심

스키마와 인덱스

  • 사람 읽기 쉬운 스키마 선호
  • 전체를 JSON 컬럼에 몰아넣는 과유연 스키마는 애플리케이션 복잡도와 성능 리스크 상승
  • 자주 질의되는 컬럼 기준의 선택적 인덱스 설계
  • 모든 컬럼 인덱싱은 쓰기 오버헤드와 공간 낭비 유발

병목 해소 패턴

  • DB 접근이 병목의 상수항이 되는 경우 많음
  • 가능하면 조인 같은 데이터 결합은 DB에서 처리하는 편이 유리
  • ORM 사용 시 루프 내 쿼리 발생 등 N+1 주의
  • 필요 시 쿼리 분할로 복잡도와 부하 분산
  • 읽기 부하는 복제본으로 분산해 쓰기 노드 보호
  • 트랜잭션과 대량 쓰기 폭주 시 쿼리 스로틀링 고려

느린 작업과 빠른 작업 분리

사용자 인터랙션 경로는 수백 ms 내 응답 목표 오래 걸리는 작업은 최소 확인만 동기 처리하고 백그라운드로 위임하는 패턴 추천 백그라운드 실행 구조의 일반형

  • 큐에 작업 적재
  • 잡 러너가 소비

장기 예약 작업은 별도 DB 테이블과 스케줄러 조합이 실용적일 때 많음

캐싱

캐시는 동일 계산 반복이나 고비용 연산의 비용 절감에 유효 그러나 캐시는 새로운 상태를 도입하므로 동기화 이슈와 스테일 데이터 리스크 존재 원칙

  • 먼저 쿼리 최적화와 인덱싱으로 기본기 강화 후 캐시 도입 검토
  • 대용량 결과물은 Redis/Memcached 대신 문서 저장소에 주기 스냅샷 저장 전략도 옵션
  • 캐시 무효화 규칙과 만료 정책의 단순화 우선

이벤트 처리

이벤트 허브 기반 분산 처리는 고용량과 지연 허용 케이스에서 적합 단순 요청–응답 API가 로깅과 문제 해결에 유리한 경우가 많음 발신자가 수신자 내부 동작을 몰라도 되는 독립성이 필요할 때 이벤트 채택 고려

데이터 전달 방식 Pull vs Push

Pull

  • 요청 후 응답 모델
  • 단순하지만 폴링 오버헤드와 과부하 가능성 존재

Push

  • 데이터 변경 시 즉시 전달
  • 효율적이며 최신성 유지에 유리

대량 클라이언트 처리 시 각 방식에 맞춘 인프라 증설 필요

  • 이벤트 큐, 팬아웃, 캐시 계층, 연결 수 관리 등

핫패스 집중

핫패스는 시스템 내 데이터가 많이 흐르고 사업 임팩트가 큰 경로를 의미 선택지가 적고 실패 영향이 크므로 신중한 설계와 집중 테스트 필요 마이너 기능의 옵션 다양화보다 핫패스 최적화에 자원 우선 배분

로깅, 메트릭, 추적

비정상 경로 로깅 강화

  • 에러 원인과 컨텍스트 수집

관측성 기초 지표 수집

  • CPU와 메모리
  • 큐 길이
  • 요청 지연과 작업 소요 시간

평균값 대신 분포 지표 관찰

  • p95, p99 지연에 주목
  • 상위 소수의 느린 요청이 핵심 사용자 문제일 수 있음

분산 추적으로 경계 간 지연과 재시도 파악

킬스위치, 재시도, 장애 복구

킬스위치로 기능 단위 차단 가능 상태 유지 무작정 재시도는 장애 전파와 증폭 위험

  • 회로 차단기로 하류 서비스 보호
  • 지수 백오프와 재시도 예산 관리

멱등키로 중복 처리 방지 장애 모드 선택

  • fail open이 사용자 영향이 적은 경로 예시로 Rate Limiting
  • 인증과 결제는 fail closed 필수

DB 조인 vs 애플리케이션 조인 논의 요약

관점 A

  • 복잡한 데이터 결합은 DB가 담당하는 게 성능과 단순성에서 유리
  • 뷰와 저장 프로시저는 데이터 추상화 계층으로 유효

관점 B

  • 확장성 요구가 큰 구조에서는 DB는 단순 인덱스 조회 역할에 집중
  • 조인과 집계는 수평 확장 쉬운 백엔드에서 처리해 자원 증설로 스케일링
  • 대역폭 관점에서 필요한 데이터만 가져와 애플리케이션에서 결합이 유리한 케이스 존재

실무 결론

  • 기본값은 DB에 맡기되 데이터 폭발, 팬아웃, 대역폭 병목, 캐시 전략과 결합 등 조건에서 예외 설계 고려
  • 관측 지표와 비용 곡선으로 선택 검증

스키마 설계와 유연성

스키마의 과유연은 앱 복잡도와 성능 문제 야기

  • 모든 것을 JSON 컬럼이나 EAV로 수용하는 설계는 가독성과 최적화 모두에 독
  • 정규화 가능한 경우엔 정규화 선호
  • 사람이 봐도 용도가 직관적인 테이블 구조 우선

단, 임시 수용을 위한 제한적 JSONB 같은 완충은 특정 상황에서 유용할 수 있으나 기본 규칙은 아님

ORM 사용 시 주의

단일 오브젝트 모델 강제는 복잡한 조회 시 유연성 저하 위험 N+1, 비효율 조인, 불필요 데이터 로딩 주의 필요 시 뷰나 핸드크래프트 쿼리로 경로 단순화

여러 서비스의 DB 공유 vs API 단일 쓰기 경로

권장 기본값

  • 한 서비스만 테이블에 쓰기 책임 부여
  • 나머지는 API 호출 또는 이벤트 발행으로 상호작용

반론과 현실 제약

  • 여러 서비스가 DB를 직접 접근하면 권한과 트랜잭션, 커스텀 쿼리 활용이 쉬움
  • 그러나 스키마 변경 시 영향 반경이 커지고 마이그레이션 제약 증가

실무 결론

  • 변화 적응성과 조정 범위 최소화를 우선시하면 API가 유리
  • 공유 DB 구조는 소규모·단기에는 빠를 수 있으나 규모 확대 시 실패 사례가 다수 보고됨
  • 구조 변경 시 영향을 받는 팀 수를 지표로 삼아 결정

데이터 전달 인터페이스 우선순위

시스템 설계의 핵심은 사용자가 맞닥뜨리는 인터페이스 설계

  • 기능 제공과 교환비용의 명확화가 본질
  • 내부 구조는 인터페이스 안정성을 지키며 후속 교체 가능

설계 논의 시간의 다수를 인터페이스 정의와 버전 전략에 투자 권장

상태와 운영 책임의 분리

무상태 컨테이너 경로는 배포와 롤백으로 복구 쉬움 상태가 붙는 DB와 파일 스토리지는 전담 운영 책임 필요

  • 백업과 복구 전략 부재는 정상 운영 중에도 잠재 리스크
  • 사고 대응은 수 분 내 배포로 해결되지 않는 영역 존재

캐시와 문서 저장소 선택지

키값 캐시가 아닌 문서 저장소에 주기적 결과 저장 전략

  • 대용량 결과물 재사용 시 비용 절감 효과
  • 만료와 불일치 관리 단순화 가능

Pull과 Push 확장 전략

Pull 고도화

  • 조건부 요청과 캐시 검증 헤더 활용
  • 백오프와 지능형 폴링 간격 조절

Push 고도화

  • 팬아웃을 큐로 비동기화
  • 연결 수 관리와 샤딩으로 수평 확장

관측과 핫패스 최적화 체크리스트

  • 핫패스 정의와 경로 단순화
  • p95, p99 지연 모니터링과 예산 설정
  • 실패 로그의 샘플링과 컨텍스트 상관ID 부여
  • 큐 적체 알람과 처리율 자동 확장
  • 스로틀링과 회로 차단기 정책 점검
  • 멱등키 적용 범위 정의

HN에서 나온 면접과 복잡도 논의 요약

  • 현실의 좋은 설계는 단순함이 미덕이지만 면접 신호로는 복잡한 다이어그램이 과대평가되는 경향 존재
  • 면접에서는 결정을 내린 이유와 고려 요소를 구조화해 설명하는 능력이 중요하다는 반론 다수
  • SQL과 NoSQL 선택은 팀 친숙도만이 아니라 작업 부하 특성과 데이터 모델 차이가 중요하다는 의견 존재
  • 복잡함 숭배가 과설계를 장려한다는 비판과, 그래도 면접은 양방향이므로 의도한 평가 포인트에 맞춰 소통해야 한다는 조언 공존

DB 공유와 서비스 경계에 대한 HN 논의 요약

  • 여러 서비스가 한 DB를 공유하면 분산 시스템이 이미 형성된 셈이라 관리 복잡도 상승 가능
  • 반대로 서비스 API가 높은 수준의 인터페이스가 되면 인증과 트랜잭션, 예외처리를 직접 구현해야 하는 부담 발생
  • 장기 유지보수성과 변경 용이성 관점에서 공유 DB보다 API 경계가 유리하다는 경험담 다수
  • DB 분리 시 가용성 계산과 운영 복잡도 증가도 현실 제약으로 고려 필요

저장 프로시저와 SQL 추상화에 대한 HN 논의 요약

  • 뷰와 저장 프로시저는 데이터 추상화에 유효하다는 견해와, 팀 언어와 도구 표준화 관점에서 유지보수 난이도가 높다는 반론 공존
  • ORM은 큰 서비스에서 유연성을 제한하고 성능 문제 유발 가능성이 있어 뷰와 핸드크래프트 쿼리 병행 제안 다수

boolean vs timestamp 논의 요약

  • 단순 boolean은 상태 확장 시 필드 난립으로 이어지므로 timestamp나 enum으로 진화 가능성 확보가 낫다는 주장 존재
  • 반론으로 논리적 boolean이 의미상 더 적합하고 데이터 크기와 분석 효율 측면에서 유리한 케이스도 있음
  • 실무 결론은 도메인 변화 방향과 감사 요구에 따라 선택하며, 변경 시점이 중요한 경우 별도 감사 테이블 도입이 더 적합

느린 작업 처리와 인프라 선택

  • 단기 대기열은 인메모리 큐로 충분
  • 장기 스케줄은 DB 기반 스케줄 테이블과 워커 조합이 단순하고 운영이 쉬움

마무리

좋은 시스템 설계의 기본값은 단순함과 상태 최소화 데이터베이스 중심의 스키마와 인덱스, 핫패스 최적화, 관측 가능성 확보가 토대 캐시와 이벤트, 백그라운드 처리, 복잡한 분산 구성은 필요한 순간에만 단계적으로 도입 기술적으로 특별한 설계는 드물고, 지루할 정도로 단순한 구성의 안정적 운영이 장기적으로 가장 강함

참고자료