1. MSA란
1-1 모놀리식 아키텍처
모놀리식 아키텍처(Monolithic Architecture)는 애플리케이션의 모든 기능을 하나의 코드베이스로 만들어 하나의 단위로 빌드하고 배포하는 구조입니다.
- 회원, 주문, 결제 기능이 한 프로젝트 안에 패키지로 나뉘어 있고, 하나의 실행 파일로 배포되며, 보통 하나의 데이터베이스를 함께 씁니다.
- 기능 간 호출은 같은 프로세스 안의 메서드 호출이고, 여러 기능에 걸친 작업도 하나의 DB 트랜잭션으로 처리합니다. 그래서 개발, 테스트, 배포가 단순합니다.
- 대신 서비스와 팀이 커지면 배포, 확장, 장애의 범위가 애플리케이션 전체에 묶인다는 한계가 드러납니다. MSA는 이 한계를 풀기 위한 구조입니다.
1-2 정의
MSA(Microservice Architecture)는 하나의 애플리케이션을 비즈니스 기능 단위의 작은 서비스 여러 개로 나누고, 각 서비스를 독립적으로 개발·배포·확장하는 구조입니다.
- 서비스마다 별도의 프로세스로 실행되고, 서로 HTTP API나 메시지로 통신합니다.
- 회원 서비스, 주문 서비스, 결제 서비스가 각자 코드베이스와 배포 파이프라인, 데이터베이스를 가집니다.
1-3 특징
- 비즈니스 기능 단위로 나눕니다. 컨트롤러, 서비스, 리포지토리 같은 기술 계층이 아니라 주문, 결제 같은 업무 단위로 나눕니다(2절).
- 독립적으로 배포합니다. 모놀리식은 결제 코드 한 줄을 고쳐도 전체를 다시 배포하지만, MSA는 결제 서비스만 배포합니다. 다른 서비스는 영향을 받지 않으므로 배포를 자주 해도 위험이 작습니다.
- 필요한 서비스만 확장합니다. 주문에만 트래픽이 몰리면 주문 서비스의 인스턴스만 늘립니다. 모놀리식은 애플리케이션 전체를 복제해야 합니다.
- 서비스마다 데이터베이스를 따로 가집니다(4-1).
- 장애가 서비스 안에 머물 수 있습니다. 프로세스가 분리되어 있으므로 한 서비스가 메모리 부족으로 죽어도 다른 서비스의 프로세스는 계속 동작합니다. 다만 동기 호출로 이어져 있으면 장애가 호출을 따라 번지므로, 이를 막는 장치가 필요합니다(3-1, 5-3).
- 서비스마다 기술을 고를 수 있습니다. 검색 서비스는 Elasticsearch, 나머지는 MySQL처럼 서비스 성격에 맞는 언어와 DB를 씁니다.
- 팀이 서비스를 맡습니다. 한 팀이 한 서비스의 개발부터 배포, 운영까지 책임지므로, 여러 팀이 하나의 코드베이스와 배포 일정을 맞출 일이 줄어듭니다.
1-4 단점
- 네트워크 호출이 늘어납니다. 메서드 호출이던 기능 간 호출이 네트워크 호출이 되어 느려지고, 상대 서비스가 응답하지 않는 실패가 생깁니다(3절).
- 데이터 일관성을 지키기 어렵습니다. 여러 서비스의 DB에 걸친 작업을 하나의 트랜잭션으로 묶을 수 없습니다(4절).
- 운영할 대상이 많아집니다. 서비스마다 배포, 모니터링, 로그 수집이 필요하고, 서비스를 찾아 연결하는 인프라가 추가로 필요합니다(5절).
- 테스트와 디버깅이 어렵습니다. 하나의 요청이 여러 서비스를 거치므로 전체 흐름을 테스트하려면 여러 서비스를 함께 띄워야 하고, 오류가 난 서비스를 따로 찾아야 합니다(5-4).
2. 서비스를 나누는 기준
2-1 나누는 기준
- 비즈니스 기능: 회원, 상품, 주문, 결제, 배송처럼 업무가 구분되는 단위로 나눕니다.
- 바운디드 컨텍스트(Bounded Context): 도메인 주도 설계(DDD)에서 하나의 모델과 용어가 같은 의미로 통하는 경계입니다. 같은 "상품"이라도 상품 관리에서는 이름·설명·이미지를, 주문에서는 주문 시점의 가격과 수량을, 배송에서는 무게와 크기를 뜻합니다. 이렇게 의미가 달라지는 경계가 서비스를 나누는 후보가 됩니다.
- 함께 바뀌는 기능은 같은 서비스에 둡니다. 서비스 안의 기능은 서로 밀접하게 관련되고(높은 응집도), 서비스끼리는 최소한의 API로만 연결되어야 합니다(낮은 결합도). 기능 하나를 바꿀 때 여러 서비스를 함께 고쳐야 한다면 경계가 잘못된 것입니다.
- 데이터마다 주인 서비스를 정합니다. 어떤 데이터를 만들고 바꾸는 책임은 한 서비스에만 둡니다. 같은 테이블을 여러 서비스가 함께 수정해야 한다면 경계를 다시 봅니다.
- 팀이 맡을 수 있는 단위와 맞춥니다. 시스템 구조는 그것을 만드는 조직의 의사소통 구조를 따라간다는 콘웨이의 법칙(Conway's Law)이 있습니다. 서비스 경계와 팀 경계가 어긋나면 서비스 하나를 바꿀 때마다 여러 팀이 일정을 맞춰야 합니다.
2-2 너무 잘게 나누면: 분산 모놀리스
- 서비스를 지나치게 잘게 나누면 기능 하나를 처리하는 데 여러 서비스를 연달아 호출해야 하고, 서비스끼리 서로의 내부 구조에 의존하게 됩니다.
- 그 결과 한 서비스를 바꿀 때 여러 서비스를 함께 고쳐 동시에 배포해야 하는 분산 모놀리스(Distributed Monolith) 가 됩니다. 모놀리식의 결합은 그대로 남고, 네트워크 호출과 운영 비용만 늘어난 상태입니다.
- 늘 함께 배포해야 하는 서비스나 같은 DB 테이블을 공유하는 서비스가 있다면, 경계를 다시 나누거나 하나로 합칩니다.
3. 서비스 간 통신
3-1 동기 통신
- 방식: 호출하는 서비스가 요청을 보내고 응답이 올 때까지 기다립니다. REST API(HTTP)나 gRPC(HTTP/2 위에서 데이터를 바이너리 형식으로 주고받는 방식)를 씁니다.
- 장점: 구현이 단순하고, 결과를 바로 받아 다음 처리에 쓸 수 있습니다.
- 단점: 호출받는 서비스가 느리거나 죽으면 호출한 서비스도 기다리다 실패합니다. 주문 → 결제 → 재고처럼 호출이 이어지면 맨 끝 서비스 하나의 장애가 앞의 서비스까지 멈추게 하는데, 이를 장애 전파(Cascading Failure) 라고 합니다.
- 쓰는 곳: 상품 조회처럼 결과가 바로 필요한 요청에 씁니다.
3-2 비동기 통신
- 방식: 서비스가 메시지 브로커(Kafka, RabbitMQ 등)에 이벤트를 발행하고, 그 이벤트가 필요한 서비스가 구독해 처리합니다. 발행한 서비스는 처리 결과를 기다리지 않습니다.
- 예: 주문 서비스가 "주문 생성됨" 이벤트를 발행하면 알림 서비스와 포인트 서비스가 각자 받아 처리합니다. 주문 서비스는 누가 이 이벤트를 받는지 알 필요가 없습니다.
- 장점
- 서비스끼리 직접 호출하지 않으므로 결합이 약합니다. 이벤트를 받는 서비스를 추가해도 발행하는 서비스는 바뀌지 않습니다.
- 받는 서비스가 잠시 죽어 있어도 메시지는 브로커에 남아 있다가 서비스가 살아나면 처리됩니다.
- 요청이 몰려도 브로커에 쌓아 두고, 처리할 수 있는 속도로 소비합니다.
- 단점
- 결과가 바로 반영되지 않고, 시간이 지나 일관된 상태가 됩니다(결과적 일관성, 4-3).
- 같은 메시지를 두 번 받을 수 있으므로, 받는 쪽은 같은 이벤트를 다시 처리해도 결과가 같도록 멱등하게 만듭니다. 처리한 이벤트 ID를 저장해 두고, 이미 처리한 ID면 건너뛰는 방식이 대표적입니다.
- 처리 흐름이 여러 서비스에 흩어져 추적하기 어렵습니다(5-4).
- 쓰는 곳: 주문 후 알림 발송, 포인트 적립처럼 결과를 즉시 돌려줄 필요가 없는 후속 작업에 씁니다.
4. 데이터 관리
4-1 서비스별 데이터베이스
- 원칙: 각 서비스는 자신의 데이터베이스를 가지고, 다른 서비스의 DB에 직접 쿼리하지 않습니다. 다른 서비스의 데이터가 필요하면 그 서비스의 API를 호출하거나 이벤트로 받습니다.
- 이유: 여러 서비스가 DB를 함께 쓰면, 한 서비스가 테이블 구조를 바꿀 때 그 테이블을 쓰는 서비스를 모두 함께 고쳐 배포해야 합니다. 서비스가 DB를 통해 다시 묶여 독립 배포가 깨집니다.
- 결과: 여러 서비스의 데이터를 SQL 조인으로 한 번에 조회할 수 없고, 여러 서비스에 걸친 변경을 하나의 DB 트랜잭션으로 묶을 수 없습니다(4-2).
- 여러 서비스의 데이터를 함께 조회하는 방법
- API 조합(API Composition): 조회를 맡은 쪽이 여러 서비스를 호출해 결과를 합칩니다. 단순하지만 호출이 많아지고, 하나라도 실패하면 결과가 불완전해집니다.
- 조회용 데이터 복제: 다른 서비스의 이벤트를 받아 조회에 필요한 데이터를 자기 DB에 복사해 둡니다. 쓰기 모델과 조회 모델을 나누는 CQRS(Command Query Responsibility Segregation)를 MSA에 적용하는 대표적인 방식입니다. 조회는 빠르지만, 복사본은 원본보다 늦게 반영됩니다.
4-2 분산 트랜잭션과 2PC
- 분산 트랜잭션: 여러 DB에 걸친 작업을 모두 성공하거나 모두 실패하게 만드는 트랜잭션입니다.
- 2PC(Two-Phase Commit): 코디네이터가 두 단계로 커밋합니다.
- 준비 단계: 모든 참여 DB에 커밋할 수 있는지 묻습니다. 각 DB는 변경을 준비하고 락을 잡은 채 응답합니다.
- 커밋 단계: 모두 가능하다고 답하면 커밋을, 하나라도 실패하면 롤백을 지시합니다.
- MSA에서 잘 쓰지 않는 이유
- 모든 참여자가 응답할 때까지 락을 쥐고 기다리므로 느리고, 한 서비스가 느리면 전체가 막힙니다.
- 참여 DB들이 준비를 마친 뒤 코디네이터가 죽으면, 참여 DB들은 커밋할지 롤백할지 모른 채 락을 쥐고 기다립니다.
- 메시지 브로커나 많은 NoSQL DB가 2PC를 지원하지 않습니다.
- 그래서 MSA에서는 하나의 큰 트랜잭션 대신 사가 패턴으로 일관성을 맞춥니다.
4-3 사가 패턴
사가 패턴(Saga Pattern)은 여러 서비스에 걸친 작업을 서비스별 로컬 트랜잭션의 연속으로 나누고, 중간에 실패하면 이미 완료한 단계를 되돌리는 보상 트랜잭션을 역순으로 실행하는 방식입니다.
정상 흐름 주문 생성 → 결제 승인 → 재고 차감
실패 시 재고 차감 실패 → 결제 취소 → 주문 취소 (보상 트랜잭션을 역순으로 실행)
- 각 단계는 자기 서비스의 DB에서 바로 커밋되는 로컬 트랜잭션입니다.
- 이미 커밋된 결제는 롤백할 수 없으므로, "결제 취소"라는 새로운 트랜잭션(보상 트랜잭션)을 실행해 결과를 되돌립니다.
- 보상 트랜잭션은 반드시 성공해야 하므로, 실패하면 재시도하고 재시도해도 안전하도록 멱등하게 만듭니다.
- 결과적 일관성(Eventual Consistency): 사가가 진행되는 동안에는 "주문은 생성됐지만 결제는 아직" 같은 중간 상태가 다른 요청에 보입니다. 모든 단계가 끝나야 일관된 상태가 됩니다. 그래서 주문에 "결제 대기" 같은 상태 값을 두어 중간 상태를 드러냅니다.
구현 방식
- 코레오그래피(Choreography): 흐름을 관리하는 중앙 구성 요소 없이, 각 서비스가 이벤트를 발행하고 다음 서비스가 그 이벤트를 구독해 자기 단계를 실행합니다. 실패하면 실패 이벤트를 발행하고, 앞 단계의 서비스들이 이를 받아 보상합니다.
- 장점: 구조가 단순하고 서비스 간 결합이 약합니다.
- 단점: 단계가 많아지면 전체 흐름이 한곳에 드러나지 않아 파악하고 디버깅하기 어렵습니다.
- 오케스트레이션(Orchestration): 오케스트레이터가 각 서비스에 단계 실행을 명령하고, 결과를 받아 다음 단계나 보상을 결정합니다.
- 장점: 전체 흐름이 오케스트레이터 한곳에 모여 있어 파악하고 바꾸기 쉽습니다.
- 단점: 오케스트레이터에 로직이 몰리고, 오케스트레이터라는 구성 요소를 따로 운영해야 합니다.
4-4 트랜잭셔널 아웃박스 패턴
- 문제: 주문을 DB에 저장하는 것과 "주문 생성됨" 이벤트를 브로커에 발행하는 것은 하나의 트랜잭션으로 묶이지 않습니다. DB 커밋 후 발행 전에 서버가 죽으면 이벤트가 사라지고, 발행한 뒤 DB 커밋이 실패하면 없는 주문의 이벤트가 나갑니다.
- 해결: 이벤트를 바로 발행하지 않고, 주문을 저장하는 같은 로컬 트랜잭션 안에서 아웃박스(outbox) 테이블에 이벤트를 함께 저장합니다. 별도의 발행기가 아웃박스 테이블을 읽어 브로커로 발행하고, 발행한 이벤트를 완료로 표시합니다.
- 주문과 이벤트가 함께 커밋되거나 함께 롤백되므로, 저장된 주문의 이벤트는 반드시 발행됩니다.
- 발행기가 발행 후 완료 표시 전에 죽으면 같은 이벤트를 다시 보내므로, 받는 쪽은 멱등하게 처리합니다(3-2).
5. MSA를 받치는 구성 요소
5-1 API 게이트웨이
API 게이트웨이(API Gateway)는 클라이언트의 모든 요청을 받는 단일 진입점입니다. 클라이언트는 각 서비스의 주소를 알 필요 없이 게이트웨이 하나만 호출합니다.
- 라우팅: 요청 경로를 보고 알맞은 서비스로 보냅니다.
/api/orders/**는 주문 서비스로,/api/payments/**는 결제 서비스로 보냅니다. - 공통 기능: 인증 토큰 검증, 요청 수 제한, 로깅처럼 모든 서비스에 필요한 일을 한곳에서 처리합니다. 각 서비스는 비즈니스 로직에 집중합니다.
- 주의: 모든 요청이 지나가므로 게이트웨이가 멈추면 전체 서비스가 멈춥니다. 여러 대로 띄우고, 비즈니스 로직은 넣지 않습니다.
5-2 서비스 디스커버리
- 필요한 이유: 서비스 인스턴스는 트래픽에 따라 늘고 줄며, 다시 뜰 때마다 IP가 바뀝니다. 호출할 서비스의 주소를 설정 파일에 고정해 둘 수 없습니다.
- 동작
- 서비스 인스턴스가 뜨면 서비스 레지스트리에 자신의 이름과 주소를 등록하고, 주기적으로 살아 있음을 알립니다.
- 호출하는 쪽은 서비스 이름으로 레지스트리에 물어 현재 살아 있는 인스턴스 목록을 받고, 그중 하나로 요청을 나눠 보냅니다(로드 밸런싱).
- 살아 있다는 신호가 끊긴 인스턴스는 레지스트리에서 빠집니다.
- 쿠버네티스 같은 컨테이너 플랫폼은 서비스 이름으로 접근하는 디스커버리와 로드 밸런싱을 기본으로 제공합니다.
5-3 서킷 브레이커
서킷 브레이커(Circuit Breaker)는 호출 실패가 기준 이상 쌓이면 일정 시간 동안 그 서비스로 가는 호출을 막고 바로 실패를 돌려주는 장치입니다. 3-1의 장애 전파를 막습니다.
- 닫힘(Closed): 평소 상태입니다. 호출을 그대로 보내고 실패율을 셉니다.
- 열림(Open): 실패율이 기준을 넘으면 열립니다. 호출을 보내지 않고 즉시 실패시키거나 대체 응답(fallback)을 돌려줍니다. 예를 들어 추천 서비스가 응답하지 않으면 인기 상품 목록을 대신 보여 줍니다.
- 호출한 쪽은 응답 없는 서비스를 기다리느라 스레드를 붙잡히지 않고, 장애가 난 서비스는 요청이 끊겨 회복할 시간을 얻습니다.
- 반열림(Half-Open): 일정 시간이 지나면 일부 호출만 시험 삼아 보냅니다. 성공하면 닫힘으로, 실패하면 다시 열림으로 돌아갑니다.
- 함께 쓰는 장치
- 타임아웃: 응답을 무한정 기다리지 않도록 호출마다 최대 대기 시간을 둡니다.
- 재시도: 일시적인 실패는 간격을 두고 몇 번 다시 시도합니다. 재시도해도 결과가 같은 멱등한 요청만 재시도합니다.
5-4 분산 추적과 중앙 로깅
- 문제: 하나의 요청이 여러 서비스를 거치고, 로그는 서비스마다 다른 서버에 남습니다. 오류가 어느 서비스의 어느 단계에서 났는지 찾기 어렵습니다.
- 분산 추적(Distributed Tracing): 요청이 처음 들어올 때 고유한 추적 ID(Trace ID)를 붙이고, 다른 서비스를 호출할 때마다 헤더로 이 ID를 넘깁니다. 같은 Trace ID로 기록된 구간을 모으면 요청 하나가 어떤 서비스를 어떤 순서로 거쳤고 구간마다 얼마나 걸렸는지 볼 수 있습니다.
- 중앙 로깅: 모든 서비스의 로그를 한곳에 모으고, 로그마다 Trace ID를 함께 남겨 요청 하나와 관련된 로그를 한 번에 검색합니다.
- 메트릭: 서비스별 응답 시간, 에러율, 처리량을 모아 대시보드로 보고, 기준을 넘으면 알림을 받습니다.
6. 언제 MSA를 선택하나
- MSA가 맞는 경우
- 여러 팀이 동시에 개발하고, 팀마다 배포 일정을 따로 가져가야 할 때
- 기능마다 트래픽 차이가 커서 일부만 따로 확장해야 할 때
- 도메인 경계가 분명해서, 나눠도 서비스끼리 주고받을 일이 적을 때
- MSA가 맞지 않는 경우
- 팀이 작고 서비스 초기라 도메인 경계가 자주 바뀔 때. 경계가 바뀔 때마다 서비스 사이의 API와 데이터를 옮겨야 합니다.
- 배포, 모니터링, 장애 대응을 자동화할 운영 역량이 없을 때
- 모놀리식으로 시작해 필요할 때 나눕니다.
- 처음에는 하나로 배포하되, 모듈 사이의 경계를 분명히 지키는 모듈러 모놀리스(Modular Monolith) 로 만듭니다. 모듈끼리는 정해 둔 인터페이스로만 호출하고, 다른 모듈의 테이블에 직접 접근하지 않습니다.
- 특정 모듈에 독립 배포나 확장이 필요해지면 그 모듈부터 서비스로 떼어 냅니다. 게이트웨이 뒤에서 기능을 하나씩 새 서비스로 옮기고, 옮긴 기능의 요청만 새 서비스로 보내며 점진적으로 전환합니다.
- MSA는 구조의 우열이 아니라 독립 배포와 확장을 얻는 대신 1-4의 비용을 치르는 선택입니다. 지금 겪는 문제가 그 비용보다 큰지로 판단합니다.
'Dev Log > Java & Spring' 카테고리의 다른 글
| [네트워크] - REST API 설계 (0) | 2026.10.21 |
|---|---|
| [ORM/JPA] - N+1 문제 (0) | 2026.10.05 |
| [운영체제] CPU 스케줄링 (0) | 2026.10.05 |
| [Spring 프레임워크] - IoC/DI, Bean 생명주기 (0) | 2026.09.23 |