3. 프로세스 간 통신
동기 RPI 패턴: REST
RPI(Remote Procedure Invocation)는 클라이언트가 서비스에 요청을 보내면 서비스가 처리 후 응답을 회신하는 IPC다.
요청 한 번으로 많은 리소스를 가져오기 어렵다.
- REST 리소스는 객체 중심이다. REST API 설계 시 클라이언트 요청 한번으로 연관된 객체를 모두 가져올 수 있는지 고민해야 한다.
- 순수 REST API 라면, 클라이언트는 적어도 2회요청을 해야한다. 시나리오가 더 복잡해지면 더 왕복 횟수가 증가하고 지연 시간도 증가한다.
- 해결법은 클라이언트가 리소스를 요청할때, 연관 리소스도 함께 조회하도록 API가 허락하는것이다. 그러나 시나리오가 복잡해질수록 API 구현시간이 많이 소요되며, 효율도 떨어지게 된다.
- 때문에 데이터를 효율적으로 조회할 수 있게 GraphQL 이나 넷플릭스 팔코 등 대체 API 기술이 각광바딕 시작했다.
작업을 HTTP 동사에 매핑하기 어렵다.
- 데이터를 수정할 때 대게 PUT 동사를 사용하지만, 주문 데이터만 하더라도 주문 취소/변경 등 다양하다.
- 또한 PUT 사용 시 필수 요건인 멱등성이 보장되지 않는 업데이트도 있다.
- 한 가지 해결법은 특정 부위를 업데이트 하는 하위 리소스를 정의하는 것이다.
- 가령 주문 취소는 POST /orders/{orderId}/cancel , 주문 변경은 POST /orders/{orderId}/revise 를 두는 것이다.
- 동사는 URL 쿼리 매개변수로 지정하는 방벙도 있지만 REST 답지 않아서 gRPC 같은 REST 대체 기술이 각광 받는 추세이다.
REST 의 장단점
✅ 장점
- 단순하고 익숙하다.
- post man 같은 브라우저 플러그인이나 curl 등의 도구를 사용하여 간편하게 테스트가 가능하다.
- 요청/응답 스타일의 통신을 직접 지원한다.
- HTTP 는 방화벽에 친화적이다.
- 중간 브로커가 필요없으므로 시스템 아키텍처가 단순해진다.
✅ 단점
- 요청/응답 스타일의 통신만 지원한다.
- 가용성이 떨어진다. 중간에서 메시지를 버퍼링하는 매개자 없이 클라이언트/서버 가 직접 통신하기 때문에 교환이 일어나는 동안 양쪽 다 실행중이어야 한다.
- 요청 한 번으로 여러 리소스를 가져오기 어렵다.
- 다중 업데이트 작업을 HTTP 동사에 매핑하기 어려울 때가 많다.
동기 RPI 패턴: gRPC
✅ 장점
- 다양한 업데이트 작업이 포함된 API를 설계하기 쉽다.
- 큰 메시지를 교환할 때 콤팩틀하고 효율적인 IPC다.(프로토콜 버퍼, 이진포맷)
- 양방향 스트리밍 덕분에 RPI, 메시징 두 가지 통신 방식 모두 가능하다.
- 다양한 언어로 작성된 클라이언트/서버 간 연동이 가능하다.
✅ 단점
- JS 클라이언트가 하는 일이 REST/JSON 기반 API보다 더 많다.(이진포맷)
- 구형 방화벽은 HTTP/2를 지원하지 않는다.
gRPC는 REST를 대체할 유력한 방안이지만, REST 처럼 동기 통신하는 매커니즘이라서 부분 실패 문제는 풀어야 할 숙제이다.
부분 실패 처리: 회로 차단기 패턴(circuit breaker)
분산 시스템은 서비스가 다른 서비스를 동기 호출할 때마다 부분 실패할 가능성이 항상 존재한다.
응답이 늦거나 서비스가 다운되면, 클라이언트는 응답 대기 도중 블로킹 되기 때문에 서비스 실패는 클라이언트로 전이되어 전체 시스템의 중단을 초래할 위험이 있다.(클라이언트가 무한 블로킹 하면 스레드 같은 주요 리소스가 고갈된다.)
견고한 RPI 프록시 설계
서비스가 다른 서비스를 동기 호출할 때 자기 스스로를 방어하는 방법
- 네트워크 타임아웃
- 미처리 요청 개수 제한: 클라이언트가 특정 서비스에 요청 가능한 미처리 요청의 최대 개수 설정, 최대 개수 이상시 즉시 실패처리
- 회로 차단기 패턴: 성공/실패 요청 개수를 지켜보다가 에러율이 주어진 임계치를 초과하면 그 이후 시도는 바로 실패처리
JVM 진영에서는 히스트릭스, 닷넷 진영에서는 폴리라는 라이브러리가 유명하다.
불능 서비스 복구
히스트릭스 같은 라이브러리는 솔루션에 불과하다.
알기 쉽게 서비스가 클라이언트에 에러를 반환하는 것이 좋을 수도 있다.
서비스 디스커버리
REST API가 있는 서비스를 호출하는 코드는 서비스 인스턴스의 네트워크 위치(IP 주소 및 포트)를 알고있어야 한다.
MSA의 경우 네트워크 위치가 동적으로 배정되며, 서비스 인스턴스의 자동 확장, 업그레이드 등 계속 달라지므로 클라이언트 코드는 서비스 디스커버리를 사용해야 한다.
애플리케이션 수준의 서비스 디스커버리 패턴 적용
애플리케이션 클라이언트/서비스가 서비스 레지스트리와 직접 통신하는 방법
ex) Spring Cloud(Netflix Eureka, Ribbon)
✅ 장점
- 다양한 플랫폼에 서비스가 배포된 경우에도 처리가 가능
✅ 단점
- Spring Cloud 개발자를 제외한 다른 자바 프레임워크를 사용한다면 다른 방안을 찾아야 한다.
플랫폼에 내장된 서비스 디스커버리 패턴 적용
Docker 같은 배포 플랫폼을 이용해서 요청을 라우팅할 수 있다.
장점
- 플랫폼에서 기본 제공된 서비스 디스커버리를 사용하면 알아서 해주기 때문에 편리하다.
단점
- 쿠버네티스에 기반한 서비스 디스커버리 같은 것은 오직 쿠버네티스로 배포한 서비스에만 적용된다.
비동기 메시징 패턴 응용 통신
메시지 브로커
브로커리스 메시징

