이전 글
https://codstudy.tistory.com/134
(위의 글과 이어지는 글이다.)
테이블 설계시 google meet 링크를 저장하는 테이블이 있었고, (id가 pk)
예약 테이블에 google meet 링크 id를 저장하게 하여 외래키 설정을 해뒀는데, 이 때문에 구글 미트 링크를 먼저 생성 후, 예약 정보를 저장해야 했다. 이 경우, 예약 flow에서 오류가 생겨 transaction이 rollback된다면,,,?
생성된 미트 링크는 어떻게 해야할까...
전에 구현할 때 바빠서 이 경우를 놓친 것 같다. 외부 API를 연동할 때는 성급하게 하면 안 됐는데 역량 부족의 문제였다.
문제의 flow를 순서대로 정리해보면 다음과 같다.
따라서 외부 시스템과 연결할 때에는 반드시 보상 트랜잭션(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)를 제공해야 가능하다.
DB에 Reservation 먼저 저장하고, 트랜잭션 커밋된 이후에 Google Meet을 생성하는 방식
// 예약 저장 성공 후 이벤트 발행
applicationEventPublisher.publishEvent(new ReservationCreatedEvent(reservation));
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void createGoogleMeet(ReservationCreatedEvent event) {
googleMeetService.createMeet(...);
}
장점
단점
지금 구조처럼 Reservation이 google_meet_id를 FK로 갖고 있으면
→ "항상 먼저 생성된 meet가 있어야 reservation이 생성됨"
FK를 제거해서 느슨하게 만들면, 예약 생성 후 구글 미트 정보는 'optional'로 저장할 수 있다.
위의 경우에서는, 우선 외래키 방향을 전환하고 예약 먼저 저장 → 구글 미트 생성이 되어야 할 것 같다.
외래키 방향을 전환하는 것은 어렵지 않지만, 예약을 먼저 저장하고 구글 미트 생성하는 부분은 트랜잭션 완료 후에 수행되도록 따로 빼는 게 좋을 것 같은데 이 부분을 어떻게 구현하면 좋을까?
위의 2번에서 설명한 대로 예약 생성 이벤트가 정상적으로 발생하면, 구글 미트 링크 생성 로직이 돌아갈 수 있도록 구현할 수 있다.
@PostMapping("/createReservation")
public ResponseEntity<?> createReservation(...) {
reservationFacade.createReservation(...);
googleMeetService.createMeet(...);
return ResponseEntity.ok(...);
}
위와 같이 구글 미트를 생성하는 코드를 컨트롤러로 뺄 수 있다.
스프링에서는 transaction 처리를 Bean으로 관리하기 때문에, reservationFacade.createReservation()에서 예외가 생겨 트랜잭션 롤백이 되면 createMeet() 메서드는 실행되지 않는다.
지금 내 상황에서는 어떤 게 적절할까?
질문을 던져보자. "과연 구글미트 링크 생성이 '예약 프로세스'에 포함될까?"
아니라고 생각한다.
구글 미트 링크 생성은 예약의 핵심이 아니라, 예약 이후 발생하는 후속 작업이 되어야 한다.
따라서 이벤트 리스너로 분리하는 게 적절하다.
예약은 예약 자체로 단일한 도메인 행동이기 때문에, 도메인 로직과 외부 연동은 분리되어야 한다.
이렇게 보니 외부 시스템과 연동하는 작업은 트랜잭션 안에 끼워넣는 게 아니라, 트랜잭션 후에 두는 게 가장 좋은 선택인 것 같다.