Spring 개발일지

[팀프로젝트] MSA 기반 티켓팅 프로그램 - booking.payment.refund 이벤트 수신 후 환불 처리

김둘리 2026. 5. 22. 08:48

 

Booking 서비스에서 환불 이벤트를 발행하면 Payment 서비스가 수신해서 토스 취소 요청 및 REFUNDED 상태로 변경한다.


1. 배경

환불이 필요한 케이스는 두 가지다.

케이스 1 - 사용자가 직접 예매 취소
케이스 2 - 좌석 선점 시간 만료

두 케이스 모두 Booking 서비스가 booking.payment.refund 토픽으로 이벤트를 발행한다. Payment 서비스는 이걸 수신해서 환불 처리한다.


2. Booking 담당자와 Payload 협의

{
    "paymentId": "결제 UUID",
    "bookingId": "예매 UUID",
    "userId": "사용자 UUID",
    "reason": "환불 사유"
}

paymentId를 포함해서 보내기로 했다. Booking 서비스에서 결제 생성 시 응답받은 paymentId를 저장하고 있기 때문에 가능하다.


3. BookingRefundPayload 추가

public record BookingRefundPayload(
        UUID paymentId,
        UUID bookingId,
        UUID userId,
        String reason
) {
}

4. PaymentKafkaConsumer 구현

@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentKafkaConsumer {

    private final PaymentCommandService paymentCommandService;
    private final ObjectMapper objectMapper;

    @KafkaListener(topics = "booking.payment.refund", groupId = "payment-service")
    public void handleBookingRefund(ConsumerRecord<String, String> record) {
        try {
            BookingRefundPayload payload = objectMapper.readValue(
                    record.value(), BookingRefundPayload.class);

            log.info("booking.payment.refund 수신 - paymentId: {}, reason: {}",
                    payload.paymentId(), payload.reason());

            paymentCommandService.refundPayment(
                    new RefundPaymentCommand(
                            payload.paymentId(),
                            payload.userId(),
                            payload.reason()
                    )
            );
        } catch (Exception e) {
            log.error("booking.payment.refund 처리 실패 - {}", e.getMessage(), e);
            throw new RuntimeException(e); // 예외 재전파 → Spring Kafka 재시도
        }
    }
}

5. 예외 재전파가 중요한 이유

처음에는 예외를 catch해서 로그만 찍었다.

} catch (Exception e) {
    log.error("처리 실패 - {}", e.getMessage(), e); // 그냥 로그만
}

이렇게 하면 메서드가 정상 종료되어 Kafka가 offset을 커밋해버린다. 메시지가 유실되고 재시도가 안 된다.

코드래빗 리뷰:

예외를 재전파해야 Spring Kafka의 재시도 메커니즘이 작동합니다.

예외를 재전파하면 Spring Kafka의 DefaultErrorHandler가 작동해서 최대 10회까지 자동 재시도한다.

throw new RuntimeException(e); // 재전파

6. 전체 이벤트 흐름 정리

케이스 1 - 사용자 직접 예매 취소
Booking → booking.payment.refund 발행
→ Payment 수신
→ 토스 취소 요청
→ REFUNDED 상태 변경

케이스 2 - 좌석 선점 시간 만료
Booking → booking.payment.refund 발행
→ Payment 수신
→ 토스 취소 요청
→ REFUNDED 상태 변경

케이스 3 - 결제 최종 실패 (선점 시간 만료 후)
Payment → payment.failed 발행
→ Booking 수신
→ 예매 취소 + 좌석 선점 해제

환불 케이스(1, 2)는 Booking이 이벤트를 발행하고 Payment가 수신한다. 결제 실패(3)는 Payment가 이벤트를 발행하고 Booking이 수신한다. 방향이 반대다.


마무리

Kafka 컨슈머 구현에서 예외 처리가 핵심이다.

예외를 삼키면 메시지가 유실된다. 예외를 재전파해야 Spring Kafka가 재시도 메커니즘을 작동시킨다.

추후 인박스 패턴을 추가해서 중복 처리 방지도 구현할 예정이다.