들어가기 전
앞선 글에서 분산 트랜잭션의 개념과 이를 해결하기 위한 두 가지 주요 패턴인 2-Phase Commit과 SAGA 패턴을 살펴보았습니다.
하지만 서비스 간 트랜잭션 관리뿐만 아니라 시스템의 확장성과 유지보수성을 고려한 아키텍처 설계도 중요한 요소입니다.
특히 모놀리틱 애플리케이션(Monolithic Application) 환경에서는 서비스 간 상호작용이 대부분 직접적인 메서드 호출이나 API 호출을 통해 이루어지지만 서비스의 규모가 커지고 기능 간 의존성이 증가할수록 서비스간 강한 결합이 발생하고 하나의 서비스 변경이 전체 시스템에 영향을 미치는 리스크도 커집니다.
이를 해결하기 위한 접근 방식 중 하나가 이벤트 기반 아키텍처(Event-Driven Architecture, EDA) 이며 이를 활용하여 구현된 스프링 이벤트 리스너(Spring Event Listener) 가 있습니다. 이번 글에서는 서비스 간 강결합을 피하고 독립적인 서비스 간 통신을 가능하게 하는 이벤트 리스너를 살펴보고 최종적으로 데이터 일관성을 유지하는 방법에 대해 알아보겠습니다.
이벤트 기반 아키텍처란?
이벤트 기반 아키텍처(Event-Driven Architecture, EDA)는 시스템 구성 요소 간의 통신이 이벤트의 생성, 감지 및 소비를 통해 이루어지는 소프트웨어 디자인 패러다임입니다. 이벤트란 시스템 내에서 발생한 상태 변화나 중요한 사건을 의미하며, 이 이벤트를 중심으로 시스템 간 상호작용이 이루어집니다.
문제 상황
MA 환경에서도 충분히 유연하고 확장 가능한 구조를 설계할 수 있지만 대부분 시간이 지남에 따라 기능이 점점 많아지고 여러 서비스가 직접적으로 엮이면서 자연스럽게 강한 결합이 발생하는 경향이 있습니다.
문제의 본질은 아키텍처가 아니라 서비스 간의 설계 방식에 있습니다만 MA 환경은 강한 결합을 피하기 어렵고 유지보수성과 확장성이 점점 낮아지기 쉽습니다.
강한 결합
class PaymentService {
fun processPayment(orderId: String, amount: Double) {
println("결제 처리: 주문 ID = $orderId, 금액 = $amount")
}
}
class OrderService(private val paymentService: PaymentService) {
fun createOrder(orderId: String, amount: Double) {
println("주문 생성: 주문 ID = $orderId")
paymentService.processPayment(orderId, amount)
}
}
OrderService
는 PaymentService
를 직접 호출하므로 PaymentService
의 메서드 시그니처가 변경되면 그 영향을 함께 받게됩니다.
추가로 PaymentService
가 외부 결제 시스템과 연동되면서 비동기 처리가 필요해진다면 OrderService
의 흐름도 이에 맞춰 수정해야 합니다.
확장성 제한
class InventoryService {
fun checkStock(orderId: String): Boolean {
println("재고 확인: 주문 ID = $orderId")
return true
}
}
class NotificationService {
fun sendNotification(orderId: String) {
println("알림 전송: 주문 ID = $orderId")
}
}
class OrderService(
private val paymentService: PaymentService,
private val inventoryService: InventoryService,
private val notificationService: NotificationService
) {
fun createOrder(orderId: String, amount: Double) {
if (!inventoryService.checkStock(orderId)) {
println("재고 부족으로 주문 실패")
return
}
paymentService.processPayment(orderId, amount)
notificationService.sendNotification(orderId)
println("주문 완료: 주문 ID = $orderId")
}
}
OrderService
가 여러 서비스를 직접 호출하면서 점점 비대(Blob Object)해지기 때문에 유지보수가 어려워집니다.
개별 기능(결제, 재고 확인, 알림 전송) 중 하나라도 실패하면 주문 로직 전체가 영향을 받습니다.
순환 참조
@Service
class OrderService(private val paymentService: PaymentService) {
fun createOrder(orderId: String) {
println("주문 생성: $orderId")
paymentService.processPayment(orderId)
}
}
@Service
class PaymentService(private val orderService: OrderService) {
fun processPayment(orderId: String) {
println("결제 처리: $orderId")
orderService.updateOrderStatus(orderId)
}
fun updateOrderStatus(orderId: String) {
println("주문 상태 업데이트: $orderId")
}
}
위 코드는 OrderService
와 PaymentService
가 서로를 직접 참조하는 구조로 되어 있습니다.
이처럼 서로 강하게 연결된 상태 에서는 객체가 생성될 때 순환 참조 오류가 발생합니다.
OrderService
는 주문을 생성한 후 결제를 처리하기 위해 PaymentService
를 호출합니다.PaymentService
는 결제를 처리한 후 주문 상태를 변경하기 위해 다시 OrderService
를 호출합니다.
이 과정에서 두 서비스가 서로 의존하면서 스프링 컨텍스트가 두 객체를 빈(Bean)으로 생성하는 시점에 BeanCurrentlyInCreationException
예외가 발생합니다.
느슨한 결합을 위한 이벤트 기반 접근
앞서 살펴본 모놀리틱 환경에서의 서비스 간 직접적인 호출이 많아질수록 유지보수성이 낮아지고 변경이 어려운 구조가 됩니다.
이를 해결하기 위해 등장한 것이 이벤트 기반 아키텍처(Event-Driven Architecture) 입니다.
EDA 를 활용하여 이제 OrderService
가 PaymentService
를 직접 호출하는 대신 이벤트를 발행(Publish) 하고 PaymentService
가 구독(Subscribe) 하는 방식으로 변경해보겠습니다.
1. 이벤트 정의
이벤트는 단순한 데이터 객체(POJO) 로 선언합니다.
data class OrderCreatedEvent(val orderId: String)
2. 이벤트 발행 (Publisher)
기존에는 OrderService
가 PaymentService
를 직접 호출했지만 이제는 이벤트를 생성하여 직접적인 의존성을 제거할 수 있도록 정의합니다.
class OrderEventPublisher(private val subscribers: List<(OrderCreatedEvent) -> Unit>) {
fun publish(event: OrderCreatedEvent) {
println("주문 생성 이벤트 발행: ${event.orderId}")
subscribers.forEach { it(event) }
}
}
3. 이벤트 구독 (Subscriber)
PaymentService
는 이제 OrderService
를 직접 참조하지 않고 이벤트를 구독하여 결제를 처리합니다.
class PaymentService {
fun handleOrderCreated(event: OrderCreatedEvent) {
println("주문 이벤트 감지 -> 결제 처리: ${event.orderId}")
}
}
4. 이벤트 기반 서비스 실행
이제 OrderService
에서 주문을 생성하면 OrderEventPublisher
를 통해 이벤트가 발행되고 이를 PaymentService
가 구독하여 처리합니다.
fun main() {
val paymentService = PaymentService()
// 이벤트 리스너 등록 (구독)
val orderEventPublisher = OrderEventPublisher(listOf(paymentService::handleOrderCreated))
val orderService = OrderService(orderEventPublisher)
orderService.createOrder("12345")
}
이벤트 기반 구조를 통해 OrderService
와 PaymentService
가 더이상 서로 직접 참조하지 않게 됐으며 추후 이런 이벤트를 사용해 비동기 처리 가능가 가능하도록 확장성이 증가됐습니다.PaymentService
변경 시 OrderService
를 수정할 필요 없기 때문에 유지보수성 또한 향상됐습니다.
스프링 이벤트
이벤트 기반 아키텍처를 사용하여 서비스 간 느슨한 결합을 통해 확장성과 유연성을 제공하며 변화에 더 민첩하게 대응할 수 있는 시스템을 구축할 수 있습니다.
다음 글에서는 스프링 프레임워크에서 제공하는 이벤트 리스너(Spring Event Listener)를 활용하여 실제 애플리케이션에서 이벤트 기반 아키텍처를 어떻게 구현할 수 있는지 또 이를 통해 어떻게 도메인간의 도메인 간 결합도를 낮출 수 있는지 살펴보겠습니다.
또한 @Async
를 통해 트랜잭션과 독립적으로 동작하는 작업을 실행하여 서비스의 응답성을 개선함과 동시에 비동기 처리로 인한 데이터 일관성을 어떻게 유지할 수 있는지 더 자세히 살펴보겠습니다.
오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.
'ETC' 카테고리의 다른 글
[ETC] 분산 트랜잭션 (0) | 2024.10.08 |
---|---|
[ETC] Merge 문의 동시성 (1) | 2024.03.24 |