TL;DR
- Command와 Event는 둘 다 메시지지만, 의도와 결합도에서 근본적으로 다르다
- @TransactionalEventListener의 BEFORE_COMMIT과 AFTER_COMMIT 선택은 "이 작업이 실패하면 메인 트랜잭션도 롤백되어야 하는가?"라는 질문으로 결정된다
- 이벤트는 확장성을 제공하지만, 일관성이 필요한 작업은 Command로 처리해야 한다
1. Command vs Event: 메시지의 두 얼굴
많은 개발자들이 EDA(Event-Driven Architecture)를 도입하면서 "이벤트"라는 용어에 집중한다. 하지만 본질적으로 EDA에서 구성 요소들이 주고받는 것은 "메시지"다. 이 메시지가 Command인지 Event인지에 따라 시스템의 결합도와 확장성이 완전히 달라진다.
1.1 코드로 보는 차이
Command 방식: 직접 명령
// ❌ Command 방식 - 강한 결합
class OrderService(
private val couponService: CouponService,
private val pointService: PointService,
private val pgClient: PgClient
) {
fun createOrder(...) {
val order = orderRepository.save(...)
// "쿠폰 사용해라", "포인트 기록해라", "결제 요청해라"
couponService.useCoupon(...)
pointService.record(...)
pgClient.requestPayment(...)
}
}
Event 방식: 사실 통보
```kotlin
// ✅ Event 방식 - 느슨한 결합
class OrderService(
private val eventPublisher: ApplicationEventPublisher
) {
fun createOrder(...) {
val order = orderRepository.save(...)
// "주문이 생성됐어" 라고 알림만 함
eventPublisher.publishEvent(OrderCreatedEvent.from(order))
// 누가 듣든 말든 OrderService는 신경 안 씀
}
}
```
1.2 핵심 차이: 의도와 시제
| 구분 | Event | Command |
| 의미 | "~이 발생했다" (사실 기록) | "~을 해라" (몀ㅇ령) |
| 시제 | 과거형 (OrderCreated) | 명령형 (CreateOrder) |
| 처리 주체 | 0~N개 (없어도 됨) | 반드시 1개 |
| 오류 처리 | 로그만 남기고 계속 진행 | 롤백/예외 전파 필요 |
| 제어 주체 | 발행자는 통보만, 처리는 위임 | 발행자가 흐름 제어 |
1.3 비유로 이해하기
회사에서 팀장이 업무를 처리하는 두 가지 방식을 상상해보자.
Command: 팀장이 김대리에게 "보고서 작성해"라고 직접 지시한다. 김대리가 없으면 업무가 막힌다. 팀장은 김대리가 누구인지, 어디 있는지 알아야 한다.
Event: 팀장이 슬랙에 "오늘 저녁 대방어 회식 있습니다"라고 공지한다. 누가 보든 말든, 몇 명이 참석하든 상관없다. 팀장은 수신자를 몰라도 된다.
Command는 명확한 대상에게 명령을 내리는 "독재" 방식이다. 시스템이 복잡해질수록(명령할 대상이 많아질수록) 명령자의 복잡도도 증가한다. Application 계층에서 각 도메인을 일일이 조율해야 한다.
Event는 "나는 내 맥락까지만 알면 됨"이다. 관심 있는 사람이 귀 기울이면 되고, 발행자는 누가 듣든 신경 쓰지 않는다. "주문이 발생했다" 이벤트에 결제와 재고가 관심을 가지면 되는 것이다.
2. 언제 Event를 쓰고, 언제 Command를 쓸까?
실제 프로젝트에서 가장 고민되는 부분이다. 이벤트로 모든 것을 처리하면 확장성은 좋아지지만, 코드 추적이 어려워지고 일관성 문제가 발생한다.
2.1 Event로 모델링할 것
부수적인 기능, 다른 방식으로 대체 가능한 것
- 알림 발송
- 로그 기록
- 통계 집계
- 검색 인덱스 업데이트
실제 코드 예시: 좋아요 기능
// 좋아요 집계는 실패해도 좋아요 자체는 성공
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleProductLiked(event: ProductLikeEvent.ProductLiked) {
transactionTemplate.execute {
productLikeRepository.increaseCount(event.productId)
}
// 집계 실패해도 좋아요는 성공 상태
// 배치 작업으로 나중에 보정 가능
}
2.2 Command로 모델링할 것
핵심 유스케이스, 실패 시 롤백이 필요한 것
- 주문 생성
- 결제 처리
- 재고 차감
왜 Command로 처리해야 할까? 일관성 경계 때문이다.
2.3 일관성 경계의 중요성

