[시스템 설계] 1. 시스템 설계의 기본
0. 들어가며
백엔드 개발을 하다 보면 개별 기술보다 큰 그림, 즉 시스템 전체 구조를 어떻게 설계할지가 더 중요할 때가 많다. 이번 시리즈는 길벗 출판사의 [요즘 개발자를 위한 시스템 설계 수업]을 읽고 정리할 것이다.
책의 내용을 단순히 요약하기보다는 내가 경험한 사례와 면접 대비 관점에서 다시 해석하는 것을 목표로 한다. 예를 들어 로드밸런서, 분산 캐시, 메시징 시스템 같은 개념을 설명할 때, 실제 프로젝트에서 어떻게 적용했는지 덧붙일 예정이다.
이 글에서는 시스템 설계의 정의와 중요성, 그리고 업계 사례를 다룬다. 본격적인 설계 방법론으로 들어가기 전, 시스템 설계를 바라보는 기본 관점을 정리한다.
1. 시스템 설계의 정의
소프트웨어 시스템 설계는 특정 요구 사항을 충족하기 위해 아키텍처, 구성요소, 인터페이스 및 여러 특성을 정의하는 과정이다.
다음 세 가지 관점을 통해 시스템 설계를 정의한다.
- 소프트웨어 시스템
- 분산 소프트웨어 시스템
- 시스템 설계의 이해
1.1. 소프트웨어 시스템
소프트웨어 시스템은 특정 작업이나 일련의 작업을 수행하기 위해 함께 동작하는 컴포넌트, 모듈, 프로그램의 집합이다.
일반적으로 데이터 관리, 트랜잭션 처리, 최종 사용자 서비스 제공 등 원하는 기능을 실행할 수 있는 연관된 애플리케이션으로 구성된다.
시스템은 단순한 프로그램처럼 간단할 수도 있고, 분산 시스템처럼 복잡할 수도 있다.
개발 과정은 애플리케이션과 유사하게 언어 선택 -> 설계 방법론 결정 -> 개발 및 운영(버그 수정과 업데이트 포함) 과정을 거친다. 이러한 활동은 모두 설계 단계에서 고려되어야 한다.
1.2. 분산 소프트웨어 시스템 (Distributed Software System)
분산 시스템은 여러 독립적인 컴포넌트나 프로세스, 노드로 구성되며, 이들은 서로 통신하며 하나의 공동 목표를 달성한다.
중앙 집중형 시스템(모놀리식 아키텍처)과 달리 분산 시스템은 지리적으로 다른 위치의 기기와 네트워크에 걸쳐 존재한다.
각 구성 요소는 원격 프로시저 호출(RPC), 메시지 전달(Message Passing), 발행/구독 모델(Publish/Subscribe) 같은 다양한 통신 프로토콜을 통해 상호작용한다.
주로 확장성, 가용성, 장애 허용성이 중요한 대규모 애플리케이션에 활용된다. 클라우드 컴퓨팅 플랫폼(AWS, GCP 등), P2P 네트워크, 분산 데이터베이스, CDN이 대표적인 예시이다.
분산시스템 예시 |
위 다이어그램은 여러 기기와 네트워크로 구성된 분산 시스템의 예시이다.
- Desktop / Mobile: End-User 요청
- CDN: 정적 리소스를 캐싱해 사용자에게 전달
- DNS → Load Balancer: 요청을 적절한 애플리케이션 서버로 라우팅
- Application Server: 비즈니스 로직 처리 후 DB/Storage와 통신
- Cache / Proxy / DB Cluster: 데이터 조회 성능 향상과 고가용성 확보
- Storage: 이미지, 파일 등 대용량 객체 저장
1.3. 시스템 설계의 이해
시스템 설계는 소프트웨어 시스템의 아키텍처, 컴포넌트, 모듈, 인터페이스, 상호작용을 정의하여 기능적·비기능적 요구사항을 충족하는 과정이다. 즉, 요구사항을 시스템의 구조/구현/운영 방식으로 구체화하는 작업이다.
목표는 다음과 같다.
- 이해하기 쉬운 구조
- 유지보수와 확장이 용이한 아키텍처
- 성능, 안정성, 신뢰성, 보안 요구사항 충족
- 변경된 요구사항이나 환경에도 유연하게 대응 가능
시스템 설계 과정
- 요구사항 분석
기능적/비기능적 요구사항을 식별한다. 특히 데이터 조회/저장 패턴을 분석해 이후 설계에 반영한다. - 상위 수준 아키텍처 설계
시스템을 이루는 컴포넌트, 모듈, 인터페이스를 정의한다. - 상세 설계
각 컴포넌트의 비즈니스 로직, 알고리즘, 상호작용 방식을 설계한다. - 사용자 인터페이스 설계
API 및 UI/UX 프로토타입을 정의하고, 백엔드와의 통신 방식을 설계한다. - 데이터베이스 설계
데이터 모델과 저장 메커니즘을 정의한다.
최종 산출물은 아키텍처 다이어그램, 상세 설계 문서, API 정의서 등으로 남는다. 이는 구현 단계의 기준선이자 운영 단계의 참고 자료로 활용된다.
2. 시스템 설계의 다양한 유형
시스템 설계는 크게 상위 수준 아키텍처 설계와 하위 수준 상세 설계로 나뉜다.
2.1. 상위 수준 아키텍처 설계
상위 수준 설계는 시스템의 전체 구조와 흐름을 정의하는 단계로, 구현 세부사항보다는 아키텍처 전반에 초점을 맞춘다.
- 시스템 아키텍처: 컴포넌트와 구성 요소 간 관계, 통신 방식을 정의한다.
- 데이터 흐름: 데이터가 수집·저장·처리되는 과정을 설계한다.
- 확장성: 부하가 증가해도 성능 저하 없이 처리할 수 있는 능력.
- 장애 허용(Fault Tolerance): 오류 발생 시에도 기능을 지속적으로 제공하는 능력.
2.1.1. 시스템 아키텍처
아키텍처 패턴을 선택할 때는 이유와 적합성을 명확히 하고, 시스템 목표와 일치하는지 검증해야 한다.
- Monolithic: 하나의 독립 애플리케이션에 모든 기능을 포함
- Client–Server: 클라이언트가 서버에 요청, 서버가 응답하는 구조
- Microservices: 네트워크로 통신하는 독립 서비스 집합
- Event-Driven: 비동기 이벤트나 메시지를 통해 상호작용
2.1.2. 데이터 흐름
데이터 흐름은 성능과 확장성에 직접 영향을 준다.
- 수집: API 호출, 실시간 스트리밍, 주기적 폴링 등
- 저장: 조회 빈도, 일관성, 성능 요구사항에 따라 적합한 저장소를 선택
- 검색: 응답 시간, 캐시 전략, 부하 분산 등을 고려
예: 과거 송금 서버에서는 5분 주기로 송금 요청을 폴링(polling)해 처리했다.
송금량이 늘면서 큐 대기와 중복 처리 문제가 발생해, 이벤트 기반 워커큐 구조로 전환했다.
전환 후에는 처리 지연이 3~5분에서 실시간 수준으로 단축되었고, DB 부하도 크게 줄었다.
이처럼 데이터 수집·처리 방식을 어떻게 설계하느냐에 따라 시스템의 응답성과 안정성이 크게 달라진다.
2.1.3. 장애 허용 (Fault Tolerance)
일부 구성 요소에 장애가 발생해도 시스템이 정상 동작할 수 있는 능력.
복제, 중복성, 점진적 성능 저하, 모니터링, 자가복구 등이 핵심 고려사항이다.
2.2. 하위 수준 상세 설계
하위 수준 설계는 시스템 구성요소의 구현 세부사항에 중점을 둔다.
목표는 성능, 메모리 사용량, 코드 유지보수성을 최적화하는 것이다.
주요 요소는 다음과 같다.
- 알고리즘: 데이터 처리나 문제 해결을 위한 절차.
- 데이터 구조: 메모리 내에서 데이터를 효율적으로 관리하는 방식.
- API: 컴포넌트나 서비스 간 상호작용을 정의하는 인터페이스.
- 코드 최적화: 코드의 성능, 가독성, 유지보수성을 높이기 위한 개선 기법.
하위 수준 설계는 결국 시스템의 구현, 인터페이스, 최적화 전략을 구체화하는 단계다.
2.2.1. 알고리즘
효율적인 알고리즘 선택은 시스템의 성능과 자원 사용량, 유지보수성에 직접적인 영향을 준다.
선택 시 고려해야 할 요소는 다음과 같다.
- 시간 복잡도: 입력 크기에 따라 연산 횟수가 증가하는 비율.
- 공간 복잡도: 입력 크기에 따라 메모리 사용량이 증가하는 비율.
- 트레이드오프: 요구사항과 제약 조건에 따라 시간·공간 복잡도 간의 균형을 조정.
예: 송금 트랜잭션 처리 시, 거래 한도 검증과 같은 단순 연산은 메모리상에서 즉시 계산하고,
송금 집계나 일일 한도 갱신처럼 비용이 큰 연산은 Redis 워커큐에 위임해 비동기로 처리했다.
이를 통해 핵심 트랜잭션 응답 시간은 평균 300ms 내로 유지하면서도,
배치 처리 구간에서는 데이터 정합성과 일관성을 함께 확보할 수 있었다.
2.2.2. 데이터 구조
데이터 구조는 시스템 성능과 자원 효율성에 직접적인 영향을 미친다.
주로 사용되는 구조로는 배열, 연결 리스트, 해시테이블, 트리, 그래프 등이 있다.
선택 시 고려사항은 다음과 같다.
- 데이터 접근 방식: 읽기, 쓰기, 갱신 빈도와 형태.
- 쿼리 성능: 검색·삽입·삭제 등의 작업 복잡도.
- 메모리 사용량: 구조 유지에 필요한 메모리 크기.
예: 알림(notification) 데이터를 1초 단위로 폴링하면서 DB 부하가 급격히 증가한 적이 있다.
CPU 사용률의 대부분이 동일 쿼리 반복 실행에 소모되어, 최근 데이터는 Redis에 캐싱하고
조회 조건 컬럼에 인덱스를 추가했다.
캐싱 적용 후 동일 부하 환경에서 DB CPU 사용률이 절반 이하로 감소했고,
응답 지연도 200ms 이상 단축되었다.
2.2.3. API
API는 시스템 내 여러 구성요소가 상호작용할 수 있도록 하는 규약이다.
명확한 API 설계는 하위 호환성을 유지하고, 시스템 간 결합도를 낮춘다.
API 설계 시 고려할 사항:
- 일관성: 모든 컴포넌트가 동일한 원칙으로 설계되어야 한다.
- 보안: 인증, 권한, 입력 유효성 검증을 통해 무단 접근을 방지한다.
- 성능: 응답 시간 최소화, 네트워크 오버헤드 감소를 목표로 한다.
예: 내부 서비스 간 통신에서는 gRPC를, 외부 클라이언트와의 통신에는 REST API를 사용했다.
REST는 HTTP 기반으로 접근성이 높고 요청 추적이 용이해 외부 서비스에 적합했지만,
gRPC는 바이너리 프로토콜 기반으로 직렬화 오버헤드가 적어 내부 서비스 간 호출에 유리했다.
이처럼 서비스 특성과 통신 경로에 따라 프로토콜을 구분함으로써, 확장성과 응답 시간을 균형 있게 유지할 수 있었다.
2.2.4. 코드 최적화
코드 최적화는 기능을 유지하면서 성능, 가독성, 유지보수성을 향상시키는 과정이다.
주요 기법은 다음과 같다.
- 리팩터링: 구조를 개선하여 코드 품질 향상.
- 루프 언롤링(Loop Unrolling): 반복문 실행 오버헤드를 줄이는 기법.
- 메모이제이션(Memoization): 동일 연산 결과를 캐싱하여 재계산을 최소화.
- 병렬 처리(Parallelism): 작업을 분할해 동시에 실행함으로써 처리 속도 향상.
예: 송금 트랜잭션 처리 로직에서 불필요한 DB 접근이 많아 CPU와 I/O 부하가 컸다.
동일 요청 내에서 반복적으로 조회되는 데이터는 1차 캐시나 메모리 맵으로 재사용하고,
정규화된 테이블 간 조인은 QueryDSL을 활용해 필요한 필드만 조회하도록 수정했다.
그 결과 트랜잭션 단위 처리 시간이 약 40% 단축되었고, CPU 사용률도 안정적으로 유지되었다.
댓글남기기