브로커리스 아케턱처의 서비스는 메시지를 서로 직접 교환한다.(ZeroMQ)
✅ 장점
- 네트워크 트래픽이 가볍고 지연 시간이 짧다.
- 브로커가 없기 때문에 SPOF(Single Point Of Failure)이 없다.
- 브로커가 없기 때문에 운영 복잡도가 낮다.
✅ 단점
- 서로의 위치를 알아야 하기 때문에 서비스 디스커버리 매커니즘 중 하나를 사용해야한다.
- 메시지 교환 시 송수신자가 모두 실행중이어야 한다.
- 전달 보장 같은 메커니즘 구현이 어렵다.
이런 한계로 인하여 대부분 메시지 브로커 기반의 아키텍처를 사용한다.
브로커 기반 메시징
메시지 브로커는 종류가 다양하기 때문에 다음 항목을 잘 검토해야 한다.
- 프로그래밍 언어 지원 여부
- 메시징 표준 지원 여부: AMQP나 STOMP등 표준 프로토콜을 지원하는가?
- 메시지 순서
- 전달 보장
- 영속화: 브로커가 고장나도 문제가 없도록 메시지를 디스크에 저장하는가?
- 내구성: 컨슈머가 메시지 브로커에 다시 접속할 경우, 접속이 중단된 시간에 전달된 메시지를 받을 수 있는가?
- 확장성
- 지연 시간
- 경쟁사 컨슈머: 경쟁사의 컨슈머를 지원하는가?
✅ 장점
- 느슨한 결합: 클라이언트는 단순히 브로커에 메시지를 던지면 되므로 서비스 인스턴스 위치를 알려주는 디스커버리 메커니즘이 필요하지 않다.
- 메시지 버퍼링
- 유연한 통신
- 명시적 IPC
✅ 단점
- 성능 병목 가능성: 브로커가 성능 병목점이 될 수 있지만, 확장으로 커버할 수 있다.
- 단일 장애점 가능성: 요즘 브로커 대부분은 고가용성이 보장되도록 설계되었기 때문에 이 또한 커버된다.
- 운영 복잡도 증가
중복 메시지 처리
메시지 브로커는 보통 적어도 한 번 이상 메시지를 전달하겠다고 약속한다.
클라이언트에서 메시지를 소비하고 ACK 하기 전에 클라이언트가 다운되었다. 클라이언트를 재기동하면 메시지 브로커는 ACK 안 된 메시지를 다시 보낼것이다.
이럴때 중복 메시지가 발생한다.
중복 메시지를 처리하는 2가지 방법
- 멱등한 메시지 핸들러 작성
- 메시지를 추적하고 중복을 솎아낸다.
멱등한 메시지 핸들러 작성
메시지 처리 로직이 멱등하면 중복 메시지는 전혀 해롭지 않다. 그러나 아닐 경우에는 중복 메시지를 솎아내는 핸들러가 필요하다.
메시지 추적과 중복 메시지 솎아내기
주문 메시지가 여러분 중복해서 올 경우 주문 로직이 여러번 실행되면 문제가 심각해진다.
반드시 메시지 핸들러가 중복 메시지를 걸러 내서 멱등하게 동작하도록 만들어야 한다.
컨슈머가 메시지 ID를 이용하여 메시지 처리 여부를 추적하면서 중복 메시지를 솎아 내면 간단히 해결된다.

