Spring 개발일지

[팀프로젝트] MSA 기반 티켓팅 프로그램 - MSA 서비스 간 통신 정리

김둘리 2026. 5. 18. 09:03

Booking 서비스 담당자와 API 스펙을 맞추면서 MSA에서 서비스 간 통신이 어떻게 이루어지는지 정리했다.


1. 전체 결제 흐름

사용자 예매 확정
→ Booking 서비스 → POST /internal/v1/payments (FeignClient)
→ Payment 서비스가 orderId 생성 후 반환
→ Booking이 orderId 보관
→ 사용자가 토스 결제창에서 결제
→ Payment 서비스 결제 승인
→ payment.completed 이벤트 발행
→ Booking 서비스가 이벤트 수신 후 예매 확정 처리

2. FeignClient란?

Booking 서비스가 Payment 서비스를 HTTP로 직접 호출하는 클라이언트다.

// Booking 서비스에서 구현
@FeignClient(name = "payment-service")
public interface PaymentClient {

    @PostMapping("/internal/v1/payments")
    PaymentResponse createPayment(@RequestBody CreatePaymentRequest request);
}

name = "payment-service"는 Eureka에 등록된 서비스 이름이다. 직접 IP를 몰라도 Eureka가 자동으로 주소를 찾아준다.


3. orderId는 누가 만들어?

Payment 서비스가 만든다!

// PaymentCommandService.createPayment()
String orderId = UUID.randomUUID().toString().replace("-", ""); // Payment가 생성
Booking → POST /internal/v1/payments (bookingId, userId, amount 전달)
→ Payment가 orderId 생성
→ orderId 포함한 응답 반환
→ Booking이 orderId 보관

4. Booking 서비스에 공유한 API 스펙

결제 생성

POST http://payment-service/internal/v1/payments

Body:
{
    "bookingId": "예매 UUID",
    "userId": "로그인한 사용자 UUID",
    "finalAmount": 결제금액
}

Response:
{
    "paymentId": "결제 UUID",
    "orderId": "토스 orderId",
    "amount": 50000,
    "status": "PENDING"
}

환불 (예매 취소 시)

POST http://payment-service/api/v1/payments/{paymentId}/refund

Headers:
X-User-Id: 사용자 UUID

Body:
{
    "cancelReason": "취소 사유"
}

5. payment.completed 이벤트

결제 승인 완료 시 아웃박스 패턴으로 발행된다.

{
    "paymentId": "결제 UUID",
    "bookingId": "예매 UUID",
    "userId": "사용자 UUID",
    "amount": 50000,
    "status": "SUCCESS"
}

Booking 서비스가 이 이벤트를 수신해서 예매 확정 처리를 한다.


6. 왜 userId를 같이 넘겨줘야 해?

처음에 bookingId만 넘기면 Payment가 Booking에서 userId를 조회하면 되지 않냐는 생각이 들었다.

근데 그러면 순환 참조가 생긴다.

Booking → Payment (결제 생성)
Payment → Booking (userId 조회) ← 순환 참조!

MSA에서 서비스 간 순환 참조는 장애 전파 위험이 있다. Booking이 죽으면 Payment도 userId를 못 가져와서 결제 생성이 실패한다.

그래서 Booking이 userId를 직접 넘겨주는 방식을 선택했다.


7. 예매 취소 = 환불

처음에 PATCH /internal/v1/payments/{paymentId}/status 같은 상태 강제 변경 API가 필요할 것 같았다.

근데 예매 취소는 환불이 같이 일어나는 거라 기존 환불 API로 처리 가능하다.

사용자 예매 취소
→ Booking이 POST /api/v1/payments/{paymentId}/refund 호출
→ 토스 취소 API 호출
→ REFUNDED 상태로 변경

별도 상태 변경 API는 필요 없었다.


마무리

MSA에서 서비스 간 통신 설계 시 고려할 것들을 정리하면 이렇다.

  • FeignClient: Eureka 기반 서비스 디스커버리로 IP 없이 통신
  • 순환 참조 방지: 필요한 데이터는 요청 시 같이 넘기기
  • 이벤트 기반: 결제 완료 후 Booking에 알릴 때는 Kafka 이벤트 사용
  • 책임 분리: 결제 생성은 Payment, 예매 확정은 Booking이 각자 처리