본문 바로가기

프로그래밍_도서/그림으로_공부하는_마이크로_서비스_구조

3. 마이크로서비스 아키텍처의 기본

3.4. 데이터베이스 접근

 

마이크로서비스 아키텍처에서는 각 서비스가 고유한 데이터베이스를 가지도록 구성하는 것이 원칙이다. 즉, 하나의 서비스는 하나의 데이터베이스와 일대일 관계를 맺으며, 자신이 소유하지 않은 데이터에 접근할 필요가 있을 경우에는 해당 데이터를 소유한 서비스를 통해 간접적으로 접근해야 한다. 이러한 설계 방식은 데이터베이스 변경에 유연하게 대응하기 위함이다. 데이터베이스 구조가 변경되면 이를 사용하는 프로그램도 함께 수정해야 할 가능성이 크다. 만약 여러 서비스나 프로그램이 하나의 데이터베이스를 공유하고 있다면, 단일한 변경이 여러 곳에 영향을 미쳐 전체 시스템의 안정성과 유지보수성을 저해할 수 있다. 따라서 서비스별로 독립적인 데이터베이스를 구성함으로써, 변경이 필요한 경우 해당 서비스만 수정하면 되므로 보다 신속하고 유연한 대응이 가능해진다.

 

3.5. 트랜젝션 처리

 

마이크로서비스 아키텍처에서는 트랜잭션 처리에 있어 로컬 트랜잭션을 권장한다. 로컬 트랜잭션이란 하나의 트랜잭션 컨텍스트(begin과 commit으로 정의된 범위) 내에서 단일 리소스—예를 들어 하나의 데이터베이스나 메시지 브로커—만을 대상으로 처리하는 방식이다. 예를 들어, 프로그램 코드에서 begin과 commit 사이에 하나의 데이터베이스만을 다룬다면, 이는 로컬 트랜잭션에 해당한다. 마이크로서비스에서는 각 서비스가 고유한 데이터베이스를 가지므로, 해당 서비스 내에서만 트랜잭션이 발생한다면 자연스럽게 로컬 트랜잭션 구조가 된다.

반면, 하나의 트랜잭션 컨텍스트 내에서 둘 이상의 리소스를 동시에 처리하는 경우를 글로벌 트랜잭션이라고 한다. 이는 분산 트랜잭션 구조를 기반으로 하며, 대표적인 구현 방식으로는 투페이즈 커밋(2PC, Two-Phase Commit) 프로토콜이 있다. 이러한 방식은 여러 시스템 간의 일관된 상태 유지를 가능하게 하지만, 마이크로서비스 환경에서는 권장되지 않는다.

그 이유는 글로벌 트랜잭션이 운영상 복잡성을 증가시키기 때문이다. 마이크로서비스는 단순하고 독립적으로 운영 가능한 구조를 지향하는데, 글로벌 트랜잭션은 서비스 간의 결합도를 높이고, 시스템의 유연성과 확장성을 저해할 수 있다. 또한, 글로벌 트랜잭션을 전제로 설계할 경우, 서비스 간의 암묵적인 제약이 생길 수 있다. 예를 들어, “데이터베이스 A와 데이터베이스 B는 반드시 하나의 트랜잭션 내에서 함께 변경되어야 한다”는 규칙은 서비스의 독립성과 자율성을 침해하며, 마이크로서비스의 핵심 가치인 느슨한 결합과 독립적 배포를 약화시킨다.

따라서 마이크로서비스에서는 가능한 한 로컬 트랜잭션을 활용하고, 서비스 간 데이터 정합성은 이벤트 기반의 비동기 메시징이나 사가 패턴(Saga Pattern)과 같은 대안적인 방식으로 해결하는 것이 바람직하다.

 

 

3.6. 데이터베이스 간 동기화

마이크로서비스 아키텍처에서는 각 서비스가 고유한 데이터베이스를 가지는 분산 구조를 채택하기 때문에, 전체 시스템은 자연스럽게 초분산 컴퓨팅 환경이 된다. 이처럼 분산된 데이터베이스 간의 동기화를 어떻게 유지할 것인가가 중요한 과제가 되는데, 이를 해결하기 위한 대표적인 설계 패턴이 바로 사가(Saga) 패턴이다.

사가 패턴은 여러 개의 로컬 트랜잭션을 이벤트를 통해 순차적으로 연결하여 전체적인 트랜잭션 흐름을 구성하는 방식이다. 마치 불을 끄기 위해 줄을 서서 양동이를 전달하는 릴레이처럼, 각 서비스는 자신의 데이터베이스에 로컬 트랜잭션을 수행하고, 그 결과를 이벤트로 다음 서비스에 전달한다. 이 과정을 통해 여러 데이터베이스 간의 상태를 점진적으로 동기화한다.

