왜 마이크로서비스는 패턴이 필요할까요?
안녕하세요, 개발자 여러분! 마이크로서비스 아키텍처(MSA)가 IT 업계의 대세가 된 지는 꽤 오래되었습니다. 작고 독립적인 서비스들이 유기적으로 연결되어 거대한 시스템을 만들어내는 이 방식은, 분명 민첩성(Agility)과 확장성(Scalability)이라는 엄청난 이점을 제공합니다. 하지만 이 빛나는 이면에는 우리가 반드시 해결해야 할 복잡성이라는 그림자가 숨어 있습니다. 😫
특히, 분산된 데이터 관리와 서비스 간의 통신 문제는 모놀리식(Monolithic) 아키텍처에서는 겪지 못했던 새로운 도전 과제들을 던져줍니다. 예를 들어, 여러 서비스에 걸쳐 일어나는 비즈니스 트랜잭션은 어떻게 원자성(Atomicity)을 보장할까요? 서비스가 수십 개로 늘어날 때 데이터 조회를 어떻게 효율적으로 처리할까요?
이러한 고통스러운 문제들에 대한 명쾌하고 체계적인 해답을 제시해 준 것이 바로 마이크로서비스 분야의 대가, 크리스 리처드슨(Chris Richardson) 님의 저서 **《마이크로서비스 패턴(Microservice Patterns)》**입니다. 이 책에는 무려 44가지의 패턴이 담겨 있지만, 그중에서도 오늘 저희가 집중적으로 파헤쳐 볼 핵심 중의 핵심, 마이크로서비스의 생존과 성공을 좌우하는 8가지 필수 패턴이 있습니다.
오늘 이 글에서는 이 [주요 키워드1: 마이크로서비스 패턴] 중에서도 가장 핵심적인 [주요 키워드2: Saga], **[주요 키워드3: Event Sourcing]**을 포함한 8가지 패턴을 깊이 있게 다루고, 독자 여러분이 실제 프로젝트에 바로 적용할 수 있도록 Java 샘플 코드와 함께 친절하게 설명해 드릴 거예요. 이 글을 통해 복잡했던 MSA의 원리를 명확히 이해하고, 여러분의 시스템을 더욱 견고하고 우아하게 설계하는 통찰력을 얻으시길 바랍니다!
본론 1: 분산 트랜잭션의 구원자, Saga (사가) 패턴
H2. Saga 패턴: 분산 트랜잭션의 일관성을 확보하는 드라마
모놀리식 애플리케이션에서는 데이터베이스 트랜잭션(ACID) 하나로 여러 로직의 일관성을 보장할 수 있었습니다. 하지만 마이크로서비스는 각자 독립적인 데이터베이스를 가지므로, 하나의 비즈니스 프로세스(예: 주문 → 결제 → 재고 차감)가 여러 서비스에 걸쳐 실행될 때 문제가 발생합니다. 이 문제를 해결해 주는 마법 같은 패턴이 바로 Saga (사가) 패턴입니다. ✨
[주요 키워드2: Saga] 패턴이란, 분산된 트랜잭션을 일련의 로컬 트랜잭션(Local Transactions)으로 구성하고, 각 로컬 트랜잭션이 성공적으로 완료되면 다음 트랜잭션을 시작합니다. 만약 중간에 어떤 트랜잭션이라도 실패하면, 이전에 성공했던 트랜잭션들을 되돌리는 **보상 트랜잭션(Compensating Transaction)**을 실행하여 전체적인 일관성을 복구하는 방식입니다. 마치 긴 드라마처럼 스텝 바이 스텝 진행하다가 문제가 생기면 이전 회차로 돌아가 문제를 해결하는 것과 같죠.
Saga는 크게 두 가지 구현 방식으로 나뉩니다.
- Choreography (안무): 중앙 집중식 관리자 없이, 각 서비스가 이벤트를 발행하고 구독하면서 순서대로 진행합니다. 마치 오케스트라의 연주자들이 지휘자 없이 서로의 연주를 듣고 박자를 맞추는 것과 같습니다. 단순한 Saga에 적합합니다.
- Orchestration (지휘): 중앙 집중식 관리자(Saga Orchestrator)를 두어 트랜잭션의 흐름을 지시하고 관리합니다. 복잡한 비즈니스 로직과 많은 서비스가 얽혀있을 때 흐름을 파악하기 쉽고 보상 로직 구현이 용이합니다.
H3. Saga 패턴의 Java 예시 (Orchestration 방식)
여기서는 비즈니스 로직의 복잡성을 관리하기 쉬운 Orchestration 방식을 간단한 **주문 프로세스(Order Service → Payment Service → Inventory Service)**에 적용하는 예시를 Java 코드로 간략히 보겠습니다.
// 1. Saga Orchestrator (중앙 지휘자) 인터페이스
public interface OrderSagaOrchestrator {
void startOrderCreation(OrderDetails details);
void handlePaymentSuccess(String orderId);
void handlePaymentFailure(String orderId);
void handleInventorySuccess(String orderId);
void handleInventoryFailure(String orderId);
}
// 2. Order Service
@Service
public class OrderService {
@Autowired private OrderSagaOrchestrator orchestrator;
public String createOrder(OrderDetails details) {
Order order = new Order(details);
orderRepository.save(order);
// 트랜잭션 시작을 오케스트레이터에게 지시
orchestrator.startOrderCreation(details);
return order.getId();
}
// 보상 트랜잭션 (재고 차감 실패 시 주문 취소)
public void cancelOrder(String orderId) {
Order order = orderRepository.findById(orderId);
if (order != null) {
order.setStatus(OrderStatus.CANCELED);
orderRepository.save(order);
// 다른 서비스에게도 보상 트랜잭션(결제 취소)을 지시해야 함 (Orchestrator 역할)
}
}
}
// 3. 간략화된 Saga Orchestrator 구현 (스텝 관리)
@Service
public class SimpleOrderOrchestrator implements OrderSagaOrchestrator {
// ... 필드 (PaymentServiceProxy, InventoryServiceProxy 등)
@Override
public void startOrderCreation(OrderDetails details) {
// Step 1: 결제 요청
try {
paymentServiceProxy.requestPayment(details.getOrderId(), details.getAmount());
} catch (Exception e) {
// 결제 시스템 통신 오류 시
orderService.cancelOrder(details.getOrderId()); // 보상 트랜잭션
}
}
@Override
public void handlePaymentSuccess(String orderId) {
// Step 2: 결제 성공 -> 재고 차감 요청
inventoryServiceProxy.reserveStock(orderId);
}
@Override
public void handleInventoryFailure(String orderId) {
// Step 2 실패 시 보상 트랜잭션: Step 1 되돌리기
paymentServiceProxy.cancelPayment(orderId);
orderService.cancelOrder(orderId);
}
// ... 기타 핸들러 구현
}
본론 2: 데이터 무결성과 감사 추적의 열쇠, Event Sourcing (이벤트 소싱)
H2. Event Sourcing 패턴: ‘현재’가 아닌 ‘과정’을 저장하라
일반적인 애플리케이션은 데이터의 **현재 상태(Current State)**만을 데이터베이스에 저장합니다. 예를 들어, 은행 잔고는 최종 금액만 저장합니다. 하지만 잔고가 어떻게, 왜 그렇게 되었는지(입금, 출금, 이자 등)의 **과정(History)**이 중요할 때가 있습니다. **[주요 키워드3: Event Sourcing]**은 바로 이 ‘과정’에 집중하는 혁신적인 데이터 관리 패턴입니다.
Event Sourcing은 애플리케이션의 상태 변화를 **이벤트(Event)**의 시퀀스로 저장하는 방식입니다. 새로운 상태 변화가 발생할 때마다, 현재 상태를 덮어쓰는 대신, 그 변화를 설명하는 불변(Immutable)의 이벤트를 **이벤트 스토어(Event Store)**라는 특별한 데이터베이스에 추가(Append Only)합니다.
장점:
- 완벽한 감사 추적: 모든 상태 변화 과정이 기록되므로, 누가, 언제, 무엇을 했는지 완벽하게 알 수 있습니다.
- 시간 여행(Time-Travel) 가능: 특정 시점까지의 이벤트들을 ‘재생(Replay)’하여 과거의 특정 상태를 정확히 재구성할 수 있습니다.
- CQRS와 시너지: 이벤트 스트림을 사용하여 읽기 전용 뷰를 쉽게 업데이트할 수 있어 CQRS 패턴과 결합될 때 엄청난 시너지를 냅니다.
H3. Event Sourcing 패턴의 Java 예시 (Aggregate & Event)
Event Sourcing에서 중요한 개념은 **애그리거트(Aggregate)**와 **이벤트(Event)**입니다. 애그리거트는 비즈니스 경계를 나타내고, 애그리거트의 상태 변화는 이벤트를 통해 발생합니다.
// 1. 기본 이벤트 인터페이스 (불변 객체)
public interface DomainEvent {
Instant getOccurredOn();
String getAggregateId();
int getVersion();
}
// 2. 특정 비즈니스 이벤트 예시
public class AccountCreatedEvent implements DomainEvent {
private final String accountId;
private final String ownerName;
private final Instant occurredOn = Instant.now();
// ... 생성자, Getter (불변성을 위해 Setter 없음)
}
public class MoneyDepositedEvent implements DomainEvent {
private final String accountId;
private final BigDecimal amount;
// ...
}
// 3. 애그리거트 (Aggregate) - 상태 변화를 이벤트로 기록
public class AccountAggregate {
private String id;
private BigDecimal balance;
private List<DomainEvent> changes = new ArrayList<>();
// 상태 재구성: 이벤트 스트림을 통해 현재 상태를 만듦
public static AccountAggregate loadFromHistory(List<DomainEvent> history) {
AccountAggregate account = new AccountAggregate();
history.forEach(account::apply); // 각 이벤트 적용
account.changes.clear(); // 로드 후에는 변경 목록 초기화
return account;
}
// 비즈니스 로직: 잔고 변경은 반드시 이벤트를 생성해야 함
public void deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) > 0) {
MoneyDepositedEvent event = new MoneyDepositedEvent(this.id, amount);
apply(event); // 내부 상태 변경
changes.add(event); // 변경 목록에 추가
}
}
// 이벤트 적용 (상태 변경 로직)
private void apply(DomainEvent event) {
if (event instanceof AccountCreatedEvent) {
this.id = ((AccountCreatedEvent) event).getAccountId();
this.balance = BigDecimal.ZERO;
} else if (event instanceof MoneyDepositedEvent) {
this.balance = this.balance.add(((MoneyDepositedEvent) event).getAmount());
}
// ... 다른 이벤트 처리
}
public List<DomainEvent> getUncommittedChanges() {
return Collections.unmodifiableList(changes);
}
}
설명: AccountAggregate는 deposit() 메서드가 호출될 때 직접 잔고를 변경하는 대신, MoneyDepositedEvent를 생성하고 이를 changes 목록에 추가합니다. 실제 잔고(balance)는 apply() 메서드에서 이벤트를 처리할 때만 변경됩니다. 데이터베이스에는 이 이벤트 객체들만 순서대로 저장됩니다. 이 구조는 **[주요 키워드1: 마이크로서비스 패턴]**의 데이터 무결성을 높이는 핵심입니다.
본론 3: 효율적인 조회 전략, API Composition (API 컴포지션)
H2. API Composition 패턴: 흩어진 데이터를 한 번에 모으는 마법
마이크로서비스 아키텍처에서는 고객이 필요로 하는 하나의 정보를 얻기 위해 여러 서비스에 분산된 데이터를 조합해야 하는 경우가 빈번합니다. 예를 들어, 웹사이트의 ‘주문 상세 정보’ 화면을 생각해 보세요.
- 주문 기본 정보 (Order Service)
- 결제 내역 (Payment Service)
- 배송 현황 (Shipping Service)
이 세 가지 정보를 고객에게 보여주려면 클라이언트가 각 서비스를 개별적으로 호출해야 할까요? 아닙니다! 이것은 통신 오버헤드를 높이고 클라이언트의 복잡성을 증가시킵니다.
API Composition 패턴은 바로 이 문제를 해결하기 위해 **집계 서비스(Aggregator Service)**를 도입합니다. 이 서비스는 클라이언트의 요청을 받아, 필요한 모든 하위 마이크로서비스(Sub-Microservices)들을 호출하고, 그 결과를 모아(Compose) 하나의 응답으로 변환하여 클라이언트에게 돌려줍니다.
이 패턴은 클라이언트에게 깔끔하고 단일화된 API 엔드포인트를 제공하여, **[주요 키워드1: 마이크로서비스 패턴]**의 프론트엔드 개발을 단순화하는 데 크게 기여합니다. 특히, 모바일과 웹 등 다양한 클라이언트를 지원해야 할 때 유용하며, 경우에 따라 API Gateway 레이어에서 이 기능을 수행하기도 합니다.
H3. API Composition 패턴의 Java 예시 (Aggregator Service)
OrderDetailsService라는 집계 서비스가 Order Service와 Payment Service를 호출하여 정보를 조합하는 예시입니다.
// DTO: 고객에게 반환할 최종 조합 데이터
public class OrderDetailsDto {
private OrderInfo orderInfo;
private PaymentInfo paymentInfo;
// ... (Getter, Setter)
}
// 하위 서비스와 통신할 인터페이스
public interface OrderServiceProxy {
OrderInfo getOrderInfo(String orderId);
}
public interface PaymentServiceProxy {
PaymentInfo getPaymentInfo(String orderId);
}
@Service
public class OrderDetailsService {
@Autowired private OrderServiceProxy orderService;
@Autowired private PaymentServiceProxy paymentService;
// 핵심! 여러 서비스를 호출하고 데이터를 모으는 컴포지션 로직
public OrderDetailsDto getOrderDetails(String orderId) {
// 1. 주문 기본 정보 요청 (Order Service)
OrderInfo orderInfo = orderService.getOrderInfo(orderId);
// 2. 결제 정보 요청 (Payment Service)
PaymentInfo paymentInfo = paymentService.getPaymentInfo(orderId);
// 3. 데이터를 하나의 DTO로 조합 (Composition)
OrderDetailsDto dto = new OrderDetailsDto();
dto.setOrderInfo(orderInfo);
dto.setPaymentInfo(paymentInfo);
return dto;
}
// 참고: 성능 향상을 위해 병렬 호출(CompletableFuture)을 고려해야 함
public OrderDetailsDto getOrderDetailsParallel(String orderId) {
// 1. 주문 정보 요청을 비동기적으로 시작
CompletableFuture<OrderInfo> orderFuture = CompletableFuture.supplyAsync(
() -> orderService.getOrderInfo(orderId)
);
// 2. 결제 정보 요청을 비동기적으로 시작
CompletableFuture<PaymentInfo> paymentFuture = CompletableFuture.supplyAsync(
() -> paymentService.getPaymentInfo(orderId)
);
// 3. 두 결과가 모두 완료될 때까지 기다렸다가 조합
return CompletableFuture.allOf(orderFuture, paymentFuture)
.thenApply(v -> {
OrderDetailsDto dto = new OrderDetailsDto();
try {
dto.setOrderInfo(orderFuture.get());
dto.setPaymentInfo(paymentFuture.get());
} catch (Exception e) {
throw new RuntimeException("API Composition Failed", e);
}
return dto;
}).join();
}
}
본론 4: 조회 성능 최적화, CQRS (Command Query Responsibility Segregation)
H2. CQRS 패턴: 쓰기와 읽기를 분리하여 성능을 극대화
CQRS (Command Query Responsibility Segregation) 패턴은 애플리케이션의 **쓰기(Command)**와 읽기(Query) 역할을 완전히 분리하는 아키텍처 패턴입니다. 대부분의 시스템에서 읽기 작업이 쓰기 작업보다 훨씬 많기 때문에, 이 둘을 분리하여 각각의 워크로드에 최적화된 설계를 적용하는 것이 목표입니다.
- Command Model (쓰기 모델): 복잡한 비즈니스 로직을 처리하며 데이터의 일관성을 보장합니다. 전통적인 RDBMS나 [주요 키워2: Saga] 패턴이 여기에 해당됩니다.
- Query Model (읽기 모델): 단순하고 빠른 데이터 조회를 위해 최적화됩니다. NoSQL(MongoDB, Cassandra)이나 최적화된 RDBMS 뷰를 사용하며, 조인을 최소화한 비정규화된(Denormalized) 형태로 데이터를 저장합니다.
쓰기 작업이 발생하면, Command 모델에서 이벤트가 발생하고(이때 **[주요 키워드3: Event Sourcing]**이 함께 사용되면 시너지가 극대화됨), 이 이벤트를 구독하여 Query 모델의 데이터를 업데이트합니다. 이 분리를 통해 읽기 성능(확장성)과 쓰기 일관성(정확성)이라는 두 마리 토끼를 모두 잡을 수 있습니다.
H3. CQRS 패턴의 Java 예시 (간단한 구조)
Spring Boot 환경에서 CQRS를 적용하는 기본적인 구조입니다. Command와 Query를 별도의 클래스로 분리하고 전용 핸들러를 만듭니다.
// 1. Command (쓰기 요청)
public class CreateProductCommand {
private final String name;
private final BigDecimal price;
// ... Getter, 생성자
}
// 2. Command Handler (쓰기 로직 처리)
@Service
public class ProductCommandHandler {
@Autowired private ProductRepository commandRepository; // 쓰기 전용 DB
public String handle(CreateProductCommand command) {
Product product = new Product(command.getName(), command.getPrice());
commandRepository.save(product);
// Event Sourcing을 사용한다면 여기서 ProductCreatedEvent를 발행
return product.getId();
}
}
// 3. Query (읽기 요청)
public class GetProductDetailQuery {
private final String productId;
// ... Getter, 생성자
}
// 4. Query Handler (읽기 로직 처리)
@Service
public class ProductQueryHandler {
@Autowired private ProductViewRepository queryRepository; // 읽기 전용 DB (ex: MongoDB)
public ProductDetailsDto handle(GetProductDetailQuery query) {
// 비정규화된 읽기 전용 뷰에서 바로 조회
return queryRepository.findById(query.getProductId());
}
}
// 5. 컨트롤러에서 분리된 핸들러 사용
@RestController
public class ProductController {
@Autowired private ProductCommandHandler commandHandler;
@Autowired private ProductQueryHandler queryHandler;
@PostMapping("/products") // 쓰기
public String createProduct(@RequestBody CreateProductCommand command) {
return commandHandler.handle(command);
}
@GetMapping("/products/{id}") // 읽기
public ProductDetailsDto getProduct(@PathVariable String id) {
return queryHandler.handle(new GetProductDetailQuery(id));
}
}
본론 5: 서비스 외부와의 연결, External API (외부 API)
H2. External API 패턴: 외부 시스템과의 안전하고 견고한 통합
마이크로서비스는 고립되어 존재하지 않습니다. 결제 서비스(PG), SMS 발송 서비스, 외부 CRM 등 수많은 **외부 시스템(External Systems)**과 연동해야 합니다. External API 패턴은 이 외부 서비스와의 통신을 안전하고 안정적으로 처리하는 방법을 다룹니다.
가장 중요한 원칙은 외부 종속성(External Dependency)을 최소화하고, 외부 시스템 장애가 우리 서비스 전체로 전파되는 것을 **차단(Isolate)**하는 것입니다.
핵심 적용 방법:
- Anti-Corruption Layer (ACL): 외부 시스템의 데이터 모델/용어와 내부 시스템의 데이터 모델/용어를 분리하고 매핑하는 변환 계층을 만듭니다. 외부의 불순물이 내부로 들어오는 것을 막는 ‘방탄복’ 역할을 합니다.
- Circuit Breaker (회로 차단기): 외부 API 호출이 일정 횟수 이상 실패하면, 잠시 동안 그 호출을 차단하여(Fail Fast), 장애가 발생한 외부 시스템에 대한 부하를 줄이고 내부 서비스의 자원 소모를 막습니다.
- Retry 및 Time-out 정책: 네트워크 지연이나 일시적 오류에 대비하여 적절한 재시도 횟수와 대기 시간(Exponential Backoff)을 설정하고, 무한 대기 상태를 막기 위한 명확한 타임아웃을 설정해야 합니다.
H3. External API 패턴의 Java 예시 (Circuit Breaker 적용)
Java에서는 보통 Resilience4j 라이브러리를 사용하여 Circuit Breaker를 쉽게 적용할 수 있습니다.
// 1. build.gradle 또는 pom.xml 에 Resilience4j 의존성 추가
// 2. 외부 결제 시스템과 통신하는 프록시 서비스
@Service
public class ExternalPaymentProxy {
private final RestTemplate restTemplate;
// ... 생성자
// @CircuitBreaker 어노테이션을 사용하여 회로 차단기 적용
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public PaymentResult processPayment(PaymentRequest request) {
String url = "https://external-pg.com/api/payment";
// 외부 API 호출
return restTemplate.postForObject(url, request, PaymentResult.class);
}
// 회로 차단기가 열리거나 호출 중 예외 발생 시 실행되는 대체 로직
private PaymentResult fallbackPayment(PaymentRequest request, Throwable t) {
// Log the error (t)
System.err.println("결제 서비스 장애 발생: " + t.getMessage());
// 결제 요청을 즉시 실패(Fail Fast) 처리하거나,
// 메시지 큐에 넣어 나중에 재처리(Async Retry)하도록 로직 구현
return PaymentResult.fail("External payment service is currently unavailable.");
}
}
본론 6: 안정적인 메시징 보장, Transactional Outbox & Polling Publisher & Transaction Log Tailing
이 세 가지 패턴은 모두 마이크로서비스 아키텍처의 가장 까다로운 문제 중 하나인 **”서비스 로컬 데이터베이스 트랜잭션”**과 **”메시지 브로커로의 이벤트 발행”**을 **원자적(Atomically)**으로 처리하는 방법을 다룹니다. 즉, 데이터베이스 저장과 메시지 전송 두 가지가 반드시 동시에 성공하거나 동시에 실패해야 하는 요구사항을 충족시키기 위한 패턴들입니다.
H2. 6-1. Transactional Outbox (트랜잭션 아웃박스)
Transactional Outbox 패턴은 로컬 데이터베이스 트랜잭션 내에 Outbox 테이블을 함께 사용하는 방식입니다.
- 비즈니스 데이터(예:
ORDER테이블)를 저장합니다. - 동일한 데이터베이스 트랜잭션 내에서, 발행할 이벤트 메시지(
OrderCreatedEvent등)를OUTBOX테이블에 저장합니다. - 두 작업(데이터 저장 및 이벤트 저장)이 하나의 로컬 트랜잭션으로 커밋되므로, **원자성(Atomicity)**이 보장됩니다.
- 이후
OUTBOX테이블의 레코드를 메시지 브로커(Kafka 등)로 전송하는 별도의 프로세스가 실행됩니다.
이 패턴은 가장 널리 사용되며, [주요 키워드1: 마이크로서비스 패턴] 중 데이터 무결성을 보장하는 핵심 패턴입니다.
H2. 6-2. Polling Publisher (폴링 발행자)
Polling Publisher는 Transactional Outbox 패턴의 다음 단계입니다.
- 서비스는 Transactional Outbox에 이벤트를 저장합니다.
- 별도의 폴링 발행자 컴포넌트가 주기적으로(Polling)
OUTBOX테이블을 조회(Select)하여, 아직 전송되지 않은 메시지들을 읽어옵니다. - 읽어온 메시지들을 메시지 브로커로 발행(Publish)합니다.
- 성공적으로 발행한 후,
OUTBOX테이블에서 해당 레코드를 전송 완료 상태로 업데이트하거나 삭제합니다.
단점: 데이터베이스에 지속적으로 부하를 주며(잦은 Select), 메시지 전송에 약간의 지연(Polling Interval)이 발생할 수 있습니다.
H2. 6-3. Transaction Log Tailing (트랜잭션 로그 테일링)
Transaction Log Tailing은 Outbox 패턴의 또 다른 구현 방식이자, Polling Publisher의 단점을 극복하는 대안입니다.
- 서비스는 마찬가지로 Transactional Outbox 테이블에 이벤트를 저장합니다.
- 별도의 프로세스(예: Debezium 같은 CDC(Change Data Capture) 도구)가 데이터베이스의 **트랜잭션 로그(Transaction Log, WAL)**를 직접 모니터링합니다.
OUTBOX테이블에 새로운 레코드가 추가되는 것을 로그를 통해 감지합니다.- 감지 즉시 해당 변경(이벤트)을 읽어와 메시지 브로커로 발행합니다.
장점: 데이터베이스에 폴링 부하를 주지 않고, 거의 실시간(Near Real-time)으로 이벤트를 전송할 수 있습니다. 가장 효율적이고 현대적인 방식 중 하나입니다.
H3. Transactional Outbox 패턴의 Java 예시 (Outbox 테이블 구조)
Java 애플리케이션에서는 Outbox 테이블을 정의하고, 비즈니스 트랜잭션과 Outbox 테이블 저장을 하나의 @Transactional 어노테이션으로 묶는 것이 핵심입니다.
// 1. Outbox Event 엔티티 (DB 테이블 구조)
@Entity
@Table(name = "OUTBOX_EVENT")
public class OutboxEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String aggregateId; // 어떤 엔티티에 대한 이벤트인지
private String aggregateType;
private String type; // 이벤트 이름 (ex: OrderCreated)
@Lob
private String payload; // 이벤트 본문 (JSON 형태)
private Instant createdOn = Instant.now();
private boolean published = false; // Polling Publisher가 이 필드를 사용
// ... Getter, Setter
}
// 2. 비즈니스 로직에 Outbox 저장 로직 포함 (Spring Data JPA 사용 예시)
@Service
public class OrderManagementService {
@Autowired private OrderRepository orderRepository;
@Autowired private OutboxEventRepository outboxRepository;
// 하나의 DB 트랜잭션으로 묶기
@Transactional
public String createOrderAndPublishEvent(OrderDetails details) {
// 1. 비즈니스 데이터 저장
Order order = new Order(details);
orderRepository.save(order);
// 2. Outbox 테이블에 이벤트 저장 (동일 트랜잭션 내)
OrderCreatedEvent eventPayload = new OrderCreatedEvent(order.getId(), details.getCustomerId());
OutboxEvent outbox = new OutboxEvent();
outbox.setAggregateId(order.getId());
outbox.setType("OrderCreated");
// Payload를 JSON으로 변환 (Jackson ObjectMapper 사용)
outbox.setPayload(new ObjectMapper().writeValueAsString(eventPayload));
outboxRepository.save(outbox);
return order.getId();
}
}
특별 정보: FAQ (자주 묻는 질문)

