외부 시스템 연동 시 트랜잭션 처리
이전 글
https://codstudy.tistory.com/134
(위의 글과 이어지는 글이다.)
테이블 설계시 google meet 링크를 저장하는 테이블이 있었고, (id가 pk)
예약 테이블에 google meet 링크 id를 저장하게 하여 외래키 설정을 해뒀는데, 이 때문에 구글 미트 링크를 먼저 생성 후, 예약 정보를 저장해야 했다. 이 경우, 예약 flow에서 오류가 생겨 transaction이 rollback된다면,,,?
생성된 미트 링크는 어떻게 해야할까...
전에 구현할 때 바빠서 이 경우를 놓친 것 같다. 외부 API를 연동할 때는 성급하게 하면 안 됐는데 역량 부족의 문제였다.
문제의 flow를 순서대로 정리해보면 다음과 같다.
- Google Meet 링크를 먼저 생성(외부 API 호출)
- Reservation 테이블에 google_meet_id를 외래키로 저장.
- 중간에 문제 생기면? → Reservation 저장 실패 & 롤백.
- 근데 생성된 Google Meet는 그대로 남아있음.
→ 데이터 불일치 발생.(DB에는 예약이 없는데, 실제로는 구글 미팅 링크가 생성된 상태.)
따라서 외부 시스템과 연결할 때에는 반드시 보상 트랜잭션(Compensating Transaction) 또는 비동기 처리 전략을 고려해야 한다.
해결 전략
1. 보상 트랜잭션(Compensating Transaction)
외부 시스템에 먼저 요청하고, 실패 시 "되돌리는 API"를 호출하는 방식
try {
GoogleMeet meet = googleMeetService.createMeet(...);
// 로컬 DB 저장 시작
Reservation reservation = reservationService.create(...); // 이 중간에 실패할 수도 있음
reservation.setGoogleMeet(meet);
reservationRepository.save(reservation);
} catch (Exception e) {
// 실패 시 외부 시스템에 보상 요청
googleMeetService.deleteMeet(meet.getMeetId());
throw e;
}
하지만 위와 같은 방법은 복잡하며, 외부 시스템이 보상 API(삭제 API)를 제공해야 가능하다.
2. 트랜잭션 성공 이후에 외부 연동 처리하기 (이벤트 기반 처리)
DB에 Reservation 먼저 저장하고, 트랜잭션 커밋된 이후에 Google Meet을 생성하는 방식
- Reservation 저장
- @TransactionalEventListener(phase = AFTER_COMMIT)로 Google Meet 생성
// 예약 저장 성공 후 이벤트 발행
applicationEventPublisher.publishEvent(new ReservationCreatedEvent(reservation));
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void createGoogleMeet(ReservationCreatedEvent event) {
googleMeetService.createMeet(...);
}
장점
- 트랜잭션 깨져도 외부 API는 호출되지 않음
- 실패하면 별도 에러 처리 로직 분리 가능 (ex. Retry, Admin 알림 등)
단점
- Google Meet 생성 실패 시 사용자에게 실시간 알림 불가
- 추후 예약 정보와 Meet을 연동하는 방식 필요
3. 예약과 Google Meet 사이의 FK를 없애고, 연관관계를 느슨하게 만든다
지금 구조처럼 Reservation이 google_meet_id를 FK로 갖고 있으면
→ "항상 먼저 생성된 meet가 있어야 reservation이 생성됨"
FK를 제거해서 느슨하게 만들면, 예약 생성 후 구글 미트 정보는 'optional'로 저장할 수 있다.
- 즉, 생성된 이후에 update로 붙이거나
- reservation_id를 GoogleMeet에서 FK로 설정해서 반대로 관계 맺기
위의 경우에서는, 우선 외래키 방향을 전환하고 예약 먼저 저장 → 구글 미트 생성이 되어야 할 것 같다.
외래키 방향을 전환하는 것은 어렵지 않지만, 예약을 먼저 저장하고 구글 미트 생성하는 부분은 트랜잭션 완료 후에 수행되도록 따로 빼는 게 좋을 것 같은데 이 부분을 어떻게 구현하면 좋을까?
1. 이벤트 기반 처리
위의 2번에서 설명한 대로 예약 생성 이벤트가 정상적으로 발생하면, 구글 미트 링크 생성 로직이 돌아갈 수 있도록 구현할 수 있다.
2. 컨트롤러에서 명시적으로 호출
@PostMapping("/createReservation")
public ResponseEntity<?> createReservation(...) {
reservationFacade.createReservation(...);
googleMeetService.createMeet(...);
return ResponseEntity.ok(...);
}
위와 같이 구글 미트를 생성하는 코드를 컨트롤러로 뺄 수 있다.
스프링에서는 transaction 처리를 Bean으로 관리하기 때문에, reservationFacade.createReservation()에서 예외가 생겨 트랜잭션 롤백이 되면 createMeet() 메서드는 실행되지 않는다.
지금 내 상황에서는 어떤 게 적절할까?
질문을 던져보자. "과연 구글미트 링크 생성이 '예약 프로세스'에 포함될까?"
아니라고 생각한다.
구글 미트 링크 생성은 예약의 핵심이 아니라, 예약 이후 발생하는 후속 작업이 되어야 한다.
따라서 이벤트 리스너로 분리하는 게 적절하다.
예약은 예약 자체로 단일한 도메인 행동이기 때문에, 도메인 로직과 외부 연동은 분리되어야 한다.
이렇게 보니 외부 시스템과 연동하는 작업은 트랜잭션 안에 끼워넣는 게 아니라, 트랜잭션 후에 두는 게 가장 좋은 선택인 것 같다.