만약 이 릴레이 도중 어느 한 서비스에서 장애가 발생하여 트랜잭션이 실패하면, 이전 단계에서 성공한 트랜잭션들을 되돌리는 보상 트랜잭션이 실행된다. 보상 트랜잭션은 정상 처리의 반대 방향으로 작동하여, 시스템을 일관된 상태로 되돌리는 역할을 한다.

그러나 사가 패턴은 각 로컬 트랜잭션이 독립적으로 실행되기 때문에, 특정 시점에서는 데이터베이스 간의 상태가 일치하지 않을 수 있다. 즉,즉각적인 일관성(strong consistency)은 보장되지 않으며, 대신 시간이 지나면서 점진적으로 일관된 상태에 도달하는 결과 일관성(eventual consistency)을 제공한다. 이로 인해 수백 밀리초에서 수 초 단위의 지연이 발생할 수 있으며, 이러한 지연을 허용할 수 있는 비즈니스 시나리오에 적합하다.

결론적으로, 사가 패턴은 마이크로서비스 환경에서 데이터베이스 간 동기화를 유연하게 처리할 수 있는 강력한 도구지만, 항상 일관성이 필요한 경우에는 적합하지 않다. 대신, 결과 일관성을 수용할 수 있는 상황에서 효과적으로 활용할 수 있다.

 

 

3.7. 데이터 결합

마이크로서비스 아키텍처에서 분산된 데이터베이스 환경은 데이터 결합(data aggregation)이라는 새로운 과제를 동반한다. 여러 서비스가 각각의 데이터베이스를 소유하고 있기 때문에, 클라이언트가 하나의 통합된 뷰(view)를 요구할 경우, 이질적인 데이터 소스를 효과적으로 조합하는 방법이 필요하다.

이를 해결하기 위한 대표적인 접근 방식 중 하나가 바로 API 컴포지션(API Composition) 기법이다. 이 방식은 여러 마이크로서비스의 API를 호출하여 각각의 데이터를 수집하고, 이를 메모리 상에서 조합하여 클라이언트에 단일 응답으로 제공하는 패턴이다. 주로 BFF(Backend for Frontend)나 API Gateway 계층에서 구현되며, 프론트엔드 요구사항에 맞춰 데이터를 가공하고 통합하는 역할을 한다.

API 컴포지션의 가장 큰 장점은 설계와 구현이 비교적 단순하고 직관적이라는 점이다. 각 서비스의 API를 호출한 결과를 조합하는 방식이기 때문에 빠르게 개발할 수 있고, 변경에도 유연하게 대응할 수 있다. 그러나 이 방식은 모든 데이터를 애플리케이션 메모리 내에서 처리하기 때문에, 다음과 같은 단점도 존재한다:

  • 호출 대상 서비스가 많아질수록 네트워크 지연(latency)이 누적된다.
  • 데이터 양이 많거나 요청 빈도가 높을 경우, 메모리 사용량이 급증하고 시스템 리소스에 부담을 준다.
  • 병목 현상이 발생할 수 있으며, 전체 응답 시간이 가장 느린 서비스에 의해 결정된다.
  • 장애 허용성이 낮아, 일부 서비스가 실패하면 전체 응답이 실패할 수 있다.

따라서 API 컴포지션은 데이터 양이 적고, 실시간성이 요구되며, 결합 대상 서비스 수가 많지 않은 경우에 적합하다. 반면, 복잡한 데이터 결합이 필요하거나 성능과 확장성이 중요한 경우에는 CQRS(Command Query Responsibility Segregation)와 같은 다른 패턴을 함께 고려할 필요가 있다.

결론적으로, API 컴포지션은 마이크로서비스 환경에서 데이터 결합을 위한 유용한 기법이지만, 그 한계와 상황에 맞는 적용이 중요하다.

 

3.7.1. CQRS

CQRS(Command Query Responsibility Segregation)는 데이터 처리의 책임을 명확히 분리하여, 쓰기 작업(Command)과 읽기 작업(Query)을 각각 독립된 컴포넌트와 데이터 저장소로 분리해 구현하는 아키텍처 패턴이다.

