마이크로서비스에 특화된 다양한 설계 패턴을 소개하려고 합니다. 본격적인 패턴 설명에 앞서 마이크로서비스에 대한 기본 개념과 함께 이로 인해 발생하는 도전 과제를 간단히 정리해 보겠습니다.
마이크로서비스란 무엇인가?
마이크로서비스란 특정 기능에 집중하며 독립적으로 배포 가능한 소규모 컴포넌트를 말합니다. 각 마이크로서비스는 자체 프로세스에서 실행되며, 일반적으로 API를 통해 다른 서비스와 통신합니다. 이 서비스들은 느슨하게 결합(loose coupling)되어 있어 확장성, 개발 속도, 유지보수 측면에서 많은 장점을 제공합니다.
모놀리식 애플리케이션 vs 마이크로서비스 애플리케이션
마이크로서비스 아키텍처는 모놀리식 애플리케이션과 비교했을 때 다음과 같은 주요 장점을 제공합니다.
-
확장성
각 마이크로서비스를 독립적으로 확장할 수 있어 리소스 사용을 최적화할 수 있습니다. -
유연성
서로 다른 마이크로서비스를 각기 다른 기술로 개발, 테스트, 배포, 유지보수할 수 있습니다. -
빠른 개발 속도
작은 단위의 팀이 각기 다른 마이크로서비스를 병렬로 개발할 수 있어 개발 주기와 출시 속도가 빨라집니다. -
높은 복원력
특정 마이크로서비스에 장애가 발생하더라도 전체 시스템에 미치는 영향이 적어 안정성이 높아집니다. -
유지보수 용이성
각 마이크로서비스가 작고 독립적인 코드베이스로 구성되어 있어 이해하기 쉽고 수정 및 디버깅이 용이합니다. -
아웃소싱 유연성
비즈니스 기능을 제3자 파트너에게 아웃소싱할 때 지적 재산 보호가 중요합니다. 마이크로서비스 아키텍처는 파트너별 구성 요소를 격리하여 핵심 서비스를 안전하게 보호할 수 있도록 도와줍니다.
하지만 마이크로서비스 아키텍처는 다음과 같은 도전 과제도 수반합니다.
-
복잡성 증가
마이크로서비스 기반 애플리케이션을 개발하고 유지보수하는 것은 모놀리식보다 더 많은 노력을 요구합니다. 각 서비스마다 별도의 코드베이스, 테스트, 배포 파이프라인, 문서화가 필요합니다. -
서비스 간 통신 문제
네트워크 통신에 의존하기 때문에 지연(latency), 장애, 복잡한 통신 처리 문제 등이 발생할 수 있습니다. -
데이터 관리
마이크로서비스마다 별도의 데이터베이스를 사용하는 경우가 많아 데이터 일관성, 동기화, 트랜잭션 관리에서 어려움을 겪을 수 있습니다. -
배포 부담
여러 마이크로서비스의 배포, 버전 관리, 확장을 관리하려면 Kubernetes와 같은 고급 오케스트레이션 및 자동화 도구가 필요합니다. -
보안
각 마이크로서비스가 새로운 취약점을 초래할 수 있어 공격 표면이 넓어지며, 이에 따라 보안 관리가 더욱 중요해집니다.
주요 마이크로서비스 설계 패턴
이제 마이크로서비스 아키텍처에서 흔히 사용되는 주요 설계 패턴 10가지를 자세히 살펴보겠습니다.
1. Database Per Service Pattern
Database Per Service Pattern은 각 마이크로서비스가 독립적인 데이터베이스를 가지도록 하는 설계 방식입니다. 이렇게 하면 느슨한 결합을 유지할 수 있으며, 각 팀이 서비스에 적합한 데이터베이스 기술을 선택할 수 있습니다.
장점
- 느슨한 결합: 각 서비스가 독립적으로 동작하므로 모듈성이 향상됩니다.
- 기술 선택의 유연성: 각 서비스에 가장 적합한 데이터베이스를 선택할 수 있습니다.
- 확장성: 서비스별로 독립적인 확장 전략을 세울 수 있어 리소스 사용이 최적화됩니다.
단점
- 복잡성 증가: 여러 데이터베이스의 백업, 복구, 확장 등을 관리하는 데 어려움이 있습니다.
- 교차 서비스 쿼리 문제: 여러 데이터베이스에 걸쳐 데이터를 조회하는 것이 어려울 수 있습니다.
- 데이터 일관성 문제: 데이터 일관성을 유지하기 위해 Event Sourcing이나 Saga 패턴과 같은 추가적인 설계가 필요할 수 있습니다.
2. API Gateway Pattern
API Gateway Pattern은 모든 클라이언트 요청을 단일 엔드포인트(API 게이트웨이)를 통해 처리하는 설계 방식입니다. 이 게이트웨이는 요청을 적절한 마이크로서비스로 라우팅하고, 인증, 로깅, 로드 밸런싱과 같은 공통 기능을 제공합니다.
장점
- 클라이언트 상호작용 단순화: 클라이언트가 다수의 마이크로서비스에 직접 요청하는 대신, 하나의 게이트웨이만 호출하면 됩니다.
- 중앙 집중식 관리: 인증, 로깅, 모니터링 등 공통 기능을 게이트웨이에서 통합 관리할 수 있습니다.
- 보안 향상: 게이트웨이에서 접근 제어, 요청 검증 등을 처리하여 보안을 강화할 수 있습니다.
단점
- 단일 장애 지점: 게이트웨이가 장애를 겪으면 전체 시스템이 영향을 받을 수 있습니다.
- 성능 오버헤드: 모든 요청이 게이트웨이를 거쳐야 하므로 잘못 설계된 경우 병목이 될 수 있습니다.
- 복잡성 증가: 게이트웨이에 많은 기능이 추가될수록 관리 복잡성이 커질 수 있습니다.
3. Backend For Frontend Pattern (BFF)
BFF 패턴은 서로 다른 프론트엔드 애플리케이션(웹, 모바일 등)에 위한 맞춤형 백엔드를 제공하는 설계 방식입니다.
장점
- 프론트엔드와의 통신 최적화: 각 프론트엔드의 요구사항에 맞는 응답을 제공하여 사용자 경험을 개선할 수 있습니다.
- 프론트엔드 단순화: BFF가 데이터 집계, 변환 등을 처리하므로 프론트엔드 코드가 단순해집니다.
- 독립적 진화 가능: 각 프론트엔드와 BFF가 독립적으로 진화할 수 있어 개발 유연성이 높아집니다.
단점
- 유지보수 복잡성 증가: 프론트엔드별로 별도의 BFF를 관리해야 하므로 유지보수 비용이 증가할 수 있습니다.
- 코드 중복 가능성: 여러 BFF에 공통으로 필요한 기능이 중복될 수 있습니다.
- 일관성 관리 어려움: 여러 BFF가 동일한 기능을 제공할 때 일관성을 유지하는 것이 어려울 수 있습니다.
4. Command Query Responsibility Segregation (CQRS)
CQRS 패턴은 읽기 작업(쿼리)과 쓰기 작업(명령)의 책임을 분리하는 설계 방식입니다.
장점
- 성능 최적화: 읽기와 쓰기 작업을 독립적으로 최적화할 수 있습니다.
- 독립적 확장성: 읽기와 쓰기 모델을 개별적으로 확장할 수 있어 리소스 사용이 효율적입니다.
- 유지보수성 향상: 읽기와 쓰기 로직이 분리되어 코드의 가독성과 유지보수성이 향상됩니다.
단점
- 복잡성 증가: 두 개의 모델을 관리해야 하므로 설계와 구현이 복잡해집니다.
- 데이터 동기화 문제: 쓰기 모델과 읽기 모델 간의 데이터 동기화를 보장하기 어려울 수 있습니다.
- 추가 도구 필요: 메시지 큐 또는 이벤트 소싱과 같은 도구가 필요할 수 있습니다.
5. Event Sourcing Pattern
Event Sourcing Pattern은 시스템의 상태 변경을 이벤트로 기록하고, 이러한 이벤트의 흐름을 통해 현재 상태를 재구성하는 방식입니다. 상태를 직접 저장하는 대신 상태 변화를 나타내는 모든 이벤트를 이벤트 스토어에 저장합니다.
장점
- 완전한 감사 기록 제공: 모든 상태 변경이 이벤트로 기록되므로 시스템의 모든 변화를 추적할 수 있습니다.
- 확장성 향상: 쓰기 작업을 이벤트 형태로 기록하고, 여러 소비자에게 이를 전달할 수 있어 높은 쓰기 확장성을 제공합니다.
- 기능 추가 용이: 새로운 이벤트 유형을 도입함으로써 기존 데이터를 손상시키지 않고 기능을 확장할 수 있습니다.
단점
- 복잡성 증가: 이벤트 스트림을 관리하고 상태를 재구성하는 방식이 일반적인 데이터 저장 방식보다 복잡합니다.
- 높은 저장소 요구량: 모든 이벤트가 저장되기 때문에 많은 저장 공간이 필요할 수 있습니다.
- 복잡한 쿼리: 필요한 현재 상태를 얻기 위해 이벤트를 재생(replay)해야 하므로 쿼리 작성이 까다로울 수 있습니다.
6. Saga Pattern
Saga Pattern은 분산 시스템에서 여러 서비스에 걸친 트랜잭션을 관리하기 위한 설계 방식입니다. 이를 위해 트랜잭션을 로컬 트랜잭션의 집합으로 나누고, 각 단계가 완료되면 다음 단계로 넘어갑니다. 만약 어느 단계에서 실패하면 이전 단계에서 수행한 작업을 취소하는 보상 트랜잭션을 실행합니다.
장점
- 데이터 일관성 유지: 서비스 간의 데이터 일관성을 유지하면서도 분산 트랜잭션의 복잡성을 줄일 수 있습니다.
- 복원력 향상: 트랜잭션이 실패할 경우 보상 작업을 통해 시스템을 복구할 수 있어 신뢰성이 높아집니다.
- 확장성 향상: 로컬 트랜잭션으로 구성되어 있기 때문에 각 서비스가 독립적으로 확장 가능합니다.
단점
- 복잡성 증가: 각 단계에 대한 보상 트랜잭션을 설계하고 관리해야 하므로 구현이 복잡할 수 있습니다.
- 자동 롤백 부재: 전통적인 ACID 트랜잭션과 달리 자동 롤백이 없기 때문에 개발자가 명시적으로 보상 작업을 설계해야 합니다.
- 격리 수준 부족: 동시 실행되는 여러 Saga가 있을 경우 데이터 이상 현상이 발생할 수 있습니다.
7. Sidecar Pattern
Sidecar Pattern은 주요 서비스와 함께 배포되는 보조 서비스를 별도의 프로세스로 실행하여 로깅, 모니터링, 보안 등의 부가 기능을 제공하는 설계 방식입니다. 주로 컨테이너 환경에서 사용됩니다.
장점
- 모듈화 및 확장성: 사이드카를 통해 새로운 기능을 쉽게 추가하거나 제거할 수 있으며, 서비스의 주 기능을 변경하지 않고도 부가 기능을 확장할 수 있습니다.
- 책임 분리: 보조 기능을 독립적인 프로세스로 실행함으로써 주 서비스와의 책임을 명확히 분리할 수 있습니다.
- 독립적 확장 가능: 사이드카와 주 서비스는 각각 별도로 확장할 수 있습니다.
단점
- 관리 복잡성 증가: 다수의 사이드카 서비스를 관리해야 하므로 운영 복잡성이 증가할 수 있습니다.
- 단일 장애 지점: 사이드카가 장애를 겪을 경우 주 서비스에도 영향을 줄 수 있으므로 고가용성 설계가 필요합니다.
- 성능 저하 가능성: 사이드카와 주 서비스 간의 통신이 지연을 초래할 수 있어 성능에 영향을 줄 수 있습니다.
8. Circuit Breaker Pattern
Circuit Breaker Pattern은 서비스 장애가 발생했을 때 이를 감지하고, 일정 시간 동안 요청을 차단하여 연쇄적인 장애를 방지하는 방식입니다. 전기 회로 차단기와 유사하게 동작합니다.
장점
- 연쇄 장애 방지: 장애가 발생한 서비스를 호출하지 않도록 차단함으로써 전체 시스템의 안정성을 높일 수 있습니다.
- 시스템 복원력 향상: 장애가 발생했을 때 시스템이 과부하에 걸리지 않도록 하여 복원력을 높입니다.
- 신뢰성 향상: 오류가 발생할 때 빠르게 대체 동작을 수행할 수 있어 사용자 경험이 향상됩니다.
단점
- 설정 복잡성: 임계값과 복구 주기를 적절하게 설정하는 것이 어려울 수 있습니다.
- 대체 동작 설계 필요: 서비스 장애 시 대체 동작(fallback)을 제공하기 위해 별도의 설계가 필요합니다.
- 불필요한 차단 가능성: 임계값 설정이 적절하지 않으면 서비스가 정상임에도 불구하고 차단이 발생할 수 있습니다.
9. Anti-Corruption Layer Pattern
Anti-Corruption Layer Pattern은 외부 시스템과 통합할 때 내부 시스템의 설계와 데이터 모델을 보호하기 위한 패턴입니다. 내부 시스템과 외부 시스템 간의 데이터 모델 차이를 변환하는 중간 계층을 제공하여 내부 시스템이 외부 변화에 영향을 받지 않도록 합니다.
장점
- 내부 시스템 보호: 외부 시스템의 변화로부터 내부 시스템을 보호할 수 있습니다.
- 유연성 향상: 외부 시스템과의 통합을 쉽게 관리할 수 있으며, 외부 시스템 변경 시에도 내부 시스템의 수정이 최소화됩니다.
- 유지보수 용이성: 내부 시스템과 외부 시스템의 책임이 명확히 분리되어 유지보수가 용이해집니다.
단점
- 추가 복잡성: 변환 계층을 설계하고 관리해야 하므로 시스템 복잡성이 증가할 수 있습니다.
- 지연 시간 증가: 변환 작업으로 인해 응답 속도가 느려질 수 있습니다.
- 확장성 문제: 많은 수의 외부 시스템과 통합할 경우 ACL 계층을 확장하는 것이 어려울 수 있습니다.
10. Aggregator Pattern
Aggregator Pattern은 여러 마이크로서비스에서 데이터를 수집하고 이를 하나의 응답으로 병합하여 클라이언트에 제공하는 설계 방식입니다.
장점
- 클라이언트 상호작용 단순화: 클라이언트가 하나의 엔드포인트만 호출하면 되므로 인터페이스가 단순해집니다.
- 네트워크 호출 최소화: 클라이언트가 여러 서비스를 호출하지 않고도 필요한 데이터를 한 번에 받을 수 있습니다.
- 중앙 집중식 데이터 처리: 데이터 집계 및 변환을 중앙에서 처리하여 일관성을 유지할 수 있습니다.
단점
- 복잡성 증가: 여러 서비스로부터 데이터를 수집하고 병합하는 로직이 복잡할 수 있습니다.
- 단일 장애 지점: Aggregator가 장애를 겪으면 전체 응답이 실패할 수 있으므로 고가용성 설계가 필요합니다.
- 지연 시간 증가: 여러 서비스로부터 데이터를 수집하는 과정에서 지연이 발생할 수 있습니다.
- 확장성 문제: Aggregator가 처리해야 할 요청 수가 많아질 경우 확장하기 어려울 수 있습니다.
결론
이 패턴들은 마이크로서비스 아키텍처의 복잡성을 관리하고 시스템의 확장성과 신뢰성을 높이는 데 필수적인 역할을 합니다.