[System] Everything I know about good system design, 좋은 시스템 설계
좋은 시스템 설계란 복잡해 보이지 않고 오랜 기간 별다른 문제가 발생하지 않는 형태- 상태(state) 를 다루는 것이 시스템 설계에서 가장 어려운 부분이며, 가능한 한 상태를 저장하는 컴포넌트 수를 줄이는 방향이 중요
- 데이터베이스는 주로 상태가 보관되는 위치로, 스키마 설계와 인덱싱, 병목 해소에 중점을 둔 접근이 필요
- 캐싱, 이벤트 처리, 백그라운드 작업 등은 성능 및 유지보수를 위해 신중하게 도입해야 하며, 남용을 피하는 것이 좋음
- 복잡한 설계보다는 충분히 검증된 간단한 컴포넌트 및 방법론을 적절하게 사용하는 것이 지속 가능하고 안정적인 시스템 구축에 핵심
시스템 설계의 정의와 전체적인 접근
- 소프트웨어 설계가 코드의 조립이라면, 시스템 설계는 다양한 서비스를 조합하는 과정
- 시스템 설계의 주요 구성 요소는 앱 서버, 데이터베이스, 캐시, 큐, 이벤트 버스, 프록시 등
- 좋은 설계는 "특별한 문제가 없다", "생각보다 쉽게 끝나다", "이 부분은 신경 쓸 필요가 없다"와 같은 반응을 이끌어냄
- 반대로 복잡하고 눈에 띄는 설계는 근본 문제를 감추거나, 과도한 설계를 나타낼 수 있음
- 복잡한 시스템은 초기부터 바로 도입하기보다는, 최소한의 작동 가능한 단순한 구조에서 점차 발전하는 방향이 유리함
상태(state)와 무상태(stateless)의 구분
- 소프트웨어 설계에서 가장 까다로운 부분이 바로 상태 관리
- 정보를 저장하지 않고 즉시 결과를 반환하는 서비스(GitHub의 PDF 렌더링과 같은)는 무상태적임
- 반면 데이터베이스에 쓰기를 수행하는 서비스는 상태를 관리함
- 시스템 내 상태 저장 컴포넌트를 최대한 줄이는 것이 좋음. 이는 시스템의 복잡도와 장애 발생 가능성을 낮춤
- 상태 관리를 한 서비스에서만 수행하고, 나머지 서비스는 API 호출 혹은 이벤트 발생 등 무상태 역할에 집중하는 구조가 권장됨
데이터베이스 설계와 병목 지점
스키마 및 인덱스 설계
- 데이터 보관을 위해서는 사람이 읽기 쉬운 형태의 스키마 설계가 필요함
- 너무 유연한 스키마(예: 전체를 JSON 컬럼에 저장)는 애플리케이션 코드 및 성능에 부담을 줄 수 있음
- 쿼리가 빈번하게 발생할 컬럼을 기준으로 적절한 인덱스를 설정해야 함. 모든 것에 인덱스를 거는 것은 오히려 쓸데없는 오버헤드 발생
병목 해결 방법
- 데이터베이스 접근이 종종 무거운 병목이 됨
- 가능한 한 복잡한 데이터를 애플리케이션에서가 아닌 데이터베이스 내에서 조인(JOIN) 등으로 처리하는 것이 성능상 유리함
- ORM 사용 시, 루프 안에서 쿼리를 발생시키는 실수를 주의해야 함
- 필요에 따라 쿼리를 분할하여, 데이터베이스의 부담이나 쿼리 복잡도를 조절하는 것도 한 방법
- 읽기 쿼리는 복제본(read-replica)으로 분산하여, 주(Write) 노드의 부하를 줄이는 전략이 효과적
- 대량의 쿼리가 몰릴 때 트랜잭션 및 쓰기 연산은 데이터베이스를 쉽게 과부하 상태로 만들 수 있으므로, 쿼리 스로틀링(제한) 처리를 고려해야 함
느린 작업과 빠른 작업의 분리
- 사용자가 인터랙션하는 작업은 수백 밀리초 내 응답이 필요
- 시간이 오래 걸리는 작업(예: 대용량 PDF 변환 등)은 최소한의 작업만 프론트에서 즉시 제공하고, 나머지는 백그라운드로 넘기는 패턴이 효과적
- 백그라운드 작업은 일반적으로 큐(예: Redis)와 잡 러너가 묶여 동작
- 멀리 예약된 작업 처리는 Redis보다 DB 테이블을 별도로 만들어 관리하고, 스케줄러를 이용하여 실행하는 형태가 실용적
캐싱
- 캐싱은 동일하거나 비싼 연산을 반복하는 경우 비용 절감과 성능 향상에 기여
- 보통 캐시를 처음 배운 주니어 엔지니어는 모든 것을 캐시하고 싶어하고, 경험 많은 엔지니어일수록 캐시 도입은 신중
- 캐시는 새로운 상태를 도입하므로, 동기화 이슈/오류/스테일 데이터 등의 위험이 존재
- 먼저 쿼리의 인덱스 추가 같은 성능 개선을 시도한 뒤 캐싱을 적용하는 것이 바람직
- 대용량 캐시는 Redis/Memcached가 아니라 S3/Azure Blob Storage와 같은 문서 저장소에 주기적으로 저장하는 방식도 활용 가능
이벤트 처리
- 대부분의 기업은 이벤트 허브(예: Kafka) 를 갖추고, 다양한 서비스가 이벤트 기반으로 분산 처리
- 이벤트의 남발보다는, 단순한 요청–응답 API 설계가 로깅과 문제 해결 면에서 더 유용
- 이벤트 기반 처리는 발신자가 수신자 동작에 신경 쓰지 않아도 될 때, 혹은 고용량·지연 허용 시나리오에 적합
데이터의 전달 방식: 푸시와 풀
- 데이터 전달에는 Pull(요청 후 응답) 과 Push(변경 시 자동 전달) 두 방식이 있음
- Pull 방식은 단순하지만 반복 요청/과부하 문제가 발생
- Push 방식은 서버에서 데이터 변경 시 클라이언트에 즉시 전달하므로, 효율적이며 최신 데이터 유지에 유리
- 대량 클라이언트 처리에는 각각 방식에 맞게 인프라(이벤트 큐, 여러 캐시 서버 등) 확장이 필요
핫패스(Hot Paths) 집중
- 핫패스란 시스템 내에서 가장 중요하고 데이터가 많이 흐르는 경로를 의미
- 핫패스는 선택지가 적고, 설계 실패 시 서비스 전체에 심각한 문제를 유발할 수 있으므로, 신중한 설계가 필수
- 옵션이 다양한 마이너 기능보다, 핫패스에 집중하여 설계 및 테스트에 자원을 배분하는 것이 효과적
로깅, 메트릭, 추적
- 장애 발생 시 원인 진단을 위해, 비정상 경로(unhappy path)에 대한 상세 로그 기록을 적극적으로 해야 함
- 시스템 자원(CPU/메모리), 큐 크기, 요청/작업 시간 등 기본적인 관측성 지표 수집이 필요
- 평균값만 보는 대신 p95, p99 지연 시간 같은 분포 지표도 반드시 관찰해야 함. 상위 소수의 느린 요청이 핵심 사용자의 문제일 수 있음
킬스위치, 재시도, 장애 복구
- 킬스위치(시스템 일시 차단) , 재시도의 전략적 활용이 중요함
- 무작정 재시도는 다른 서비스에 부담만 주며, 사전에 회로 차단기(circuit breaker) 등으로 요청을 제어해야 효과적임
- Idempotency Key(멱등키) 도입으로 동일 요청 재처리 시 중복 작업 방지가 가능함
- 일부 장애 상황에서 열린 실패(fail open) 또는 닫힌 실패(fail closed) 중 선택이 필요함. 예를 들어, Rate Limiting은 fail open(허용) 쪽이 사용자 영향이 적은 방향임. 반면 인증은 fail closed가 필수임
마무리
- 서비스 분리, 컨테이너, VM 도입, 트레이싱 등 일부 주제는 생략됐으나, 잘 검증된 컴포넌트를 적재적소에 사용하는 것이 장기적으로 가장 안정적인 시스템 구축으로 이어짐
- 기술적으로 특별한 설계는 실제로 매우 드물며, 지루할 정도로 단순한 설계가 오히려 실무에서 가장 자주 쓰임
- 본질적으로 좋은 시스템 설계란 눈에 띄지 않고, 충분히 입증된 방법론을 안전하게 조합하는 과정임
https://news.hada.io/topic?id=22580
좋은 시스템 설계 | GeekNews
좋은 시스템 설계란 복잡해 보이지 않고 오랜 기간 별다른 문제가 발생하지 않는 형태상태(state) 를 다루는 것이 시스템 설계에서 가장 어려운 부분이며, 가능한 한 상태를 저장하는 컴포넌트 수
news.hada.io
'프로그래밍 > Architect' 카테고리의 다른 글
불확실할 때도 결정은 해야 한다··· 결단력 있는 리더가 되기 위한 9가지 비결 (0) | 2025.02.19 |
---|---|
빠르게 변화하는 시대에 대응하는 IT리더의 특징 4가지 (3) | 2024.12.02 |
IT 리더의 2025년 목표, ‘IT 인식 회복’이 우선이다 (2) | 2024.11.28 |
채용 계획 없는데도 “인재 모십니다” · · · ‘유령 일자리 공고’ 주의보 (1) | 2024.11.26 |
좋은 소프트웨어 개발 습관 (zarar.dev) (1) | 2024.11.25 |