전통적인 시스템에서는 읽기와 쓰기 작업을 동일한 데이터 모델과 저장소에서 처리하기 때문에, 서로 다른 성격의 요구사항을 동시에 만족시키기 어렵다. 예를 들어, 읽기 작업은 일반적으로 요청 빈도가 높고 빠른 응답 속도가 요구되며, 반면 쓰기 작업은 빈도는 낮지만 트랜잭션의 안정성과 데이터 정합성이 매우 중요하다. 이러한 상반된 요구를 하나의 시스템에서 처리하려다 보면, 성능과 확장성 측면에서 한계에 부딪히게 된다.

CQRS는 이러한 문제를 해결하기 위해 다음과 같은 구조를 취한다:

  • ✍️ 쓰기(Command) 경로: 트랜잭션을 보장하는 안정적인 저장소를 사용하여 데이터의 정확성과 무결성을 우선시한다. 도메인 로직이 집중되는 영역이며, 복잡한 비즈니스 규칙을 처리한다.
  • 🔍 읽기(Query) 경로: 읽기 전용 데이터 저장소를 별도로 구성하여, 빠른 조회와 고속 응답을 가능하게 한다. 이 저장소는 쓰기 저장소의 데이터를 비동기적으로 복제하거나 가공하여 구성된다.

이러한 구조를 통해 각 작업에 최적화된 설계를 적용할 수 있으며, 다음과 같은 이점을 얻을 수 있다:

  • ✅ 읽기와 쓰기 각각에 맞는 성능 최적화 가능
  • ✅ 서비스별로 독립적인 확장성 확보
  • ✅ 복잡한 도메인 로직과 단순한 조회 로직을 분리하여 유지보수성 향상

단, CQRS는 시스템 복잡도를 증가시키고, 데이터 동기화 지연으로 인해 일관성 문제가 발생할 수 있으므로, 반드시 필요한 경우에만 도입하는 것이 바람직하다. 특히 마이크로서비스 환경에서는 데이터 결합이나 고성능 조회가 필요한 상황에서 API 컴포지션의 한계를 보완하는 수단으로 활용될 수 있다.

 

 

3.7.2. 이벤트 소싱

 

이벤트 소싱(Event Sourcing)은 시스템의 상태를 직접 저장하는 대신, 상태 변화의 원인이 되는 도메인 이벤트를 시간 순서대로 기록하여 전체 상태를 추적하고 재구성하는 아키텍처 패턴이다. 전통적인 방식에서는 데이터베이스에 엔터티의 현재 상태만 저장하지만, 이벤트 소싱에서는 “무엇이 일어났는가”를 중심으로 데이터를 저장한다. 예를 들어, “주문 생성됨”, “결제 완료됨”과 같은 이벤트가 그대로 저장되며, 이 이벤트들의 집합이 시스템의 상태를 구성한다.

이벤트는 이벤트 저장소(Event Store)에 순차적으로 기록되며, 이 저장소는 쓰기 모델의 유일한 진실의 원천(source of truth)이 된다. 이벤트 저장소는 하나의 로컬 저장소이기 때문에 글로벌 트랜잭션이 필요 없고, 도메인 경계를 넘지 않으므로 마이크로서비스 환경에서의 독립성과 확장성에도 유리하다.

하지만 이벤트 자체는 빠른 검색이나 복잡한 조회에 적합하지 않기 때문에, 이벤트 소싱에서는 별도의 읽기 전용 저장소(Read Model)를 구성한다. 이때 메시지 지향 미들웨어(MOM: Message Oriented Middleware)를 활용하여, 이벤트 저장소에 기록된 이벤트를 비동기적으로 발행하고, 읽기 모델이 이를 구독하여 자체 데이터베이스를 갱신하는 구조를 사용한다. 이 방식은 CQRS와 결합된 대표적인 구현 형태로, 쓰기 모델은 이벤트 중심으로 상태를 보존하고, 읽기 모델은 최신 상태를 유지하면서 고속 조회가 가능하도록 설계된다.

이벤트 소싱은 시스템의 상태를 시간의 흐름에 따라 기록하고 재생할 수 있기 때문에, 감사 추적이나 상태 복원, 장애 복구 등의 측면에서도 유용하다. 다만, 이벤트 발행과 읽기 모델 갱신 사이에 지연이 발생할 수 있어 결과 일관성을 전제로 설계해야 하며, 이벤트 스키마 변경이나 순서 보장 등 운영 측면에서도 신중한 관리가 필요하다. 이벤트 소싱은 단순한 저장 방식이 아니라, 시스템의 동작을 기록하고 재현할 수 있는 이력 중심의 설계 철학이며, CQRS와 함께 사용할 때 그 효과가 극대화된다.

 