DB에 컨슈머가 소비한 메시지 ID를 저장한다.
트랜잭셔녈 메시징
DB 테이블을 메시지 큐로 활용
DB테이블을 임시 메시지 큐로 사용하는 트랜잭셔널 아웃박스 패턴이 가장 알기 쉬운 방법이다.

주문 서비스라고 한다면, 주문 정보를 주문 테이블에 저장함과 동시에 주문 이벤트는 OUTBOX 테이블에 저장한다.
메시지 릴레이는 OUTBOX 테이블을 읽어서 주문 이벤트를 메시지 브로커에 발행한다.
트랜잭셔널 아웃박스 패턴은 MSA에서 데이터 일관성과 메시지 전송의 원자성을 보장하기 위해 사용하는 패턴이다.
특히 이벤트 발행(Event Publishing)이나 비동기 통신(Kafka, RabbitMQ 등)을 사용할 때, DB 상태와 메시지 브로커 상태가 불일치할 위험을 줄이기 위해 사용됩니다.
왜 필요하지?
문제 상황
예를 들어 OrderService
에서 주문을 생성하고 Kafka
를 통해 "주문 생성됨" 이벤트를 보내는 경우를 보자:
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // DB 저장 성공
kafkaProducer.send(orderCreatedEvent); // Kafka 전송 실패!
}
결과:
- DB에는 주문이 저장됨
- Kafka에는 아무 이벤트도 전송되지 않음
→ 시스템 간 데이터 불일치 발생 (이벤트 소비자는 이 주문을 모름)
✅ 트랜잭셔널 아웃박스란?
핵심 아이디어:
- 메시지를 외부로 바로 전송하지 않고, 먼저 로컬 데이터베이스의
outbox
테이블에 저장한다. - DB 트랜잭션 내에서 비즈니스 데이터와 메시지를 함께 커밋한다.
- 별도의 백그라운드 프로세스(폴링 프로세서)가
outbox
테이블을 읽고 외부 시스템(Kafka 등)에 메시지를 발행한다.
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
outboxRepository.save(new OutboxMessage(...)); // 같은 트랜잭션
}
이후 백그라운드에서:
public void processOutboxMessages() {
List<OutboxMessage> messages = outboxRepository.findUnprocessed();
for (OutboxMessage message : messages) {
kafkaProducer.send(message);
outboxRepository.markAsSent(message);
}
}
✅ 장점
- DB와 메시지의 원자성 확보: 둘 다 하나의 트랜잭션에 포함되어 일관성 보장
- 이벤트 유실 방지: 메시지 발송 실패 시 재시도 가능 (Outbox는 그대로 있음)
- 보완적이며 유연함: 복잡한 분산 트랜잭션(XA) 없이도 일관성을 유지 가능
- 브로커 장애 대응 용이: 메시지 브로커가 잠시 꺼져도 Outbox에 저장된 메시지는 나중에 재전송 가능
트랜잭셔널 아웃박스 패턴은 29cm 에서도 사용되었다고 한다(https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb)
이벤트 발행: 폴링 발행기 패턴
마이크로서비스 아키텍처에서 트랜잭션 일관성과 이벤트 발행의 분리를 해결하기 위한 패턴입니다.
주로 트랜잭셔널 아웃박스 패턴과 함께 사용되며, 신뢰성 있는 이벤트 발행을 가능하게 합니다.
왜 필요하지?
이벤트 발행과 DB 트랜잭션이 동시에 처리되지 않으면 이런 문제가 생긴다.
- DB는 커밋되었는데 메시지는 발행 실패 → 시스템 간 불일치
- 메시지는 발행됐는데 DB는 롤백됨 → 잘못된 이벤트 전파
이를 해결하려고 등장한 것이 Outbox + Polling Publisher입니다.
✅ 폴링 발행기 패턴이란?
정의:
DB에 저장된 이벤트(Outbox 테이블)를 주기적으로 폴링(polling) 하여,
아직 전송되지 않은 이벤트를 찾아 메시지 브로커(Kafka, RabbitMQ 등)에 발행(publish) 하는 패턴
✅ 구성 요소
- Outbox 테이블 (event_log 등)
- 실제 비즈니스 트랜잭션과 같은 DB 트랜잭션 안에서 이벤트 저장
- 예:
| id | payload | status | created_at | |-----|--------------------------|---------|-------------------| | 1 | {"orderId":123, ...} | PENDING | 2025-05-08 12:00 |
- Polling Publisher
- 주기적으로 Outbox 테이블을 조회 (
status = 'PENDING'
) - Kafka, RabbitMQ 등으로 메시지 전송
- 전송 성공 시
status
→SENT
로 업데이트
- 주기적으로 Outbox 테이블을 조회 (
- 정리 작업 (optional)
- 오래된 Outbox 데이터는 주기적으로 삭제 or 보관
✅ 장점
- 트랜잭션 일관성 보장: 메시지 저장과 DB 작업이 한 트랜잭션으로 묶임
- 신뢰성 있는 메시지 발행: 실패해도 다음 폴링 시 재시도 가능
- 복잡한 분산 트랜잭션 불필요: 로컬 트랜잭션으로 충분
✅ 단점
- 지연 발생 가능성: 폴링 주기에 따라 이벤트 발행이 늦어질 수 있음 (예: 1초 단위 폴링
- Outbox 테이블 커질 수 있음: 정리 로직 필요
- 추가 구성 요소 필요: Polling Publisher 배치 서비스 또는 별도 스레드 구현 필요
- 규모가 커질수록 DB 부하가 심해지기 때문에 규모가 작은 경우에만 적합한 방법이다.
✅ 구현 예시 (Spring + JPA + Kafka)
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
outboxRepository.save(new OutboxEvent("ORDER_CREATED", order.toEventPayload()));
}
// 별도 스케줄러로 처리
@Scheduled(fixedDelay = 1000)
public void publishPendingEvents() {
List<OutboxEvent> events = outboxRepository.findByStatus("PENDING");
for (OutboxEvent event : events) {
kafkaTemplate.send("order-topic", event.getPayload());
event.setStatus("SENT");
}
outboxRepository.saveAll(events);
}
그러면 여기서 궁금한것이 생긴다.
전송이 완료된 데이터들은 언제 삭제되는것이지? OUTBOX 테이블에 점점 쌓일텐데..
✅ OUTBOX 이벤트는 언제 삭제하나요?
전송이 완료된(status = SENT
) 이벤트는 다음 조건을 만족할 때 삭제한다.
- 정상적으로 메시지가 전송되었음이 확실할 때
- 재처리가 더 이상 필요 없는 충분한 시간이 지났을 때
- 예:
SENT
상태가 되고 1일 이상 경과한 이벤트
- 예:
1. 배치 작업 or 스케줄러 사용
DELETE FROM outbox_event
WHERE status = 'SENT'
AND created_at < NOW() - INTERVAL '1 day';
- 매 시간마다 실행하거나,
- Kafka 등에서 전송 확인 후 수동 삭제 가능
2. TTL 기반 자동 삭제 (DB 지원 시)
- PostgreSQL:
pg_cron
사용 - MongoDB: TTL 인덱스 지원
✅ 주의사항
- 즉시 삭제는 위험: 네트워크 지연 등으로 전송이 실제 실패했는데 SENT로 표시됐을 수 있음
- Kafka의 exactly-once 전송 전략을 사용해도, DB 기준에서는 완전히 안전하지 않음 → 약간의 보존 기간 두는 것 권장
이런 DB 폴링 발행기 패턴은 규모가 작을 경우 쓸 수 있는 단순한 방법이다
규모가 커졌을때는 DB 트랜잭션 로그 테일링 방법이 좀 더 정교하고 성능이 좋은 방법이다.
이벤트 발행: 트랜잭션 로그 테일링 패턴

서비스는 DB 트랜잭션 로그를 뒤져 OUTBOX 테이블에 삽입된 메시지를 발행한다.
✅ 주된 사용 사례
- CDC (Change Data Capture) 시스템
- 디비지움: 오픈소스 CDC 플랫폼 (Kafka 기반)
- DynanoDB 스트림즈: 최근 24시간동안 DynanoDB 테이블 아에템에 적용된 변경분을 시간 순으로 정렬한 데이터를 갖고있다.
- 이벤추에이트 트램: 오픈 소스 트랜잭션 메시징 라이브러리, MySQL 빈로그 포로토콜, Postgres WAL, 폴링을 이용해서 OUTBOX 테이블의 변경분을 읽어 아파치 카프카로 발행
- 실시간 복제 또는 동기화 (예: MySQL → Elasticsearch)
- 이벤트 기반 시스템 (데이터 변경 이벤트를 메시지 큐로 발행)
- 감사 및 모니터링 (Audit logging)