최근 백엔드 개발자 채용 공고를 유심히 보셨다면, 'MSA'라는키워드를 심심치 않게 발견하셨을 겁니다. 대체 MSA란 무엇이고, 왜 이렇게 많은 기업들이 요구하는 걸까요?
이 질문에 답하기 위해, 오늘은 마이크로서비스가 어떤 기술적 배경에서 출발했는지, 그리고 기존 아키텍처의 어떤 한계를 극복하기 위해 등장했는지 그 발전 과정 전반을 가볍게 다루어보고자 합니다. 이 글을 통해 MSA의 개념을 명확히 이해하고, MSA 환경에서 발생하는 문제들은 무엇인지, 그리고 그 문제들을 해결하기 위한 기술적 진화의 흐름을 이해해 봅시다.
Monolithic Architecture
초기 서비스 개발은 대부분 모놀리식(Monolithic) 아키텍처에서 시작합니다. 이름 그대로, 모든 비즈니스 로직과 기능이 하나의 거대한 애플리케이션, 하나의 코드베이스 안에 구현된 구조를 말합니다. 이 구조에서는 개발 환경 설정이 단순하고 배포도 간단해 개발 속도가 매우 빠르다는 큰 장점이 있습니다. 대규모 트래픽을 처리해야 하는 프로덕트가 아니라면 대부분 아래와 같은 구조로 서비스를 구성하죠.

하지만 서비스가 성장하고 코드가 수백만 줄에 달하면서 위와 같은 구조는 점자 한계를 드러낼 수 밖에 없습니다. 그 중에서도 가장 큰 문제점은 확장성입니다. 가령, 단일 서버에 모든 서비스들이 포함되어 있다면, 쇼핑몰의 상품 조회 기능에만 트래픽이 몰려도 사용량이 적은 어드민 기능까지 포함하여 수평 확장해야 하는 등 비효율이 발생하죠.
또한, 코드 한 줄을 수정해도 전체 시스템을 다시 빌드하고 테스트해야 하므로 개발 속도는 점점 느려지고, 새로운 기술 스택을 도입하는 것은 사실상 불가능에 가까워집니다. 결국 이러한 한계점들 때문에 개발자들은 모놀리식 구조를 넘어 새로운 대안을 고민하게 되었습니다.
MicroService Architecture의 등장

이러한 모놀리식의 한계를 극복하기 위해 등장한 것이 바로 마이크로서비스 아키텍처(MSA)입니다. MSA의 핵심 아이디어는 거대한 서비스를 비즈니스 단위인 도메인(Domain) 단위로 잘게 나누는 것에서 시작합니다. 가령, 하나의 거대한 쇼핑몰 어플리케이션을 '주문 서비스', '결제 서비스' 등과 같이 비즈니스 단위로 분리해내는 것이죠.
더 나아가, 서비스별로 독립된 데이터베이스를 소유하도록 분리하여 물리적으로 서비스 간의 의존성을 완벽히 끊어내 각 서비스가 독립적으로 개발, 배포, 확장할 수 있는 단위가 되도록 하는 것이 마이크로서비스의 핵심입니다.
MSA 도입으로 인한 새로운 문제들의 시작
우리는 모놀리식 아키텍처의 한계를 극복하기 위해 MSA를 도입했지만, 각 서비스들을 전부 물리적으로 분산하는 과정에서 분산 시스템이라는 새로운 복잡성이 생겨났습니다. 서비스가 물리적으로 분리되며 기존에는 고민할 필요가 없었던 새로운 기술적인 과제들이 등장하는 것이죠. 새로운 기술적 과제들은 아래와 같습니다.
- 통신 복잡성: 서비스들간 소통은 어떻게 해야 할까?
- 데이터 일관성: 여러 서비스에 걸친 작업을 어떻게 하나의 트랜잭션처럼 처리할까?
- 장애 전파: 서비스 하나가 고장 나면 전체 시스템이 멈추는 건 아닐까?
- 복잡성: 이 많은 서비스들을 어떻게 관리하고 배포해야 할까?
이제부터는 구체적으로 각 과제들에 대해 살펴보고, 이러한 과제들을 해결하기 위해 등장한 주요 기술들과 아키텍처 패턴들을 하나씩 살펴보겠습니다.
서비스 사이의 통신 복잡성
모놀리식 내부의 단순한 함수 호출은 이제 다른 서버에 위치한 서비스를 호출해야 합니다. 이 과정에서 네트워크 지연, 요청 실패 및 재시도 처리, 서비스 간 API 버전 관리 등 고려해야 할 사항이 기하급수적으로 늘어납니다.
초기 MSA 환경에서는 주로 REST API가 통신 방식으로 사용되었지만, 수많은 서비스가 텍스트 기반의 JSON 형식으로 데이터를 주고받으면서 성능의 한계에 부딪혔습니다. 이를 해결하기 위해 등장한 것이 바로 gRPC와 같은 고성능 RPC(원격 프로시저 호출) 프레임워크입니다. gRPC는 HTTP/2를 기반으로, 데이터를 바이너리 포맷으로 직렬화하여 통신하므로 훨씬 가볍고 빠르다는 장점을 가집니다.
gRPC를 통해 통신 자체의 속도, 통신 과정에서의 latency는 어느 정도 개선했지만, 하나의 서비스가 다른 서비스의 응답을 기다려야 하는 동기적 방식의 근본적인 문제는 여전했습니다. 이러한 구조는 하나의 요청이 여러 서비스를 거치는 과정에서 대기 시간이 누적되는 연쇄 지연을 일으킬 수밖에 없어 조금 더 근본적인 극복이 필요했습니다.