3.7.3. CQRS&이벤트 소싱의 장단점

  • 장점
    • 쿼리 구현의 용이성
      읽기 모델을 조회 목적에 맞게 최적화할 수 있어, 복잡한 쿼리도 단순하고 빠르게 구현 가능
    • 데이터 변경 이력 감시 가능
      이벤트 소싱을 통해 모든 상태 변경이 이벤트로 기록되므로, 변경 이력 추적 및 감사(Audit)가 용이
    • 접근 제어 구현의 유연성
      읽기/쓰기 모델이 분리되어 있어, 역할에 따라 접근 권한을 세분화하기 쉬움
    • 서비스 모델링과의 높은 친화성
      도메인 중심 설계(DDD)와 잘 어울리며, 복잡한 비즈니스 로직을 명확하게 분리하고 표현할 수 있음
  • 단점
    • 기존 설계 방식과의 괴리감
      전통적인 CRUD 기반 설계와는 개념적으로 다르기 때문에, 초기 학습 곡선이 존재하고 팀 내 이해를 맞추는 데 시간이 필요함
    • 구현 복잡성 증가
      이벤트 저장소, 메시지 브로커, 읽기 모델 동기화 등 추가 인프라와 설계 요소가 필요
    • 이벤트 스키마 관리의 어려움
      시간이 지남에 따라 이벤트 구조가 진화하면서, 과거 이벤트와의 호환성을 유지하는 것이 까다로움

 

3.8. 서비스 간 연계

마이크로서비스 아키텍처에서는 서비스 간 통신을 위한 표준 사양이 정해져 있지 않으며, 각 서비스의 목적과 특성에 따라 적절한 통신 프로토콜을 선택할 수 있다. 그중 가장 널리 사용되는 방식은 REST와 메시징(Messageing)이다.

REST는 클라이언트가 요청을 보내고, 서버로부터 응답을 받을 때까지 기다리는 구조의 동기형 통신 방식이다. 이 방식은 요청-응답 구조가 단순하고 이해하기 쉬우며, 처리 시간이 짧은 작업에 적합하다. 그러나 서비스 로직이 복잡하거나 처리 시간이 오래 걸리는 작업에서는 몇 가지 한계가 발생한다. 클라이언트는 응답을 받을 때까지 블로킹 상태가 되며, 서버는 해당 요청을 처리하는 동안 리소스를 계속 점유하게 된다. 이러한 상황이 반복되면 서버에 요청이 누적되고, 결국 성능 저하나 장애로 이어질 수 있다. 따라서 REST 기반의 동기형 통신은 단순하고 빠른 처리가 필요한 경우에는 적합하지만, 장시간 처리나 고부하 환경에서는 확장성과 안정성 측면에서 한계를 가진다.

이러한 한계를 극복하기 위해, 복잡하고 무거운 처리를 포함한 서비스 간 연계에는 메시징 기반 통신이 효과적이다. 메시징은 메시지 지향 미들웨어(MOM: Message Oriented Middleware)를 통해 게시자(Publisher)와 구독자(Subscriber)가 이벤트를 주고받는 방식으로, 서비스 간의 결합도를 낮추고 비동기 처리를 가능하게 한다.

메시징 기반 통신에는 다음과 같은 세 가지 주요 패턴이 존재한다:

  • 단방향 & 비동기형: 요청만 보내고 응답을 기다리지 않는 방식. 예를 들어 이벤트 발행이 이에 해당한다.
  • 요청/응답 & 동기형: REST와 유사하게 요청 후 응답을 기다리는 방식.
  • 요청/응답 & 비동기형: 요청과 응답이 분리되어 비동기로 처리되는 방식. 메시지 큐 기반의 RPC(Remote Procedure Call) 등이 이에 해당한다.

이 중에서도 비동기형 통신은 확장성과 유연성이 중요한 시스템에 특히 적합하다. 예를 들어, 분산 데이터베이스 환경에서의 데이터 동기화, Saga 패턴을 활용한 분산 트랜잭션 처리, CQRS 패턴에서의 결과적 일관성(Eventual Consistency) 구현 등 마이크로서비스 아키텍처에서는 비동기 메시징이 필수적인 경우가 많다.

결론적으로, 마이크로서비스 간 연계 방식은 서비스의 특성과 요구사항에 따라 선택되어야 하며, 단순한 요청-응답에는 REST가, 복잡한 처리나 확장성이 중요한 시나리오에는 메시징 기반의 비동기 통신이 더욱 적합하다.