결론: 복잡성을 넘어서 우아한 아키텍처로
오늘 저희는 **[주요 키워드1: 마이크로서비스 패턴]**의 근간을 이루는 8가지 핵심 패턴 중, [주요 키워2: Saga], [주요 키워드3: Event Sourcing], API Composition, CQRS, External API, Transactional Outbox, Polling Publisher, Transaction Log Tailing까지 자세히 살펴보았습니다.
마이크로서비스 아키텍처는 분명 높은 자유도와 확장성을 제공하지만, 그 대가로 분산된 데이터와 통신이라는 복잡성을 우리에게 안겨주었습니다. 이 복잡성을 회피하는 대신, 오늘 배운 패턴들(특히 분산 트랜잭션을 위한 Saga와 데이터 이력 관리를 위한 Event Sourcing)을 활용하여 문제를 정면 돌파해야 합니다.
이 패턴들은 단순한 이론이 아니라, 수많은 기업의 성공과 실패를 통해 검증된 실전 전략입니다. 여러분의 다음 마이크로서비스 프로젝트를 설계할 때, 이 핵심 패턴들을 반드시 고려하여 더욱 견고하고 유지보수하기 쉬우며, 궁극적으로는 사용자들에게 더 빠르고 안정적인 서비스를 제공하는 우아한 시스템을 구축하시길 바랍니다!