하나의 트랜잭션 안에서 처리해야 할 것은 Command로, Event는 이 흐름을 방해하지 않도록 설계한다
큰 작업을 여러 Event로 쪼개면 중간에 실패했을 때 이미 커밋된 내용을 롤백할 수 없다. "주문은 생성됐는데 결제는 실패"하는 어중간한 상태가 된다. 일관성이 필요한 작업은 Command로 묶어야 한다.
실제 프로젝트에서는 다음과 같이 구분했다:
Command (동기, 트랜잭션 필수)
- 주문 생성 시 쿠폰 사용 (BEFORE_COMMIT)
- 결제 성공 시 주문 완료 + 재고 차감 (BEFORE_COMMIT)
Event (비동기, 실패 허용)
- 주문 완료 시 데이터 플랫폼 전송 (AFTER_COMMIT)
- 좋아요 시 집계 업데이트 (AFTER_COMMIT)
3. @TransactionalEventListener: BEFORE_COMMIT vs AFTER_COMMIT
Spring의 @TransactionalEventListener는 트랜잭션 생명주기의 특정 시점에 이벤트를 처리할 수 있게 해준다. 하지만 BEFORE_COMMIT과 AFTER_COMMIT 중 무엇을 선택해야 할까?
3.1 트랜잭션 흐름 이해하기
@TransactionalEventListener는 이벤트 발행 시점이 아니라 커밋 시점에 리스너가 실행된다는 것이다.
※ @EventListener는 이벤트 발행 시점에 리스너가 실행된다.
[트랜잭션 시작]
↓
publishEvent() 호출 → 큐에 등록만 됨 (아직 실행 안 함)
↓
비즈니스 로직 실행
↓
[커밋 직전] ← BEFORE_COMMIT 리스너 실행 (트랜잭션 내부)
↓
[DB COMMIT 실행]
↓
[커밋 완료] ← AFTER_COMMIT 리스너 실행 (트랜잭션 외부)
3.2 코드
@Service
class OrderService(
private val eventPublisher: ApplicationEventPublisher,
) {
private val log = LoggerFactory.getLogger(OrderServiceTest::class.java)
@Transactional
fun order() {
log.info("========================================")
log.info("[OrderService] 트랜잭션 시작")
log.info(
"[OrderService] 트랜잭션 이름: {}",
TransactionSynchronizationManager.getCurrentTransactionName(),
)
// 트랜잭션 동기화 콜백 등록
TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun beforeCommit(readOnly: Boolean) {
log.info(">>> [TX-SYNC] beforeCommit 콜백 시작")
}
override fun beforeCompletion() {
log.info(">>> [TX-SYNC] beforeCompletion 콜백")
}
override fun afterCommit() {
log.info(">>> [TX-SYNC] afterCommit 콜백 - 커밋 완료!")
}
override fun afterCompletion(status: Int) {
log.info(">>> [TX-SYNC] afterCompletion 콜백")
}
},
)
log.info("[OrderService] 주문 저장 완료: {}", 1L)
log.info("[OrderService] 이벤트 발행")
eventPublisher.publishEvent(OrderCreatedEvent(1L))
log.info("[OrderService] 메서드 종료 - 이제 트랜잭션 커밋 시작됨")
log.info("========================================")
}
}
package com.loopers
@Component
class OrderEventListener {
private val log = LoggerFactory.getLogger(OrderServiceTest::class.java)
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun handleBeforeCommit(event: OrderCreatedEvent) {
log.info(">>> [BEFORE_COMMIT] 시작")
log.info(
">>> [BEFORE_COMMIT] 트랜잭션 활성: {}",
TransactionSynchronizationManager.isActualTransactionActive(),
)
log.info(
">>> [BEFORE_COMMIT] 트랜잭션 이름: {}",
TransactionSynchronizationManager.getCurrentTransactionName(),
)
log.info(" >> [BEFORE_COMMIT] 종료")
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleAfterCommit(event: OrderCreatedEvent) {
log.info(" >> [AFTER_COMMIT] 시작 - 커밋 이미 완료됨")
log.info(
">>> [AFTER_COMMIT] 트랜잭션 활성: {}",
TransactionSynchronizationManager.isActualTransactionActive(),
)
log.info(
">>> [AFTER_COMMIT] 트랜잭션 이름: {}",
TransactionSynchronizationManager.getCurrentTransactionName(),
)
log.info(">>> [AFTER_COMMIT] 종료")
}
}
3.3 실제 로그로 확인하기
Getting transaction for [com.loopers.OrderService.order]
[OrderService] 트랜잭션 시작
[OrderService] 트랜잭션 이름: com.loopers.OrderService.order
[OrderService] 주문 저장 완료: 1
[OrderService] 이벤트 발행
[OrderService] 메서드 종료 - 이제 트랜잭션 커밋 시작됨
Completing transaction for [com.loopers.OrderService.order]
>>> [TX-SYNC] beforeCommit 콜백 시작
>>> [BEFORE_COMMIT] 시작
>>> [BEFORE_COMMIT] 트랜잭션 활성: true
>>> [BEFORE_COMMIT] 트랜잭션 이름: com.loopers.OrderService.order
>>> [BEFORE_COMMIT] 종료
>>> [TX-SYNC] beforeCompletion 콜백
JpaTransactionManager : Initiating transaction commit
JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(907853957<open>)]
TransactionImpl : committing
>>> [TX-SYNC] afterCommit 콜백 - 커밋 완료!
[TX-SYNC] afterCompletion 콜백
>>> [AFTER_COMMIT] 시작 - 커밋 이미 완료됨
>>> [AFTER_COMMIT] 트랜잭션 활성: false
>>> [AFTER_COMMIT] 트랜잭션 이름: com.loopers.OrderService.order
>>> [AFTER_COMMIT] 종료
1. publishEvent() 호출 시점에는 리스너가 실행되지 않는다 - 이벤트만 큐에 등록됨
2. BEFORE_COMMIT은 [OrderService] 메서드 종료 이후에 실행 - 커밋 프로세스의 일부
3. 트랜잭션 활성 여부: BEFORE_COMMIT은 true, AFTER_COMMIT은 false
4. 실제 DB COMMIT은 BEFORE_COMMIT과 AFTER_COMMIT 사이에 발생
5. AFTER_COMMIT에서 예외가 발생해도 주문은 이미 커밋된 상태
3.3 선택 기준: 단 하나의 질문
이 작업이 실패하면 메인 트랜잭션도 롤백되어야 하는가?
| 답변 | 선택 | 이유 |
| 예 | BEFORE_COMMIT | 후속 작업 실패 시 전체 롤백, 트랜잭션 내에서 반드시 실행 |
| 아니오 | AFTER_COMMIT | 메인 트랜잭션은 보장, 후속은 별개, 롤백 대상 아님 |
하지만 BEFORE_COMMIT을 선택하기 전에 한 가지 더 고민해야 할 질문이 있다.
"굳이 이벤트로 분리해야 하는가?", "느슨한 결합" 때문일까?
BEFORE_COMMIT은 느슨한 결합이 아니다
BEFORE_COMMIT으로 처리하면 느슨한 결합이 될까? 다음 코드를 보자:
@Transactional
fun createOrder(request: CreateOrderRequest) {
val order = orderRepository.save(Order.create(request))
eventPublisher.publishEvent(OrderCreatedEvent(order))
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun handleOrderCreatedForCoupon(event: OrderCreatedEvent) {
couponService.applyCoupon(event.userId, event.couponId)
}
표면적으로는 Facade에서 쿠폰 사용 로직이 분리됐다. 하지만 본질적으로는 여전히 같은 트랜잭션 안에 있다.
문제점:
1. 논리적 결합은 줄었지만, 물리적 결합은 끊어내지 못했다: 여전히 하나의 트랜잭션이다. 쿠폰 사용이 실패하면 주문 생성도 롤백된다.
2. 코드 추적이 더 어려워진다: Facade의 트랜잭션 동작을 이해하려면 BEFORE_COMMIT 리스너까지 찾아가서 읽어야 한다. 직접 호출보다 가독성이 떨어진다.
// ✅ 추적하기 쉬움 - 한눈에 흐름 파악
@Transactional
fun createOrder(request: CreateOrderRequest) {
val order = orderRepository.save(Order.create(request))
couponService.applyCoupon(order.userId, order.couponId) // 여기서 뭐하는지 바로 보임
}
// ❌ 추적하기 어려움 - 리스너 찾아가야 함
@Transactional
fun createOrder(request: CreateOrderRequest) {
val order = orderRepository.save(Order.create(request))
eventPublisher.publishEvent(OrderCreatedEvent(order)) // 뭐가 실행될지 모름
}
// 리스너를 찾아가서 읽어야 비로소 쿠폰 사용을 안다는 걸 알 수 있음
BEFORE_COMMIT의 유일한 장점: 확장성
그렇다면 BEFORE_COMMIT은 언제 유용할까? 단 하나의 명확한 장점이 있다.
"트랜잭션이 달린 Facade 코드를 거의 수정하지 않고, 독립적으로 로직을 추가할 수 있다"
```kotlin
// 초기 코드: 주문만 생성
@Transactional
fun createOrder(request: CreateOrderRequest) {
val order = orderRepository.save(Order.create(request))
eventPublisher.publishEvent(OrderCreatedEvent(order))
}
// 나중에 요구사항 추가: 쿠폰 사용 기능 추가
// Facade는 수정하지 않고, 리스너만 추가
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun handleOrderCreatedForCoupon(event: OrderCreatedEvent) {
event.couponId?.let {
couponService.applyCoupon(event.userId, it)
}
}
원래라면 Facade를 직접 수정해야 했지만, 이벤트 구조라면 리스너만 추가하면 된다. Open-Closed Principle(개방-폐쇄 원칙)에 부합한다.
하지만 실무에서는 이런 확장이 자주 일어나지 않는다. 대부분의 경우 핵심 로직은 처음부터 정의되어 있고, 나중에 트랜잭션 내부 로직을 추가하는 경우는 드물다.
AFTER_COMMIT의 진짜 장점: 비동기 처리
이벤트 처리의 진짜 장점은 AFTER_COMMIT + @Async 조합이다.
@Transactional
fun createOrder(request: CreateOrderRequest) {
val order = orderRepository.save(Order.create(request))
eventPublisher.publishEvent(OrderCreatedEvent(order))
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleOrderCreatedForDataPlatform(event: OrderCreatedEvent) {
dataPlatformPublisher.send(event) // 비동기로 외부 시스템 호출
}
장점:
1. 트랜잭션 길이 최소화: 외부 I/O를 기다리지 않아 DB 락 시간 단축
2. 장애 격리: 외부 시스템 장애가 주문 생성에 영향 없음
3. 진정한 느슨한 결합: 트랜잭션도 분리, 실행도 분리
Outbox 패턴과의 조합
메시지 유실을 방지하면서 비동기 처리를 하려면 Outbox 패턴을 사용한다:
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun saveToOutbox(event: OrderCreatedEvent) {
// 1. Outbox 테이블에 이벤트 저장 (같은 트랜잭션)
outboxRepository.save(OutboxEvent(event))
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun publishToKafka(event: OrderCreatedEvent) {
// 2. Kafka로 발행 (비동기)
kafkaTemplate.send("order-events", event)
outboxRepository.markAsPublished(event.id)
}
BEFORE_COMMIT에서 Outbox 테이블에 기록하고, AFTER_COMMIT에서 Kafka로 발행한다. 발행 실패 시 배치 작업으로 Outbox 테이블을 읽어 재전송할 수 있다.
결론: 언제 무엇을 사용할까?
그냥 Facade에 둬라:
- 핵심 비즈니스 로직
- 자주 변경되지 않는 로직
- 직관적인 흐름 파악이 중요한 로직
BEFORE_COMMIT으로 분리해라:
- 나중에 추가될 가능성이 높은 로직
- Outbox 패턴 구현
- 여러 팀이 독립적으로 개발하는 로직
AFTER_COMMIT으로 분리해라:
- 외부 시스템 호출
- 실패해도 메인 플로우에 영향 없는 로직
- 비동기 처리가 필요한 로직 (알림, 로그, 통계)
BEFORE_COMMIT은 느슨한 결합이 아니라 확장 가능한 설계를 위한 것이고, AFTER_COMMIT이야말로 진정한 느슨한 결합과 비동기 처리의 이점을 제공한다.
4. 실전 사례: 주문 생성 흐름
실제 프로젝트의 주문 생성 흐름을 통해 BEFORE_COMMIT과 AFTER_COMMIT을 어떻게 선택했는지 살펴보자.
4.1 주문 생성 시나리오
1. 주문 저장
2. 쿠폰 사용 ← 실패하면 주문도 취소되어야 함
3. 주문 생성 이벤트 발행
4. 데이터 플랫폼 전송 ← 실패해도 주문은 유지
쿠폰 사용: BEFORE_COMMIT
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun handleOrderCreatedForCoupon(event: OrderEvent.OrderCreated) {
event.couponId?.let { couponId ->
try {
couponService.applyCoupon(event.userId, event.couponId)
} catch (e: ObjectOptimisticLockingFailureException) {
// 쿠폰 중복 사용 감지 → 주문도 롤백됨
throw CoreException(ErrorType.COUPON_ALREADY_USED)
}
}
}
쿠폰 사용은 주문의 핵심 로직이다. 쿠폰이 중복 사용되거나 사용 불가능하면 주문 자체가 실패해야 한다. 따라서 BEFORE_COMMIT을 사용해 같은 트랜잭션 내에서 처리한다.
데이터 플랫폼 전송: AFTER_COMMIT
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handleOrderCreatedForUserActivity(event: OrderEvent.OrderCreated) {
try {
dataPlatformPublisher.send(event)
} catch (e: Exception) {
log.error("데이터 플랫폼 전송 실패 (재시도 필요)", e)
// 주문은 이미 성공 상태
}
}
데이터 플랫폼 전송은 부가 기능이다. 실패해도 주문은 성공 상태로 유지되어야 한다. 외부 시스템 장애가 주문 생성을 막아서는 안 된다. AFTER_COMMIT + @Async로 비동기 처리한다.
4.2 PG 결제 요청: 왜 AFTER_COMMIT인가?
PG 결제는 고민이 많은 부분이다. "결제가 핵심 로직인데 AFTER_COMMIT으로 처리해도 되나?"
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handlePaymentRequest(event: PaymentEvent.PaymentRequest) {
try {
val transactionKey = pgService.requestPayment(...)
transactionTemplate.execute {
paymentService.updateTransactionKey(event.paymentId, transactionKey)
}
} catch (e: Exception) {
log.error("PG 요청 실패 - 보상 트랜잭션 필요", e)
}
}
AFTER_COMMIT이 적합한 이유:
1. PG는 비동기 결제 모델이다: 요청 성공 ≠ 결제 성공. 실제 결제 결과는 콜백(Webhook)으로 받는다. PG 요청이 실패해도 주문은 "결제 대기" 상태로 저장할 수 있다.
2. 외부 시스템 장애 격리: PG가 타임아웃되거나 장애가 발생해도 주문 데이터는 보존된다. 나중에 재시도 가능하다.
3. 트랜잭션 길이 최소화: BEFORE_COMMIT을 사용하면 PG 응답을 기다리는 동안 DB 락이 유지된다. 성능 저하의 원인이 된다.
BEFORE_COMMIT이 필요한 경우는?
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun validateSomething(event: OrderCreatedEvent) {
// 메인 트랜잭션과 반드시 함께 성공/실패해야 하는 검증
// 예: 외부 재고 시스템과 동기화 검증 (드문 케이스)
// 예: Outbox 테이블 기록
}
실패 시 주문 자체가 무효가 되어야 하는 필수 검증이나, 동기적으로 완료되어야 하는 짧은 작업만 BEFORE_COMMIT으로 처리한다.
5. 이벤트 설계의 실전 노하우
5.1 이벤트는 어느 계층에 위치하는가?
실제 프로젝트 구조:
domain/
├─ payment/
│ └─ PaymentEvent.kt (domain 계층)
interfaces/
└─ event/
└─ PaymentEventListener.kt (interface 계층)
- 이벤트 DTO는 domain 계층: 도메인 개념을 표현하므로 domain 계층에 위치한다.
- 이벤트 핸들러는 interface 계층: 인프라 구현체이므로 interface(또는 infrastructure) 계층에 위치한다.
중요한 원칙: 이벤트 핸들러는 컨트롤러처럼 얇아야 한다. 로직을 직접 작성하지 말고 Facade나 Domain Service를 호출한다.
// ✅ 좋은 예: 핸들러는 위임만
@TransactionalEventListener
fun handlePaymentSucceeded(event: PaymentEvent.PaymentSucceeded) {
orderService.complete(event.orderId)
productService.deductAllStock(orderDetails)
}
// ❌ 나쁜 예: 핸들러에 비즈니스 로직
@TransactionalEventListener
fun handlePaymentSucceeded(event: PaymentEvent.PaymentSucceeded) {
val order = orderRepository.findById(...)
order.status = OrderStatus.COMPLETED
order.completedAt = LocalDateTime.now()
orderRepository.save(order)
// 비즈니스 로직이 흩어짐
}
5.2 내부 이벤트 vs 외부 이벤트
- 내부 이벤트: 같은 애플리케이션 내에서 발행/구독 → ApplicationEventPublisher 사용
- 외부 이벤트: 다른 시스템으로 발행 → Kafka, RabbitMQ 등의 메시지 브로커 사용
구현체는 infrastructure 계층에서 인터페이스를 구현한다.
interface DataPlatformPublisher {
fun send(event: Any)
}
// Infrastructure 계층
class KafkaDataPlatformPublisher : DataPlatformPublisher {
override fun send(event: Any) {
kafkaTemplate.send("topic", event)
}
}
5.3 이벤트는 언제 사용하는가?
@Async + AFTER_COMMIT으로 적용할 수 있는 경우:
- 좋아요 수 집계
- 검색 로깅
- 사용자 활동 추적
- 알림 발송
이런 경우들은 실패해도 괜찮은 로직이다. Eventual Consistency가 허용된다.
그 외에는 신중하게:
이벤트를 많이 쓰면 코드 읽기가 어려워진다. "이벤트를 쓰면 결합도가 낮아진다"고 하지만, 실제로는 이벤트 리스너 코드를 직접 찾아가 봐야 한다. Facade에서 직접 호출해도 문제없는 경우가 많다.
Outbox 패턴과의 궁합:
- BEFORE_COMMIT에 Outbox 테이블 저장
- AFTER_COMMIT에 외부 이벤트 발행
- 외부 시스템 장애 시에도 이벤트 유실 방지
6. 자주 묻는 질문
Q1. @Async를 쓰면 이벤트가 유실되지 않나요?
99%는 유실되지 않는다. Spring의 @Async는 기본적으로 ThreadPoolTaskExecutor를 사용하며, 애플리케이션이 정상적으로 종료될 때 작업을 완료하려고 시도한다.
다만, 서버가 갑자기 종료되거나 Out Of Memory가 발생하면 유실 가능하다.
Q2. 이벤트 발행 순서는 보장되나요?
같은 트랜잭션 내에서 발행한 이벤트의 순서는 보장된다. 하지만 여러 트랜잭션에 걸친 순서는 보장되지 않는다.
순서가 중요하다면:
- 이벤트에 순서 정보(sequence number) 포함
- 메시지 브로커의 파티션 키 활용
- 단일 트랜잭션 내에서 처리
Q3. Axon Framework는 무엇인가요?
Event Sourcing과 CQRS 패턴을 구현하기 위한 Java/Kotlin 프레임워크다. 이벤트를 저장소에 영구 저장하고, 이벤트 스트림을 재생해 상태를 복원할 수 있다.
하지만 대부분의 경우 Axon Framework까지는 필요 없다. Spring의 @TransactionalEventListener와 메시지 브로커만으로도 충분히 EDA를 구현할 수 있다. Axon은 도메인이 복잡하고, Event Sourcing이 명확히 필요한 경우에만 고려하자.
7. 결론
Command와 Event는 메시지의 두 얼굴이다. Command는 명확한 제어와 일관성을 제공하고, Event는 확장성과 느슨한 결합을 제공한다.
BEFORE_COMMIT과 AFTER_COMMIT의 선택은 "이 작업이 실패하면 메인 트랜잭션도 롤백되어야 하는가?"라는 질문에서 시작한다. 핵심 로직은 BEFORE_COMMIT으로, 부가 기능은 AFTER_COMMIT으로 처리한다.
이벤트는 강력하지만 과용하면 독이 된다. @Async + AFTER_COMMIT으로 적용할 수 있는 경우(실패해도 괜찮은 로직)에만 사용하고, 일관성이 중요한 경우는 Command로 처리하자.
루프백 3기 오픈
https://www.loopers.im/education
교육 과정 | Loop:Pak
Loop:Pak 부트캠프 교육 과정 상세 정보. NextNode Backend Edition을 포함한 전문 개발자 양성 커리큘럼과 현업 멘토링 프로그램을 만나보세요.
www.loopers.im
루퍼스 3기가 오픈되었습니다.
추천인 코드를 입력하면 할인가로 수강할 수 있습니다. 다만 결코 작은 금액은 아니기에, 신청 전 적지 않은 고민을 했는데요. AI 기술의 빠른 발전과 함께 연차가 쌓이면서, 스스로의 실력에 대한 의구심이 들던 시기였습니다.
며칠간 고민한 끝에 이번 기회를 마지막이라 생각하고 수강을 결정했습니다. 지금 돌이켜보면, 후회 없는 선택이었다고 자신 있게 말할 수 있습니다.
만약 저와 비슷한 고민을 하고 있거나, 루퍼스 과정에 대해 궁금한 점이 있다면 언제든지 편하게 연락 주셔도 좋습니다.
LinkedIn 또는 jikimee64@gmail.com으로 언제든 문의 부탁 드립니다. 감사합니다.
추천인 코드
TZFI0
'외부활동 > 루퍼스 2기' 카테고리의 다른 글
| [루퍼스/루프백 2기] Kafka At-Least-Once 전송과 Consumer 멱등성 구현기 (0) | 2025.12.16 |
|---|---|
| [Loop:PAK] 7주차 WIL (0) | 2025.12.06 |
| [Loop:PAK] 6주차 WIL (0) | 2025.12.04 |
| [루퍼스/루프백 2기] 실패를 실패로 끝내지 않는 법: Circuit Breaker의 철학 (0) | 2025.12.04 |
| [Loop:PAK] 5주차 WIL (0) | 2025.11.25 |