콘서트 예약은 유저와 좌석이 필요하다.
유저는 "예약되지 않은 좌석" 을 예약할 수 있다.
이후, 유저는 예약한 좌석에 대해 결제할 수 있다.
또한, 결제 요청 정보를 가져가는 외부 시스템이 있다. (로그 분석 등)
결제 요청 이후, 발생하는 일은 다음과 같다.
DB에 상태 변경 요청
DB 커밋
이벤트 발행
(외부 서버) 결제 정보 전달
만약, 2(DB 커밋)
이후 외부 API 서버에 결제 정보 전달 중 실패하면 어떤 일이 일어날까?
예를 들어, 외부 시스템 요청 중 Connection Timeout
이 났다면, 해당 요청이 완료된 것일까?
직접 외부 시스템을 확인하지 않는 이상, 보내는 입장에서는 알 도리가 없다!
다시 보낸다면 중복된 요청일 수도 있다.
다시 보내지 않는다면 해당 정보가 전달이 안됐을 수도 있다.
이 문제를 어떻게 해결 할까?
외부 메시지 저장소 (카프카) 를 이용해보자
우리 서버의 역할은 카프카에 결제 정보를 전달만 하면 된다.
"카프카에 정보 넣어놨으니, 알아서 가져가!"
즉, 해당 결제 정보를 가져가는 책임을 외부 시스템에 위임할 수 있다.
그럼 모든 문제가 없어질까..?
아니다!
DB에 결제 요청 정보를 적재하는 상황을 되돌아보자.
DB에 요청을 보내는 1
의 하위 4가지 작업은 마치 1개의 작업처럼 움직인다.
그 중 하나라도 실패한다면, 2(DB Commit)
에서 실패하고, 모든 실패한 작업을 되돌린다.
이처럼 모든 작업이 성공할시 성공하고, 하나라도 작업이 실패하면 모두 실패하는 성질을 원자성이라 한다
또한 원자성을 갖는 작업을 트랜잭션
이라고 한다.
카프카에 정보를 보내는 작업은 어떤가?
DB에 요청 보내는 작업인 1
, 2
를 제외한 3
, 4
는 요청 중 하나가 실패하더라도, 모든 작업이 실패하진 않는다.
정확하게는
@Async
와@EventListener
를 그냥 사용했을 때, 모든 작업이 실패하진 않는다.
그렇다면, @TransactionalEventListener
를 이용하여 3
, 4
까지 작업을 원자적으로 만들어보자
@Transactional
public method(){
// do business logic
publishEvent();
}
@TransactioanlEventListener(phase = BEFORE_COMMIT)
public listener(){
// now I am in Transaction!
sendMessageToKafka();
}
위와 같이 리스너에 BEFORE_COMMIT
옵션을 이용한다면 카프카에 메시지를 보내는 요청까지 원자적으로 처리할 수 있다.
그러나 치명적인 문제가 생긴다.
외부 시스템에 정보 전달
을 하다가 실패하면, 결제가 실패한다.
결제는 돈을 벌어다주는 너무나도 중요한 비지니스 로직이다.
(외부 시스템에 정보 전달보다도 훨씬 더!)
그러므로, 외부 시스템에 정보 전달에 실패하여도, 메인 로직인 결제가 실패하지 않아야 한다.
그렇다면, @TransactioanlEventListener의 phase=AFTER_COMMIT
옵션을 이용하여 DB 트랜잭션에서 분리할 수 있다.
그러나, 처음 Connection Timeout
상황으로 돌아갔다.
카프카에 메시지를 적재하는 순간 서버가 종료되면, 직접 카프카를 확인해야 한다. (외부 시스템이 카프카로 변경됐을 뿐이다.)
상황을 정리해보자
외부 결제 정보 전달에 실패하더라도 결제는 실패하지 않아야 한다.
하지만 외부 결제 정보 전달이 꼭 결제와 동시에 일어나지 않아도 된다면,
외부 결제 정보 전달 성공/실패 여부를 추적하고, 실패시 다시 전송을 하는 시스템을 생각해 볼 수 있다.
이를 결과적 일관성(Eventual Consistency)이라 한다.
메인 로직이 성공한다면, 부가적인 로직이 결국엔 성공하도록 하는 시스템을 말한다.
결과적으론 결제와 외부 결제 정보 전달에 성공하는 시스템을 고안해보자.
트랜잭셔널 아웃 박스 패턴 (Transactional Outbox Pattern) 을 이용해보자.
Outbox는 메일함이다.
도메인 로직을 처리할 때 같이 원자적으로 처리되는 메일함 을 생각해보자.
이러한 원자성을 이용하기 위해 DB를 이용한다.
원자적으로 처리되는 메일함인 event_outbox
테이블을 생성한다.
또, 카프카에 메시지 전송이 됐는지 여부를 확인하기 위해 카프카의 특징 또한 이용한다.
카프카 특징
카프카는 넷플릭스와 비슷하다.넷플릭스는 다수의 이용자가 동시에 시청도 가능하고, 어디까지 봤는지도 알고 있다.
카프카도
consumer_group_id
를 이용하여, 여러 서버가 동시에 메시지를 받고, 해당 그룹이 어디까지 읽었는지도 알 수 있다.
이를 이용하여 외부 시스템만 내가 보낸 메시지를 받는게 아니라, 메시지 전송을 하는 나도 카프카로부터 메시지를 읽을 수 있다.
그렇다면 내가 보낸 메시지를 카프카로부터 받아 카프카에 보내졌는지 검증할 수 있다는 뜻이다.
즉, 방송국이 정상적으로 방송이 송출되는지 확인을 하기 위해, TV를 켜 방송 화면을 보고 "보인다!"하고 확인하는 과정이다.
event_outbox
에 카프카에 메시지를 보낼꺼야
라고 적어놓고 카프카에 메시지를 발송한다.consumer_group_id
를 외부 시스템과는 다르게 지정하여, event_outbox
에 카프카에 메시지가 보내졌다.
라고 수정한다.event_outbox
테이블을 순회하며, 보내지지 않은 메시지를 주기적으로 확인하고, 다시 전송한다.실제로, 트랜잭션 아웃박스 패턴을 이용하여 얻게되는 효과를 정리해보자
1 트랜잭션 수행 성공 여부 | 2 카프카 메시지 발송 성공 여부 | 구분 | 설명 |
---|---|---|---|
O | O | 성공 | 도메인 로직 성공적으로 수행, 카프카도 메시지 정상 발송/수신 한 성공 케이스 |
X | O | 일어날 수 없는 케이스 | 트랜잭션이 실패한다면, 트랜잭션 이벤트 리스너의phase=AFTER_COMMIT 조건에 의해 메시지 발송 자체가 불가능 |
O | X | 관리 가능한 실패 | 카프카 메시지 발송 시도 중 모종의 이유로 실패한 경우, 4 의 배치를 이용하여 메시지 재전송 |
X | X | 실패 | 비지니스 로직도, 메시지 전송도 아예 실패한 경우. 그러나 정합성은 보장된다 |
정리된 표에서 알 수 있듯이, 도메인 로직과 메시지 발송이 성공하거나 실패할 수 있다.
그러나, 모든 경우에서 결과적 일관성
을 모두 보장할 수 있다.
즉, 트랜잭션 아웃박스 패턴은 외부 메시징 시스템을 이용할 때 결과적 일관성
을 보장하는 전략이다.
글을 읽다보니 '아..! 나도 저거 알아야하는데!' 하진 않으신가요?!
여기까지 시리즈를 읽으셨다면, 아마 항해 플러스 과정에 관심있으신 분이라고 생각합니다.
궁금한 점이 있으시다면 저에게 편하게 댓글 남겨주세요!
혹은 highestbright98@naver.com
으로 메일을 남겨주셔도 굉장히 빨리 답변한답니다!
항해 플러스 백엔드에 관련해서 궁금하신점이 있다면 언제든 편하게 말씀주세요!
그리고 글이 도움됐다면, 등록하실때 제 추천인 코드 [9MPLfu] 입력하면,
20만원 할인의 혜택이 있답니다! (물론 저에게도 혜택이 있습니다! 😉)