MSA 에서 분산된 원격 서비스간 트랜잭션 관리 방법

MSA 에서 분산된 원격 서비스간 트랜잭션 관리 방법

지난 주말에 KSUG세미나에서 최범균 님의 “몇가지 도메인 구현 이야기” 강연 중 이벤트 부분 ( http://www.slideshare.net/madvirus )을 들으면서 한동안 고민하던 MSA 환경에서 분산된 서비스간의 트랜잭션 관리를 위한 방법으로 활용하면 좋겠다는 생각이 들어서 생각을 정리해 본다.

굳이 꼭 MSA라는 거창한 단어를  갖다 붙이지 않더라도, 서로 다른 두개의 플랫폼간에 서비스들을 orchestration하여 사용할 때 트랜잭션 처리가 정말 만만치 않다. 특히 내가 일하고 있는 금융 도메인에서는 이 트랜잭션이라는 것이 비니지스 프로세스에 굉장히 중요한 부분을 차지하고 있기 때문에 이 주제를 가지고 architecturing하는 것이 간단한 문제가 아니라고 생각한다.

MSA라는 아키텍처 패러다임이 근래에 많이 회자되고 있고, 누구는 buzzword라고도 얘기하고.. 자칫 이 주제에 대해서 잘못 얘기했다가 물어 뜯기는 사람들을 많이 봐와서 섣불리 얘기를 꺼내기 쉽지 않지만.. 뭐 나같은 사람이 하는 얘기 누가 깊게 관심가질까 하는 마음에 간단히 정리해 보고자 한다.

MSA의 특성을 얘기할 때 개인적으로 제일 중요하고 어려운것이라고 생각하는 것이 마틴파울러가 MSA의 9가지 특성을 얘기하면서 언급하는 “Decentralized Data Management”이다.

(마틴파울러가 얘기하는 9가지 특성은 “이렇게 해야한다”가 아니고 MSA를 하다보면 이러한 특성을 갖게 된다고 라고 이해하는 것이 옳다. 자세한 얘기는 나중에 따로 다루도록 하는 것으로…)

DDM에 대해서 이해를 하려면 그 반대 개념인 CDM ( Centralized Data Management )부터 이해할 필요가 있다. 사실 CDM 이라는 단어보다 ‘트랜잭션 관리’라고 얘기하는 것이 훨씬 친숙할 것이라고 생각된다. 아주 작은 어플리케이션을 개발하더라도 트랜잭션 없이 개발하는 것은 거의 사실상 불가능하지 않을까. 아주 오래전이야 이 트랜잭션에 대한 제어를 개발자가 한땀한땀 코딩을 해서 더 어려운 점이 있었지만 요즘에서야 프레임워크에서 알아서 다 해주니 비지니스의 무결성을 보장해 주는 좋은 도구인 이 트랜잭션관리 기능을 쓰지 않을 이유가 없다.

예를 들면 쇼핑몰에서 물건을 구입하려고 했을 때 상품을 장바구니에 담고 결제를 시도하게 되는데 이 때 어떤 이유에서는 결제하는 과정에서 오류가 발생하게 된다면 상품의 재고를 하나 복원(롤백)시켜야 할 필요가 있다. 이렇게 결제하는 기능과 상품의 재고를 하나 감소시키는 것은 하나의 트랜잭션으로 묶어서 관리하게 되고 이런 것을 DDM 의 반대 개념으로 생각해 볼 수 있다.

최근 MSA가 이슈가 되면서 위에 예시로 든 “결제” , 기능과 “상품” 기능은 서로 다른 팀에서 각각의 컴포넌트로 개발되어 독립된 환경에 배포되어 서비스하는 것을 미덕으로 여기고 있다.

이때 중요한 것이 MSA의 특성 중 하나인 DDM을 적용하게 되는데, 이렇게 한다는 것은 곧 물리적인 DB도 따로 분리하는 것이 좋고 당연히 두개의 어플리케이션 간에는 의존성을 제거해야 마땅하다.

에릭에반스가 DDD에서 얘기하는 Bounded Context도 이러한 개념이다.

자, 그럼 다 좋은데 이 상황에서 우리가 앞에서 얘기했던 결제가 성공하면 상품의 재고를 하나 감소시키고, 실패했을 때 상품의 재고를 하나 증가(롤백)시키는 것은 어떻게 해야 할까? 결제와 상품 두개의 서비스는 서로 언어도 통일되어 있지 않을 가능성도 있고 DB도 서로 다른 제품을 사용할 가능성이 많다(Polyglot). 당연히 하나의 트랜잭션으로 묶는 것을 구현하기 쉽지 않다. 사실 거의 불가능한다.

이런 경우 MSA전문가들이 제시하는 “원론적인” 몇몇 얘기들 중 하나가 보상 트랜잭션을 만들어야 한다는 것이다. ( 첨부터 트랜잭션으로 묶을 필요가 없도록 서비스를 디자인 하라 라고 하는 것은 개인적으로 현실과 너무 동떨어진 얘기인 것 같고, XA같은 것을 이용해서 이기종 분산 DB를 트랜잭션으로 묶어보라고 하는 것도 실제 XA를 적용하면서 얼마나 고생하는지 잘 봐온 터라 아직은 별로 고려하고 싶지 않다. 더구나 최근에는 DB같은 인프라스트럭쳐에도 의존관계를 되도록 느슨하게 가져가는 것이 권장되고 있다.)

즉, 보상 트랜잭션(compensating operations)이 무슨 얘기인고 하니, 상품쪽 서비스에서는 상품의 재고를 하나 감소시키는 API를 제공해다고 하면 이에 대한 보상 트랜잭션으로 상품의 재고를 하나 증가시는 API를 제공하는 것이고, 결제가 실패하는 경우 이 보상 트랜잭션을 호출하라는 것이다.

일견하면 API를 두개씩 (혹은 두배로) 만들어야 하니 귀찮기는 해도 MSA로 얻어지는 장점을 생각한다면 썩 괜찮은 방법인것도 같다.

하지만 실제 구현을 하려다 보면 상품의 재고를 하나 증가시키는 보상트랜잭션을 호출하다가 오류가 나면 어떻게 하지? 라는 상황을 만나게 된다.뭐 억지로 한다라고 하면 로그를 모아놓고 배치처리를 하는 방법도 있고, 성공할때까지 계속 호출하는(헉.. 정말로?)방법도 있을 수 있다.하지만 뭐든 별로 우아하지 않아 보인다.

자! 그럼 이렇게 하나의 트랜잭션을 묶고 싶은 분산된 서비스간의 보상 트랜잭션을 우아하게 처리하려면 어떻게 해야 할까? 이렇게 하나로 묶고 싶은 두개의 기능이 꼭 “실시간” 으로 동기화 처리 되지 않아도 된다고 하면 “역활과 책임”의 관점에서 썩 괜찮은 방법이 있다. (사실 이런 경우 수초의 지연은 사용자에게 거의 실시간으로 받아들여지기도 한다.)

“역할과 책임”의 관점에 볼때, 위 예시에서 보면 결제라고 하는 상위 서비스에서 상품이라고 하는 하위 서비스를 사용하고 있는데, 상품의 재고를 늘리고 줄이는 것과 그 과정에서 발생할 수 있는 오류를 적절하게 처리하는 것을  상품 서비스의 역할과 책임이라고 하자.  ( 좀 야비한가? 뭐 세상이 다 그런거 아니겠는가? )

그럼 “결제”의 입장에서 어떤 방식으로 결제가 성공하거나 실패했을 때 상품의 재고량에 대한 역할과 책임을 상품이 갖게 할 수 있을까?

“결제” 어플리케이션에서는 “결제”에 같이 묶고자 하는 다른 서비스 ( 재고 감소, 포인트 적립 등..) 트랜잭션에 대한 이벤트를 “결제” 의 boundary안에 있는 DB에 저장하고 (여기까지는 결제트랜잭션과 하나의 트랜잭션으로 묶어야 한다), DB에 저장된 이벤트들을 “상품”서비스에 전달해 주던지, 가져하게 하는 것이다. 이렇게 발생한 하위 이벤트(상품 재고, 포인트 적립등)들을 “결제”서비스의 context안에 저장하는 것 까지는 “결제”서비스의 역할과 책임으로 보고 저장된 이벤트들을 실제로 처리하는 역할과 책임은 상품이나 결제의 하위 서비스들이 가져가게 하자.

이렇게 “결제” boundary에 쌓이게 되는 이벤트들을 “상품”이 처리하게 하는 방법은 두가지로 생각해 볼 수 있다. 먼저 “결제”가 “상품”에게 이벤트를 전달하는 방식과- event forwarder-  “상품”이 “결제”로 부터 이벤트를 가져하는 방식-event diapatcher-으로 생각해 볼 수 있다.

먼저, 첫번째 방식은 “결제”가 “상품”에게 이벤트를 전달하는 방식은 “결제”이벤트가 발생할 때마다 “상품”에게 이벤트를 전달함으로써 실시간에 가깝게 처리될 수 있고 상태관리를 하지 않아도 된다는 장점이 있지만, 앞서 고민했던 “결제”가 “상품”에게 이벤트를 전달하다가 실패하는 경우 “결제”입장에서 어떻게 처리해야 할지 고민이 될 수 밖에 없다.

두번째 방법은 “상품”이 “결제”로부터 주기적으로 이벤트의 목록을 요청해서 받아가는 방식인데, “상품”이 이벤트를 어디까지 처리했는지 상태관리를 할 필요가 있고, 오류가 발생한 경우 재시도 정책등을 “상품”에서 가져갈 수 있는 장점이 있다. 상태 관리를 할 필요가 있다는 말은 “상품”이 어디까지 이벤트를 처리했는지 알고 있어야 하고 “결제”에 요청할 때는 처리하고자 하는 범위(인덱스)를 같이 말해줘야 한다.

즉, “14987번 이후에 발생한 모든 이벤트를 주세요”  또는 “5000번 이후에 발생한 이벤트 10개를 주세요” 나 ,실패한 것을 재처리 힐 필요가 있다면 “14876번 이벤트를 주세요”라고 요청 할 수 있다.

어떻게 보면 “결제”라는 서비스를 위해 “상품”이라는 서비스가 너무 많은 일을 해야 하는 것이 아닌가 라고 생각될 수 있지만, “상품의 재고량에 대한 정확한 정보를 전달”하는 역할과 책임은 “결제” 서비스가 아닌 “상품”서비스에 있다고 볼 수 있다.

실제 적용하게 되면 또 어떤 다른 문제가 생길지 모르지만 ,  이벤트들을 저장하는 형태(데이터 구조)나, 이벤트를 가져가고 전달해 주는 것들에 대한 표준을 정하고, 공통된 기능으로 만들어 사용하면 꽤나 유용하게 사용될 것 같은 생각이 든다.

참고한 사이트

http://www.slideshare.net/madvirus

http://martinfowler.com/articles/microservices.html