이 문제를 해결하기 위해 이벤트 기반 아키텍처(Event-Driven Architecture, EDA)가 도입되었습니다. EDA 모델에서 서비스는 다른 서비스를 직접 호출하는 대신, '주문 완료'와 같이 특정 상태로의 변경시에 이벤트(Event)를 생성하여 메시지 브로커(e.g. Kafka, RabbitMQ)에 발행(Publish)하기만 합니다. 그러면 해당 이벤트를 다른 서비스들이 기호에 맞게 메시지를 구독(Subscribe)하여 필요한 작업을 비동기적으로 처리합니다. 이렇게 서비스 간의 결합도를 낮추고 네트워크 전체의 오버헤드를 줄일 수 있었습니다.
분산 환경에서의 데이터 일관성
MSA 환경의 두 번째 과제는 분산된 데이터베이스의 일관성을 유지하는 것입니다. 각 서비스가 독립된 DB를 가지므로, 더 이상 단일 데이터베이스가 보장하는 ACID 트랜잭션을 사용할 수 없기 때문입니다. 예를 들어, 사용자가 상품을 주문하는 상황에서 각 서비스가 아래와 같은 작업을 수행한다고 가정해 봅시다.
- 주문 서비스가 주문을 생성
- 결제 서비스가 금액을 처리
- 재고 서비스가 재고를 차감
만약 2번의 결제 단계에서 사용자가 잔액이 부족해 실패한다면 어떻게 될까요? 이미 생성된 주문 정보와 줄어든 재고는 원래대로 되돌려야 데이터 불일치가 발생하지 않습니다. 이처럼 여러 서비스에 걸친 분산 트랜잭션을 관리하기 위해 사가 패턴(Saga Pattern)이 등장합니다.

사가 패턴은 하나의 글로벌 트랜잭션을 각 서비스가 책임지는 로컬 트랜잭션의 연속으로 구성하는 것입니다. 그리고 각 로컬 트랜잭션마다, 실패 시 실행할 보상 트랜잭션을 미리 정의해 둡니다. 만약 특정 로컬 트랜잭션이 실패하면, 사가는 이전에 성공했던 모든 트랜잭션에 대한 보상 트랜잭션을 역순으로 실행합니다. (e.g. '결제 실패' 시 → '주문 취소' 로컬 트랜잭션 실행 )
기존에는 DBMS 수준에서 제공하던 원자성을 Application 수준에서 보장하는 것이죠. 이렇게 하면 일시적으로는 서비스가 데이터 불일치 상황에 놓일 수는 있지만 결국에는 모든 작업이 성공적으로 처리되거나 모두 취소되어 일관성을 달성할 수 있게 됩니다.
MSA 환경에서의 시스템 최적화
전통적인 데이터 관리 방식에서는 보통 하나의 데이터 모델이 생성(Create), 수정(Update), 삭제(Delete)와 같은 쓰기 작업과 데이터 읽기(Read) 작업을 모두 담당합니다. 하지만 이 두 작업은 본질적으로 요구사항이 매우 다릅니다.
- 쓰기 (명령, Command): 데이터의 일관성과 무결성을 지켜야 하므로 복잡한 유효성 검사와 비즈니스 로직을 포함합니다.
- 읽기 (조회, Query): 사용자에게 데이터를 빠르고 효율적으로 보여주는 데 초점을 맞추며, 종종 여러 테이블의 데이터를 조합(Join)한 형태를 필요로 합니다.
하나의 모델로 이 두 가지 상충하는 요구를 모두 만족시키려 하면, 모델이 지나치게 복잡해지거나 양쪽 모두의 성능에 제약을 주게 됩니다.
CQRS는 바로 이 문제에서 출발합니다. 이름 그대로 명령(Command)과 조회(Query)의 책임을 완전히 분리하여, 각 작업에 최적화된 별도의 모델과 경로, 심지어 다른 데이터 저장소까지 사용하자는 것이죠. 이를 통해 어플리케이션의 성격이나 트래픽에 따라 Read-Heavy일 경우에는 읽기 부분만 별도로 확장한다던지, 혹은 읽기용 DB에는 NoSQL, 쓰기용 DB에는 RDB를 사용하는 등 독립적인 확장이 가능해 집니다.
서비스 사이에서의 안정성 확보
MSA에서는 각 서비스들이 네트워크를 통해 서로 소통을 하게 되는데요, 네트워크의 예외 상황은 극복할 수 없는 문제이기 때문에 각 서비스들은 이를 전제로 아키텍처를 설계하게 됩니다. 일시적인 네트워크 장애가 있다면 일정 시간을 기다리고 통신을 재시도하는 패턴이 일반적이죠. 그런데 이런 재시도 패턴이 오히려 전반적인 서비스 장애의 트리거가 되기도 합니다. 다음과 같은 호출 상황을 가정해 봅시다.

