2개의 다른 트랜잭션을 안정적으로 다루기 위한 고민을 담은 글입니다.
2개의 다른 트랜잭션을 멀티 트랜잭션이라고 부르겠습니다.
바로 시작하겠습니다.
멀티 트랜잭션 핸들링을 고민하게 된 계기
레거시 애플리케이션에서는 XA 트랜잭션을 사용해 멀티 트랜잭션을 핸들링하고 있다.
시스템을 이관하면서 XA 트랜잭션을 사용하지말아달라는 요구사항이 들어왔고, 이를 수정해야했다.
XA 트랜잭션은 A라는 데이터베이스와 B라는 데이터베이스에서 동시에 데이터를 처리할 때,
이들을 하나의 트랜잭션으로 묶어서 ACID를 보장해주는 글로벌 트랜잭션이다.
2개의 DB를 사용하는 이유는??
2개의 데이터베이스를 A 데이터베이스, B 데이터베이스라고 부르겠다.
A 데이터베이스는 애플리케이션의 전체적인 비즈니스 로직 데이터들이 저장되고 수정되며 삭제된다.
B 데이터베이스는 비즈니스의 최종 결과물을 저장하고, 고객사에서 매핑해줘야하는 데이터는 B 데이터베이스에서 조회한다.
XA 트랜잭션을 사용하지 않는 다른 레거시
2개의 트랜잭션을 다루는 다른 레거시가 있다?!
XA 트랜잭션을 쓰지 않고, 두 개의 트랜잭션을 다루는 다른 레거시 프로젝트가 있다.
배치가 아닌 API인데 구조는 다음과 같다.
1번 로직: A 데이터베이스에서 데이터를 수정하고 전송할 데이터를 종합해 insert 후 커밋한다.
2번 로직: A 데이터베이스에 insert한 값을 기반으로 B 데이터베이스에 insert 후 커밋한다.
3번 로직: B 데이터베이스에 insert가 성공했다면 1번 로직에서 저장한 데이터에 B 데이터베이스에 insert를 성공했다는 상태 값을 update 한다.
위와 같은 레거시 구조에서는 문제가 발생할 수 있는데.... 어떤 문제가 발생하는지 살펴보고, 레거시가 이런 문제를 어떻게 막는지 알아보자.
- A 데이터베이스에 insert하는 로직(1번)에서 예외가 발생하면 롤백이 정상적으로 수행되므로 문제가 없다.
- B 데이터베이스에 insert하는 로직(2번)에서 예외가 발생하면 1번 로직이 롤백이되지 않아 문제가 생긴다. B 문제라고 하겠다.
- A 데이터베이스에 성공했다는 상태 값을 update 하는 로직에서 예외가 발생하면 1번 로직 2번 로직 모두 롤백되지 않아 문제가 생긴다. 이는 C 문제라고 하겠다.
로직에 원자성이 보장되지 않아 문제가 생기는데... 그렇다면 레거시는 이를 어떻게 막을까??
동일한 데이터의 1번 로직, 2번 로직에서도 중복 insert를 허용하면서 문제를 막고 있다.
주문이라는 예시를 활용해 2번 로직과 3번 로직에서 예외가 터졌을 때 예시를 들어 설명해보겠다.
1번 로직에서 insert 되는 데이터를 주문 메세지, 2번 로직에서 insert 되는 데이터를 주문 요청이라고 하겠다.
극단적으로 단순화한 예제 코드다.
fun logic() {
saveOrderMessage() //1번 로직, 간단하게 표현.
sendOrderRequest() //2번 로직
updateOrderMessage() //3번 로직
}
2번 로직에서 예외가 발생
- a라는 주문의 주문 메시지가 insert 되었는데, 2번 로직에서 예외가 터졌다.
- 그럼 a 주문의 주문 메시지는 롤백되지 않는다.
- 레거시는 A 데이터베이스에서 주문의 중복된 주문 메시지를 허용하면서 문제를 해결한다.
- 주문의 중복된 주문 메시지를 허용하면서 실패하더라도, 계속 해당 데이터를 삽입하고 B 데이터베이스에 insert를 진행할 수 있다.
- 즉 a 주문의 주문 메세지1, a 주문의 주문 메세지2가 계속 저장되면서 해당 로직을 반복적으로 계속 수행할 수 있는 것이다.
3번 로직에서 예외가 발생
- a 주문의 주문 메세지와 주문 요청이 저장됐는데 예외가 발생했다.
- 마찬가지로 B 데이터베이스에서 주문의 중복된 주문 요청을 허용하면서 문제를 해결한다.
- a 주문의 주문 요청1, a 주문의 주문 요청2가 계속 저장되면서 해당 로직을 반복할 수 있다는 것이다.
결론적으로 주문 메시지와 주문 요청 데이터의 중복된 insert와 update를 허용하면서 api가 반복된 수행이 가능하기 때문에 문제가 발생하지 않았다.
뭔가 이상하지 않은가..? 그렇다면 데이터베이스 B에 중복된 데이터가 계속 들어가면 문제가 생기지 않을까?!?!
문제는 발생하지 않는다. B 데이터베이스를 활용하는 외부 애플리케이션에서 중복된 주문 데이터가 들어와도 1건만 처리하기 때문이다.
애플리케이션으로 가져가는 주문 요청에 처리했다는 상태 값을 남긴다.
결국 문제가 발생하지 않는 이유는 B 데이터베이스에서 데이터를 가져가 활용하는 외부 애플리케이션이 중복된 데이터를 검증하고 처리한다는 지식을 가지고 개발했기 때문이다.
필자가 생각한 레거시 로직의 문제점
이 로직에는 몇 가지 문제점이 존재한다.
1. 클라이언트 수동 반복
우선 클라이언트가 문제가 발생하면 수작업으로 반복적인 처리를 진행한다는 것이다.
만약 네트워크 문제가 발생했다면 해당 비즈니스를 정상적으로 처리하기 위해 클라이언트는 API를 계속 호출할 것이다.
클라이언트에게 굉장히 불친절해보인다.
2. 데이터 파악이 힘듦
데이터를 검증하려고 데이터베이스에서 데이터를 찾으면 중복된 데이터로 인한 빠른 데이터 파악이 힘들다.
물론 B 데이터베이스를 사용하는 외부 애플리케이션도 어떤 주문 메세지를 가져갔는지 상태 값을 update하지만
어떤 데이터가 정상적으로 사용됐는지 주문에 대한 중복된 주문 메세지, 주문 요청이 많아 파악이 힘들다.
심지어 API는 계속 수행이 반복하기 때문에 외부 애플리케이션이 사용하지는 않지만, A 데이터베이스에 insert하고 update된 주문 메세지 중복이 상당히 많을 수 있다.
3. 외부 구현을 알고 의존한 모습
억지일 수도 있으나.. 만약 외부 애플리케이션이 중복 데이터를 검증하지 않았으면 문제가 발생할 수 있다.
개발은 보수적으로 진행하는게 맞다고 필자는 생각한다.
이건 어디까지 '외부 애플리케이션이 검증하니까 우리는 계속 보내도 문제가 없겠네 ㅋㅋ' 라는 생각을 가지고 개발한 형태라고 생각한다.
즉 우리 애플리케이션에서 안정적인 구조를 짜지 않고, 외부 구현에 대한 지식을 가지고 개발한 로직이므로 문제점이라고 생각한다.
그렇다면 어떻게 개선할까?? (with 트랜잭셔널 아웃박스 패턴)
일단 우리는 주문 메세지와 주문 요청이 단 1건만 들어가도록 수정할 것이다.
먼저 기존 API를 그대로 가져가나 주문 메세지에 대한 검증 로직을 추가한다.
fun logic() {
validateOrderMessage() //동일한 주문의 중복된 주문 메세지(A 데이터베이스)가 데이터가 있는지 검증.
saveOrderMessage() //1번 로직
sendOrderRequest() //2번 로직
updateOrderMessage() //3번 로직
}
그리고 로직이 실패할 경우를 대비해 배치를 하나 만들어준다. 로직은 아래 코드와 같다.
fun orderBatchJobLogic() {
findOrderMessage() //주문 요청 insert 성공 못했다는 상태를 가진 주문 메세지 조회 (A 데이터베이스)
if(isNotOrderValid()){ //해당 주문 요청 있는지 없는지 검증 (B데이터베이스)
//존재하지 않는다면
saveOrderRequest() //주문 메세지를 저장 (B데이터베이스)
}
/**
* 2번 로직에서 실패했다면, 주문 요청을 저장하고 주문 메세지 상태도 업데이트
* 3번 로직에서 실패했다면, 주문 요청만 검증하고 주문 메세지 상태 업데이트
*/
updateOrderMessage()
}
이렇게 로직을 바꾸면 데이터도 1건만 들어가고 배치를 사용하므로 사용자가 반복해서 처리하지 않아도 되며
외부의 지식 없이도 안정적으로 동작하는 애플리케이션을 로직을 구성할 수 있다.
구현하고 보니 MSA는 아니지만 트랜잭셔널 아웃박스 패턴과 같은 개념이라고 생각한다.
비즈니스 로직을 완료하고 트랜잭션 안에 발생해야할 메시지를 데이터베이스에 저장했으며,
별도의 프로세스(배치)가 데이터베이스에 저장된 이벤트를 읽어서 메세지 브로커는... 아니지만 다른 데이터베이스에 전송하기 때문이다.
트랜잭셔널 아웃박스 패턴의 좋은 글은 아래에서 참고 가능하다.
- https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&boardType=techBlog
- https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb
열심히 고민했으니..!!! 이제는 진짜 구현할 차례다.
열심히 뚝딱뚝딱 만들어보자.
이상으로 포스팅을 마칩니다. 감사합니다.
댓글