아키텍처 결정

알림톡, 트랜잭션이 끝난 뒤에 보내야 한다

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 테이블에 기록하고, 별도 프로세스가 읽어서 발송한다. 서버 크래시에도 유실이 없다.

결론 서비스 초기 단계에서 Outbox 패턴은 오버엔지니어링이다. 현재 구조는 구현 복잡도 없이 데이터 정합성과 응답 지연 최소화를 동시에 달성한다.
← 목록으로 App Runner 삽질기 →