서비스 A → B → C → D 각 서비스가 실패 시 5번씩 재시도한다고 가정했을 때, 서비스 D에 장애가 발생하면
- C는 D에게 5번의 요청을 전송
- C가 실패하자, B는 C에게 5번 재시도 (C는 D에게 5*5 = 25번 요청)
- B마저 실패하자, A는 B에게 5번 재시도 (C는 D에게 25*5 = 총 125번 요청)
문제점이 보이시나요? 이처럼 단순한 재시도는 특정 서비스의 장애를 시스템 전체로 증폭시키는 문제를 야기합니다. 이런 문제 상황을 해결하기 위해 서킷 브레이커 패턴을 사용합니다.
전기 회로의 차단기처럼, 특정 서비스에서 오류가 반복되면 일시적으로 해당 서비스로의 모든 요청을 차단하고 즉시 에러를 반환합니다. 이후 일정 시간이 지나면 시험적으로 요청을 보내 서비스의 복구 여부를 확인하고, 정상이면 다시 회로를 연결하여 요청을 정상화합니다. 이를 통해 장애가 다른 서비스로 전파되는 것을 막고 시스템 전체의 안정성을 확보하는 것이죠.
복잡한 서비스 생태계를 통합하여 관리하자
서비스가 발전함에 따라 생겨나는 수많은 마이크로서비스들은 어떻게 통합되고 관리해야 할까요? MSA는 개별 서비스의 개발만큼이나 전체 생태계를 효율적으로 운영하는 것이 중요합니다.

클라이언트가 수십, 수백 개의 서비스 주소를 모두 알고 직접 통신하는 것은 거의 불가능하며 매우 비효율적입니다. API 게이트웨이는 이 모든 서비스 앞단에서 모든 클라이언트의 요청을 받는 단일 진입점 역할을 수행합니다.
API 게이트웨이는 들어온 요청을 분석하여 가장 적절한 마이크로서비스로 전달하고, 여러 서비스에 걸쳐 필요한 공통 기능들을 게이트웨이 한곳에서 처리하여 개별 서비스의 부담을 덜어주며, 여러 마이크로서비스를 호출해야 하는 요청을 게이트웨이가 대신 처리하고, 그 결과를 조합하여 한 번에 클라이언트에게 반환하기도 합니다.
또한, 수많은 마이크로서비스를 각각 수동으로 배포하고, 트래픽에 따라 확장하며, 장애 발생 시 복구하는 것은 엄청난 운영 부담이 발생할 수밖에 없습니다. 컨테이너 오케스트레이션 도구(e.g. k8s)는 이러한 서비스 군단의 배포, 확장, 상태 관리를 자동화하고 총괄하는 역할을 수행합니다.
마무리
지금까지 우리는 MSA의 등장 배경과 MSA를 도입함에 따라 발생하는 문제들이 무엇인지를 가볍게 살펴보았습니다. MSA를 도입하여 개발자들은 비즈니스의 성장에 따라 더 이상 감당하기 어려워진 모놀리식의 '확장성'과 '민첩성' 문제를 해결할 수 있었죠. 그 과정에서 분산 시스템이라는 새로운 복잡성이 생겨났고, 오늘 소개한 gRPC, 이벤트 기반 아키텍처, 사가 패턴과 같은 수많은 기술들이 이 복잡성을 다루기 위해 함께 발전해왔습니다.
하지만 앞서 다룬 것처럼 MSA를 도입하는 순간 프로젝트의 복잡성도 크게 증가하는 단점이 있습니다. 우리는 현재 프로젝트가 MSA가 필요한 프로젝트인지 잘 검토하기 위한 시야를 키워내야 하고 이를 위해 MSA를 공부해야 한다고 생각합니다.
'CS' 카테고리의 다른 글
| TDD, 좋은 설계를 위한 내비게이션 (0) | 2025.10.13 |
|---|