아키텍처 결정
알림톡, 트랜잭션이 끝난 뒤에 보내야 한다
Commission이 생성되면 배정된 선생님께 알림톡을 보내야 한다. 처음엔 단순하게 생각했다. 저장하고, 바로 쏘면 되지 않나? 근데 이 순서가 생각보다 복잡한 문제를 만든다.
문제: 저장 실패해도 알림이 나간다
UseCase 안에서 트랜잭션이 아직 커밋되지 않은 상태에서 NCP API를 호출하면 어떻게 될까?
// ❌ 이렇게 하면
val commission = commissionAdaptor.save(...)
alimtalkSender.sendCommissionAssigned(...) // NCP 호출
// → 이후 다른 DB 작업이 실패해서 rollback
// → 선생님은 알림을 받았지만 DB엔 의뢰가 없다
트랜잭션이 롤백되면 DB 변경은 취소되지만, 이미 나간 HTTP 요청은 취소할 수 없다. 유령 알림이 생긴다.
해결: 커밋 후에 쏜다
Spring의 @TransactionalEventListener(AFTER_COMMIT)은 트랜잭션이 성공적으로 커밋된 이후에만 이벤트 핸들러를 실행한다. UseCase에서는 이벤트만 발행하고, 실제 알림 발송은 분리된 핸들러가 담당한다.
// UseCase — 이벤트 발행만
eventPublisher.publishEvent(
CommissionAssignedEvent(
teacherUserId = commission.teacherUserId, ...
)
)
// EventListener — 커밋 후 별도 스레드에서 실행
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handle(event: CommissionAssignedEvent) {
val teacher = userAdaptor.findById(event.teacherUserId)
val phoneNumber = teacher.phoneNumber ?: return
commissionNotificationSender.sendCommissionAssigned(...)
}
왜 @Async도 붙였나 — 장애 도메인 격리
알림은 부가 기능이다. 의뢰 배정 API의 본질은 Commission을 DB에 저장하는 것이고, 알림톡은 그것을 선생님에게 알려주는 편의 수단이다. 이 둘은 서로 독립적이어야 한다.
API 성공 ≠ 알림 성공
NCP 서버가 일시 다운되거나, 응답이 5초씩 걸리거나, 전화번호가 없어서 알림을 건너뛰어도 — Commission 저장 자체는 성공이다. 클라이언트에게 500을 돌려줄 이유가 없다.
@Async가 없으면 NCP API 호출이 끝날 때까지 HTTP 응답이 블로킹된다. 더 심각한 건 NCP에서 예외가 발생하면 그게 그대로 클라이언트에게 전파된다는 점이다.
// ❌ @Async 없이
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handle(event: CommissionAssignedEvent) {
commissionNotificationSender.sendCommissionAssigned(...)
// NCP 예외 → 커밋은 됐는데 클라이언트에 500 전달
// NCP 응답 300ms → HTTP 응답도 300ms 지연
}
// ✅ @Async 붙이면
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun handle(event: CommissionAssignedEvent) {
commissionNotificationSender.sendCommissionAssigned(...)
// 별도 스레드에서 실행 → 예외는 로그에만 기록
// HTTP 응답은 즉시 반환
}
두 어노테이션이 하는 일
@TransactionalEventListener(AFTER_COMMIT) — 커밋 성공 후에만 실행 (유령 알림 방지)@Async — 별도 스레드에서 실행, 알림 장애가 API 응답으로 전파되지 않음
트레이드오프는 분명히 있다
| 상황 | 결과 |
|---|---|
| NCP 일시 장애 | 알림 유실. 재시도 없음 |
| 커밋 직후 서버 크래시 | 알림 유실. 극히 드문 케이스 |
| async 스레드 예외 | 로그에만 기록. caller에 전파 안 됨 |
이걸 감수하는 이유
알림톡은 보조 기능이다. 발송에 실패해도 사용자가 앱에서 상태를 직접 확인할 수 있고, 비즈니스 데이터 무결성에는 영향이 없다.
언제 이 방식이 부족해지는가
지금 구조로 충분하지 않은 시점이 오면 Outbox 패턴을 도입한다. 이벤트를 DB 테이블에 기록하고, 별도 프로세스가 읽어서 발송한다. 서버 크래시에도 유실이 없다.
- 알림 발송 보장이 SLA에 포함될 때
- 발송 실패 이력을 DB에 남기고 재시도해야 할 때
- 마이크로서비스로 분리가 필요해질 때
결론
서비스 초기 단계에서 Outbox 패턴은 오버엔지니어링이다. 현재 구조는 구현 복잡도 없이 데이터 정합성과 응답 지연 최소화를 동시에 달성한다.