DevOn

Java/Spring 기반 통합 개발 프레임워크

DevOn logo
Tech Blog 목록으로
Featured

MSA 환경의 데이터 정합성, SAGA로 풀어내기

MSA 전환에서 트랜잭션이 끊기며 생기는 데이터 정합성 문제를, 2PC의 한계와 SAGA 패턴의 사상·구조를 통해 정리하고 DevOn Boot가 제공하는 DevOn SAGA를 소개한다.

|DevOn솔루션팀
insightmsasagadistributed-transactioneventual-consistencyoutboxdevon-boot

금융 시스템을 MSA로 전환하면 가장 먼저 부딪히는 벽이 "트랜잭션"입니다. 하나의 애플리케이션 안에서 당연하게 보장되던 데이터 정합성이, 서비스를 쪼개는 순간 더 이상 공짜로 주어지지 않기 때문입니다. 이 글에서는 MSA가 왜 대두되었고, MSA가 만들어내는 정합성의 한계가 무엇이며, 이를 해결하기 위해 등장한 SAGA 패턴이 어떤 사상과 구조를 가지는지, 그리고 실제 구현에서 반드시 함께 고려해야 할 기술적 장치들까지 차근차근 살펴봅니다.

1. MSA의 대두

글로벌 IT 기업이 쏘아 올린 신호탄

MSA(Microservices Architecture)는 Netflix, Amazon과 같은 글로벌 IT 기업들이 대규모 트래픽과 빠른 기능 출시 요구를 감당하기 위해 채택하면서 본격적으로 주목받기 시작했습니다. 하나의 거대한 애플리케이션(모놀리식, Monolithic)으로 모든 기능을 운영하던 방식에서 벗어나, 비즈니스 단위로 잘게 나눈 독립적인 서비스들의 집합으로 시스템을 구성하는 방식입니다.

이 흐름은 국내 금융권을 비롯한 다양한 산업의 시스템에도 빠르게 번졌습니다. 특히 클라우드 환경의 보편화가 이 변화를 가속했습니다.

사례로 보는 MSA: Netflix 아키텍처

MSA의 대표적인 사례로 자주 인용되는 것이 Netflix입니다. Netflix는 모놀리식 구조에서 출발했지만, 폭증하는 전 세계 트래픽과 빠른 기능 개선 요구를 감당하기 위해 AWS 클라우드 기반의 마이크로서비스 구조로 전환했습니다. 추천, 사용자 관리, 결제 같은 기능이 각각 독립적으로 배포·확장되는 별도의 서비스로 분리되어 있습니다.

Netflix 마이크로서비스 아키텍처

[Netflix 마이크로서비스 아키텍처]

이 구조에서 눈여겨볼 핵심 구성 요소는 다음과 같습니다.

  • 엣지 게이트웨이(Zuul): 모든 클라이언트 요청이 가장 먼저 도착하는 단일 진입점입니다. 들어온 요청을 적절한 마이크로서비스로 라우팅하고, 인증·모니터링 같은 공통 관심사를 처리합니다.
  • 서비스 디스커버리(Eureka): 수많은 서비스의 인스턴스가 동적으로 뜨고 사라지는 환경에서, 각 서비스가 서로의 현재 위치(주소)를 찾을 수 있게 해 주는 서비스 레지스트리입니다. 서비스들은 Eureka에 자신을 등록하고, 통신할 상대를 Eureka에서 조회합니다.
  • 회복탄력성(Hystrix 등): 한 서비스의 지연이나 장애가 이를 호출하는 다른 서비스로 연쇄 전파되지 않도록 장애를 격리하는 장치입니다. 그림의 추천 서비스, 사용자 서비스, 결제 서비스와 같이 각 서비스에 내장되어 다른 서비스를 호출할 때 동작합니다.
  • 콘텐츠 전송(Open Connect): 실제 영상 스트림은 마이크로서비스 코어가 아니라 별도의 CDN인 Open Connect를 통해 사용자에게 가까운 곳에서 직접 전달됩니다.

여기서 등장하는 Zuul, Eureka, Hystrix, Ribbon(클라이언트 측 로드 밸런싱) 등은 Netflix가 오픈소스로 공개한 것으로, Spring Cloud Netflix를 통해 Spring 생태계에 통합되어 널리 쓰였습니다. 즉 Netflix의 사례는 단순한 남의 회사 이야기가 아니라, Spring 기반으로 MSA를 구축하는 많은 시스템이 직접 차용한 구조이기도 합니다. 다만 이렇게 잘 분리된 구조에서도, 여러 서비스에 걸친 하나의 업무를 처리할 때 트랜잭션 정합성을 어떻게 지킬 것인가 하는 문제는 그대로 남습니다. 바로 이 글이 다루는 SAGA가 필요해지는 지점입니다.

클라우드와 MSA가 맞물린 이유

모놀리식 애플리케이션은 기능 하나를 늘리거나 트래픽이 몰릴 때 애플리케이션 전체를 통째로 늘려야(scale-out) 합니다. 100개의 기능 중 1개의 기능에만 부하가 몰려도, 그 1개를 위해 나머지 99개를 포함한 인스턴스 전체를 복제해야 하는 비효율이 발생합니다. 반면 클라우드는 필요한 자원을 필요한 만큼, 필요한 시점에 늘리고 줄이는 데 최적화된 환경입니다. 서비스가 잘게 나뉘어 있으면 트래픽이 몰리는 특정 서비스만 골라서 확장할 수 있기 때문에, 클라우드의 탄력적인 자원 운용 방식과 MSA의 구조가 자연스럽게 맞물립니다.

여기에 더해 컨테이너(Docker)와 오케스트레이션 플랫폼(Kubernetes)의 성숙은 "작게 나눈 서비스를 독립적으로 배포·확장·복구한다"는 MSA의 운영 모델을 현실적으로 가능하게 만들었습니다. 즉 MSA의 유행은 단순한 설계 사조의 변화가 아니라, 이를 뒷받침하는 클라우드·컨테이너 인프라가 함께 성숙했기 때문에 가능했던 것입니다.

MSA가 제공하는 주요 장점은 다음과 같이 정리할 수 있습니다.

  • 독립 배포: 서비스별로 따로 빌드·배포할 수 있어, 작은 변경을 위해 전체 시스템을 재배포할 필요가 없습니다. 배포 주기가 빨라지고 변경의 위험 범위가 좁아집니다.
  • 독립 확장: 부하가 큰 서비스만 선택적으로 확장할 수 있어 자원을 효율적으로 사용합니다.
  • 장애 격리: 한 서비스의 장애가 시스템 전체로 번지는 것을 구조적으로 막기 쉽습니다.
  • 기술 다양성: 서비스마다 업무 특성에 맞는 데이터베이스나 기술 스택을 선택할 수 있습니다(database-per-service).
  • 조직 확장성: 팀을 서비스 단위로 나눠 독립적으로 개발·운영할 수 있어, 조직이 커져도 개발 속도를 유지하기 쉽습니다.

이러한 장점들이 클라우드 보급과 맞물리면서, MSA는 대규모·고가용성 시스템을 지향하는 조직의 사실상 표준 아키텍처 후보로 자리 잡게 되었습니다.

2. MSA의 한계

성공적인 MSA를 위한 필수 전제 조건: "업무 분석과 재설계"

MSA의 장점은 강력하지만, 그것은 어디까지나 업무 시스템을 제대로 분석하고 재설계했을 때 얻어지는 결과입니다. 기존에 하나의 애플리케이션 안에서 동작하던 기능들이 여러 서비스로 분산되면, 과거에는 애플리케이션 내부의 단순한 메서드 호출이었던 것이 이제는 다른 서비스를 향한 네트워크 통신으로 바뀝니다.

이 변화는 두 가지 비용을 만듭니다.

  1. 통신 부하: 프로세스 내부 호출(in-process call)은 나노초 단위로 끝나고 실패하지 않지만, 네트워크 호출은 밀리초 단위의 지연과 함께 타임아웃·재시도·부분 실패라는 새로운 실패 모드를 동반합니다. 한 번의 사용자 요청이 여러 서비스를 연쇄적으로 호출하면 지연이 누적되고, 한 서비스의 느려짐이 호출 체인 전체로 전파될 수 있습니다.
  2. 끊어진 트랜잭션: 하나의 트랜잭션으로 묶여 있던 작업이 서비스 경계를 넘으면서 여러 개의 독립적인 트랜잭션으로 쪼개집니다. database-per-service 원칙에 따라 각 서비스가 자기 데이터베이스를 갖기 때문에, 하나의 데이터베이스 트랜잭션으로 여러 서비스의 변경을 묶을 방법이 사라집니다.

따라서 서비스 간 호출을 최소화하도록 업무를 재설계하는 과정이 MSA 전환의 핵심입니다. 서비스 경계(bounded context)를 어떻게 긋느냐에 따라, 하나의 업무가 한 서비스 안에서 끝날 수도 있고 여러 서비스를 넘나들 수도 있기 때문입니다. 하지만 현실의 많은 MSA 전환 프로젝트는 기존 서비스를 단순히 Java로 다시 짜는 수준의 재설계에 머무는 경우가 많아, 이러한 업무 재설계가 공정에 제대로 반영되지 않곤 합니다.

게다가 설계를 아무리 잘 하더라도, 계정계 시스템에서는 중요 공통 서비스(예: 계좌, 고객, 한도 등)에 다양한 애플리케이션이 의존할 수밖에 없습니다. 서비스 간 의존성은 줄일 수는 있어도 완전히 없앨 수는 없습니다.

MSA 구현의 한계점: 트랜잭션 끊김으로 인한 정합성 문제

여러 한계 중에서도 트랜잭션이 끊기면서 생기는 데이터 정합성 문제는 성격이 다릅니다. 통신 부하는 성능의 문제지만, 정합성 붕괴는 시스템이 다루는 데이터의 신뢰 자체를 무너뜨리고, 금융 시스템에서는 대형 장애와 정산 사고로 직결되는 문제이기 때문입니다.

먼저 모놀리식이 어떻게 정합성을 보장했는지 짚어 보겠습니다. 단일 데이터베이스는 ACID 속성을 제공합니다. 원자성(Atomicity)은 묶인 작업이 전부 성공하거나 전부 실패하도록 보장하고, 일관성(Consistency)은 제약 조건을 지키며, 격리성(Isolation)은 동시에 실행되는 트랜잭션들이 서로의 중간 상태를 보지 못하게 막고, 지속성(Durability)은 커밋된 결과가 사라지지 않도록 보장합니다. 모놀리식에서 "예금 차감 + 대출 상환"을 하나의 트랜잭션으로 묶으면, 이 네 가지가 자동으로 지켜집니다.

구체적인 예를 들어 보겠습니다. 계좌 서비스, 대출 서비스, 뱅킹 서비스가 있다고 가정합니다. "내 예금계좌의 잔액을 확인해 대출 계좌의 대출을 상환한다"는 업무를 생각해 봅시다.

모놀리식 vs MSA의 트랜잭션 차이

[모놀리식 vs MSA의 트랜잭션 차이]

모놀리식 시절에는 이 모든 것이 한 애플리케이션 안에 있었기 때문에, 예금 차감과 대출 상환이 하나의 트랜잭션으로 처리되었습니다. 중간에 어떤 단계가 실패하면 데이터베이스가 전체를 자동으로 롤백해 주므로, "돈은 빠져나갔는데 상환은 안 된" 상태가 발생할 수 없었습니다.

그러나 MSA에서는 계좌·대출·뱅킹이 각각 독립된 서비스이고, 각자 자신의 데이터베이스를 가집니다. 예금 차감은 계좌 서비스의 로컬 트랜잭션으로 이미 커밋되었는데, 그다음 단계인 대출 상환이 실패하면 어떻게 될까요? 계좌 서비스 입장에서는 이미 정상적으로 커밋을 마쳤기 때문에, 다른 서비스의 실패를 이유로 자동으로 되돌려 줄 방법이 없습니다. 결과적으로 예금은 줄었는데 대출은 그대로인 정합성 붕괴 상태가 남습니다.

이런 문제를 해결하기 위해 분산 환경에서의 트랜잭션 처리 방안이 다양하게 연구되었습니다.

3. SAGA의 탄생 배경

2PC 기반의 트랜잭션 확보 방안과 한계점

분산 환경의 정합성 문제를 풀기 위해 전통적으로 제시된 방법이 2단계 커밋(2PC, Two-Phase Commit) 입니다. 2PC는 중앙의 코디네이터(coordinator)가 모든 참여 서비스에 "커밋할 준비가 되었는가"를 먼저 묻고(Prepare 단계), 모두가 동의하면 그때 일괄적으로 커밋을 지시하는(Commit 단계) 프로토콜입니다.

동작을 조금 더 풀어 보면 이렇습니다. Prepare 단계에서 각 참여자는 자신의 작업을 수행할 수 있는지 확인하고, 가능하다면 관련 자원에 락을 건 채로 "준비 완료"를 코디네이터에게 응답합니다. 코디네이터는 모든 참여자가 "준비 완료"를 보냈을 때만 Commit 단계로 진입해 일괄 커밋을 지시합니다. 한 명이라도 거부하면 전체를 롤백합니다. 모든 참여자가 함께 커밋하거나 함께 롤백하므로, 분산 환경에서도 강한 원자성(all-or-nothing)을 보장합니다.

문제는 이 강한 보장을 얻기 위해 치르는 대가입니다. 2PC는 다음과 같은 한계를 가집니다.

  • 블로킹(blocking) 특성: 각 참여자는 코디네이터의 최종 결정이 내려질 때까지 자원(락)을 잡은 채 대기해야 합니다. 이 락이 유지되는 동안 해당 데이터에 접근하려는 다른 트랜잭션들도 함께 대기하게 되어, 처리량과 확장성이 크게 떨어집니다.
  • 단일 장애점(Single Point of Failure): 코디네이터가 Prepare 이후, Commit 지시 이전에 장애를 일으키면, 참여자들은 커밋해야 할지 롤백해야 할지 결정을 받지 못한 채 자원을 잡고 무한정 대기하는 상황에 빠질 수 있습니다.
  • 성능 오버헤드: 여러 번의 네트워크 왕복과 로그 기록(disk force)이 더해져 지연이 커집니다.

이러한 블로킹과 강한 결합도(tight coupling) 때문에, 2PC는 독립 배포와 확장, 고가용성을 지향하는 MSA의 철학과 잘 맞지 않습니다. CAP 정리의 관점에서 보면, MSA는 가용성(Availability)과 분할 내성(Partition tolerance)을 중시하는 반면 2PC는 강한 일관성(Consistency)에 치우쳐 있어 지향점이 어긋납니다. 또한 서비스마다 다른 종류의 데이터베이스를 쓰는 database-per-service 환경에서는 모든 참여자가 2PC 프로토콜을 지원해야 한다는 전제 자체를 만족시키기 어렵습니다. 즉 2PC는 정합성은 만족시키지만 MSA가 필요로 하는 성능과 가용성을 만족시키지 못했습니다.

성능을 지키며 정합성을 확보하기 위한 선택, SAGA

흥미롭게도 SAGA라는 개념 자체는 MSA보다 훨씬 이전에 등장했습니다. 1987년 Hector Garcia-Molina와 Kenneth Salem이 ACM SIGMOD에서 발표한 논문 "Sagas"에서 처음 제안된 개념으로, 원래는 단일 데이터베이스에서 오래 실행되는 트랜잭션(LLT, Long-Lived Transaction) 을 다루기 위한 것이었습니다. LLT는 데이터베이스 자원을 오랫동안 점유해, 더 짧고 빈번한 다른 트랜잭션들의 처리를 지연시키는 문제가 있었습니다.

이 논문의 핵심 아이디어는 다음과 같습니다. 하나의 긴 트랜잭션을 여러 개의 작은 로컬 트랜잭션의 시퀀스로 나누고, 각 단계마다 그 단계를 논리적으로 되돌리는 보상 트랜잭션(compensating transaction) 을 정의해 둔다는 것입니다. 그러면 시스템은 "시퀀스 전체가 성공적으로 완료되거나, 아니면 이미 수행한 부분을 보상 트랜잭션으로 되돌려 부분 실행을 바로잡는다"는 것을 보장할 수 있습니다. 원논문은 이 개념과 구현이 비교적 단순하면서도 성능을 크게 개선할 잠재력이 있다고 설명합니다.

이 30여 년 전의 아이디어가 오늘날 분산 환경에서 다시 주목받게 되었습니다. "긴 트랜잭션을 작은 로컬 트랜잭션으로 쪼개고 보상으로 실패를 처리한다"는 메커니즘이, "서비스 경계를 넘는 트랜잭션"이라는 MSA의 문제에 그대로 적용될 수 있었기 때문입니다. 이렇게 분산 환경에 적용된 형태를 흔히 분산 사가(distributed saga) 라고 부릅니다.

정리하면, SAGA는 2PC처럼 자원을 잡고 기다리는 대신, 각 서비스가 자신의 로컬 트랜잭션을 즉시 커밋하고 실패 시에는 보상으로 되돌리는 방식을 택합니다. 그 대가로 SAGA는 강한 즉시 정합성(strong consistency)이 아니라 최종적 정합성(eventual consistency) 을 지향합니다. 즉 "어느 시점에는 일시적으로 불일치가 보일 수 있지만, 결국에는 일관된 상태로 수렴한다"는 모델입니다. 이는 MSA가 추구하는 성능·확장성·가용성과 맞바꾼 합리적인 선택입니다.

4. SAGA 기술

핵심 사상: 로컬 트랜잭션 + 보상 트랜잭션

SAGA의 기본 아이디어는 단순합니다. 하나의 큰 작업을 여러 개의 작은 단계로 나누고, 각 단계마다 그것을 되돌리는 "보상" 작업을 미리 준비해 두는 것입니다.

  • 로컬 트랜잭션: 한 서비스가 자기 데이터베이스에 수행하고 바로 확정(커밋)하는 작업입니다. 예: 계좌에서 100원 출금.
  • 보상 트랜잭션: 그 작업을 되돌리는 작업입니다. 예: 계좌에 100원 다시 입금.

작업이 순서대로 진행되다가 중간에 실패하면, SAGA는 이미 끝낸 단계들을 역순으로 되돌립니다.

SAGA 보상 트랜잭션 흐름

[SAGA 보상 트랜잭션 흐름]

여기서 일반적인 데이터베이스 트랜잭션과 다른, SAGA만의 중요한 특징 두 가지를 알아 둘 필요가 있습니다.

(1) 자동으로 되돌려 주지 않는다

일반 트랜잭션은 실패하면 데이터베이스가 알아서 원래대로 되돌려 줍니다. 하지만 SAGA에서는 각 단계가 이미 확정된 상태라, 개발자가 "되돌리는 방법(보상)"을 직접 만들어 두어야 합니다.

그리고 보상은 "처음부터 없었던 일"로 만드는 것이 아니라 "반대 작업으로 메우는 것"에 가깝습니다. 예를 들어 결제를 취소하면 명세서에는 결제와 환불 기록이 모두 남습니다. 그래서 보상을 설계할 때는, 같은 보상이 여러 번 실행돼도 결과가 같도록 하고(멱등성), 이메일 발송이나 실물 출고처럼 되돌릴 수 없는 작업은 되도록 맨 뒤에 배치하는 것이 좋습니다.

(2) 중간 상태가 다른 작업에 보일 수 있다

일반 트랜잭션은 처리가 끝나기 전까지 중간 상태를 다른 작업이 보지 못하게 막아 줍니다. 하지만 SAGA는 각 단계를 바로 확정하기 때문에, 아직 끝나지 않은 "임시 상태"가 다른 작업에 그대로 보일 수 있습니다.

SAGA의 중간 상태 노출 문제

[SAGA의 중간 상태 노출 문제]

이게 왜 문제일까요? 어떤 작업이 SAGA의 임시 값을 미리 읽고 판단을 내렸는데, 나중에 그 SAGA가 실패해 값이 되돌려지면 잘못된 판단이 되어 버립니다. 그래서 SAGA에서는 이런 상황을 막는 장치를 함께 설계해야 합니다. 가장 기본이 되는 방법은 "처리 중" 표시를 남기는 것입니다. 아직 확정되지 않은 데이터에 "처리 중" 같은 상태를 표시해 두면, 다른 작업이 그 값을 보고 함부로 행동하지 않게 됩니다. (이 외에도 여러 기법이 있지만, 핵심은 "확정 전 값은 확정 전이라고 알린다"는 것입니다.)

단계에도 성격이 있다

SAGA를 안전하게 설계하려면, 각 단계가 "되돌릴 수 있는 단계"인지 "되돌릴 수 없는 단계"인지를 구분하는 것이 도움이 됩니다. 되돌릴 수 없거나 위험한 작업(예: 한도를 늘리거나 권한을 주는 작업)은 가능한 한 SAGA의 뒤쪽에 배치합니다. 그러면 앞 단계에서 실패하더라도 위험한 작업까지 가기 전에 멈출 수 있어, 되돌릴 일 자체가 줄어듭니다.

두 가지 구현 방식: 코레오그래피(Choreography)와 오케스트레이션(Orchestration)

SAGA를 실제로 구성하는 방식은 크게 두 가지입니다.

코레오그래피 vs 오케스트레이션 비교

[코레오그래피 vs 오케스트레이션 비교]

코레오그래피(Choreography) 는 중앙 지휘자가 없는 방식입니다. 각 서비스가 자기 일을 마치면 "끝났다"는 신호(이벤트)를 보내고, 다음 서비스가 그 신호를 받아 이어서 일합니다. 마치 정해진 안무에 따라 서로 신호를 주고받으며 춤추는 것과 비슷합니다. 서비스가 적고 흐름이 단순할 때는 깔끔하게 동작하지만, 단계가 많아지면 "지금 전체가 어디까지 진행됐는지", "실패하면 어떤 순서로 되돌려야 하는지"가 여러 서비스에 흩어져 흐름을 파악하기 어려워집니다.

오케스트레이션(Orchestration)오케스트레이터라는 중앙 지휘자를 두는 방식입니다. 지휘자가 각 서비스에게 차례로 "이 작업을 해라"라고 지시하고 결과를 받으며, 중간에 실패가 나면 지금까지 끝난 단계들을 역순으로 되돌리도록 지시합니다. 오케스트라의 지휘자가 연주자들을 차례로 이끄는 모습과 같습니다. 지휘자는 SAGA가 지금 어느 단계에 있는지를 계속 기록해 두기 때문에, 도중에 시스템이 재시작돼도 멈췄던 지점부터 이어서 처리할 수 있습니다.

복잡한 업무에서는 왜 오케스트레이션이 유리한가

단계가 많고 분기가 복잡한 업무에서는 오케스트레이션이 더 적합합니다. 이유는 간단합니다.

  • 흐름이 한곳에 모인다: 전체 순서와 실패 시 보상 순서가 지휘자 한 곳에 정의돼 있어, 흐름을 이해하고 고치기 쉽습니다.
  • 상태를 한눈에 본다: 어디까지 됐고 어디서 실패했는지 지휘자가 알고 있어, 되돌리기와 모니터링이 쉽습니다.
  • 서비스끼리 덜 얽힌다: 각 서비스는 "다음에 누구를 부를지" 신경 쓸 필요 없이 지휘자의 지시만 따르면 됩니다.

여러 공통 서비스가 얽히고 단계가 많은 금융 업무에서는, 흐름을 한곳에서 통제하고 실패 시 보상을 확실히 책임지는 오케스트레이션의 이점이 특히 크게 작용합니다.

메시지 유실, 중복을 방지하기 위한 설계

SAGA가 잘 돌아가려면, 각 서비스가 자기 데이터를 바꾸는 일"바꿨다"고 알리는 메시지를 보내는 일이 어긋나지 않아야 합니다.

그런데 데이터베이스에 쓰는 것과 메시지를 보내는 것은 서로 다른 시스템이라, 하나로 묶기가 어렵습니다. 데이터는 바뀌었는데 메시지가 안 나가면 다음 서비스가 그 사실을 모르고, 반대로 메시지는 나갔는데 데이터 변경이 취소되면 일어나지도 않은 일을 처리하게 됩니다. 이를 이중 쓰기 문제라고 합니다.

Transactional Outbox 패턴

[Transactional Outbox 패턴]

대표적인 해결책이 Outbox 방식입니다. 보낼 메시지를 곧장 외부로 보내지 않고, 업무 데이터와 함께 같은 데이터베이스에 저장해 둡니다(둘이 한 묶음이라 어긋날 수 없습니다). 그런 다음 별도의 전달 담당이 그 저장된 메시지를 꺼내 실제로 발행합니다. 이 방식은 메시지가 빠지지 않는 대신 같은 메시지가 두 번 전달될 수 있어서, 받는 쪽은 같은 메시지를 두 번 받아도 한 번만 처리한 것처럼 동작하도록(멱등) 만들어야 합니다. 이를 위해 받은 메시지를 기록해 중복을 걸러내는 방식(Inbox)을 함께 씁니다.

정리하면, SAGA는 혼자 동작하는 패턴이 아니라 안전한 메시지 발행(Outbox)과 중복 안전 처리(Inbox)가 함께 갖춰져야 견고해집니다.

SAGA 적용 시 체크포인트

금융 시스템에 SAGA를 적용할 때 함께 챙겨야 할 점을 간단히 정리하면 다음과 같습니다.

  • 보상 설계: 각 단계를 되돌리는 방법을 빠짐없이 만들고, 여러 번 실행돼도 안전하게(멱등) 합니다.
  • 순서 설계: 되돌리기 어려운 작업은 뒤쪽에 배치합니다.
  • 중간 상태 관리: "처리 중" 표시 등으로 확정 전 값이 잘못 쓰이지 않게 합니다.
  • 안전한 메시징: Outbox·Inbox로 메시지 누락과 중복을 막습니다.
  • 최종적 정합성 합의: 잠깐의 불일치가 업무적으로 허용되는지 미리 합의합니다.
  • 지휘자의 안정성: 오케스트레이션을 쓴다면 중앙 지휘자가 멈추지 않도록 상태 보관과 복구를 챙깁니다.

5. DevOn Boot가 제공하는 DevOn SAGA

앞에서 SAGA가 무엇이고, 왜 복잡한 환경에서는 오케스트레이션 방식이 유리한지, 그리고 안전하게 동작하려면 어떤 장치들이 함께 필요한지 살펴봤습니다. 문제는 이 모든 것을 개발팀이 직접 구현하려면 손이 많이 간다는 점입니다. 오케스트레이터도 만들어야 하고, 아웃박스·인박스도 붙여야 하고, 상태 저장과 재처리, 모니터링까지 일일이 챙겨야 합니다.

LG CNS의 DevOn Boot 프레임워크는 이런 부담을 덜어 주기 위해, 오케스트레이션 방식의 SAGA 기능(DevOn SAGA) 을 프레임워크 차원에서 제공합니다. 앞 장에서 설명한 개념들이 대부분 기능으로 미리 준비되어 있어, 개발자는 업무 흐름과 보상 로직 설계에 집중할 수 있습니다.

DevOn SAGA가 제공하는 주요 기능을 하나씩 살펴보겠습니다. 그림으로 먼저 전체 그림을 보면 이해가 쉽습니다.

DevOn SAGA 기능 구성

[DevOn SAGA 기능 구성]

DevOn SAGA 주요 기능

[DevOn SAGA 주요 기능 (출처: DevOn Boot Sales Deck)]

오케스트레이션 방식의 흐름 관리

오케스트레이션 방식이므로, 중앙의 오케스트레이터가 SAGA의 전체 순서를 지휘합니다(4장에서 설명한 그 방식입니다). DevOn SAGA는 진행 중인 SAGA가 "지금 어느 단계까지 왔는지"를 상태로 관리하고 저장소에 보관합니다. 덕분에 중간에 프로세스가 재시작되더라도 진행 중이던 작업을 이어서 처리할 수 있고, 실패가 나면 정확한 역순으로 보상을 진행할 수 있습니다.

메시지 발행의 정합성을 보장하기 위한 기능: 이벤트 발행 · Outbox · Inbox · 중복 수신 방지

오케스트레이터와 각 서비스는 메시지를 주고받으며 협력합니다. 이때 메시지가 누락되거나 잘못 처리되면 정합성이 깨지므로, DevOn SAGA는 신뢰성 있는 메시징 장치를 함께 제공합니다.

  • 이벤트 발행 기능: 오케스트레이터가 각 서비스에 "이 작업을 수행하라"는 메시지를 보내고, 서비스는 처리 결과를 응답으로 돌려줍니다.
  • Outbox 관리: 4장에서 본 이중 쓰기 문제를 막아 주는 기능입니다. 업무 데이터 변경과 보낼 메시지를 같은 트랜잭션으로 함께 저장한 뒤 안전하게 발행하므로, "데이터는 바뀌었는데 메시지는 안 나간" 상황이 생기지 않습니다.
  • Inbox 관리: 받는 쪽에서 들어온 메시지를 기록해 두는 기능입니다. 어떤 메시지를 이미 처리했는지 추적할 수 있습니다.
  • 중복 수신 방지: 메시지는 같은 것이 두 번 이상 도착할 수 있습니다(최소 1회 전달). Inbox에 기록된 정보를 이용해 이미 처리한 메시지는 다시 처리하지 않도록 걸러 줍니다. 같은 요청이 여러 번 와도 결과가 한 번 처리한 것과 같아지는 멱등 처리가 자연스럽게 보장됩니다.

이 네 가지가 맞물려, 개발자가 신경 쓰지 않아도 메시지가 어긋남 없이 안전하게 오가도록 해 줍니다.

운영 관리를 통한 모니터링

분산 환경에서 가장 답답한 순간은 "지금 무슨 일이 벌어지고 있는지 모를 때"입니다. DevOn SAGA는 운영 관리 기능을 통해, 진행 중인 SAGA와 실패한 SAGA의 상태를 모니터링할 수 있게 해 줍니다. 어디까지 진행됐고 어디서 멈췄는지를 눈으로 확인할 수 있어, 장애 대응과 운영이 한결 수월해집니다.

실패를 복구하기: 보상 실패 시 재처리

SAGA에서 실패가 나면 이미 완료한 단계를 보상으로 되돌린다고 했습니다. 그런데 그 보상 작업 자체도 네트워크 문제 등으로 실패할 수 있습니다. DevOn SAGA는 앞서 언급한 운영 관리의 모니터링 기능을 통해 관리자가 상태를 확인하여 오류 원인을 파악하고 조치하여 재처리를 할 수 있는 기능을 제공합니다.

화면과 편리한 연결 지원: async to sync 기능

SAGA는 본질적으로 여러 단계를 거치는 비동기 흐름입니다. 하지만 사용자 화면 입장에서는 "버튼을 눌렀으면 결과가 나올 때까지 기다렸다가 성공/실패를 바로 보여 주는" 동기 방식이 훨씬 자연스럽습니다.

DevOn SAGA의 async to sync 기능은 이 간극을 메워 줍니다. 내부적으로는 비동기로 여러 서비스를 오가며 SAGA가 진행되지만, 화면에서 보낸 요청은 SAGA가 끝날 때까지 기다렸다가 최종 결과를 한 번에 응답으로 받습니다. 개발자와 사용자 입장에서는 마치 평범한 동기 호출 하나를 한 것처럼 보이므로, 비동기 흐름을 직접 다루는 복잡함 없이 SAGA의 장점을 누릴 수 있습니다.

6. 다른 선택지와의 비교: LRA 패턴

SAGA를 구현하는 또 다른 표준 기반 접근으로 LRA(Long Running Actions) 가 있습니다.

LRA란

LRA는 Eclipse 재단 소속의 MicroProfile에서 주도하고 있는 오케스트레이션 기반 SAGA 구현입니다. 중앙의 LRA 코디네이터에 각 서비스(참여자)가 등록(enlist)하고, 참여자는 @Compensate(되돌리기)·@Complete(완료) 어노테이션이 붙은 REST 엔드포인트를 미리 정의해 둡니다. 작업이 성공으로 닫히면 코디네이터가 각 참여자의 @Complete를, 실패로 취소되면 @Compensate를 호출합니다.

JTA의 @Transactional이 단일 트랜잭션을 어노테이션으로 다루듯 LRA는 서비스 간 SAGA를 어노테이션으로 다룬다고 보면 됩니다. 표준 사양이라는 점, 자바가 아닌 이기종 외부 서비스도 REST로 참여시킬 수 있다는 점이 강점입니다.

LRA와 DevOn SAGA의 통신 방식 비교

[LRA와 DevOn SAGA의 통신 방식 비교]

DevOn SAGA와의 차이

둘 다 "중앙 코디네이터가 지휘하는 오케스트레이션 SAGA"지만, 서비스를 잇는 방식이 다릅니다. LRA는 코디네이터가 참여자의 REST 콜백을 호출하는 HTTP 기반 구조이고, DevOn SAGA는 메시지를 매개로 한 비동기 구조입니다. DevOn SAGA는 4·5장에서 본 Outbox·Inbox로 이중 쓰기 문제 방지와 중복 수신 차단(멱등 처리)을 함께 제공해, 상대 서비스가 잠시 응답하지 못해도 흐름이 끊기지 않고 결합도가 낮습니다.

이 차이는 자원 부하에서도 드러납니다. LRA는 등록·완료·보상·상태 조회가 모두 HTTP 요청이라, 참여자나 동시 진행 SAGA가 늘면 커넥션·스레드 자원에 부담이 쌓입니다. LRA에도 비동기 옵션(202 Accepted 응답 등)이 있어 응답 대기 점유는 줄일 수 있지만, 전송 자체는 여전히 HTTP라서 코디네이터의 주기적 상태 조회·재호출이 새 트래픽으로 더해질 뿐 부하가 사라지진 않습니다. 반면 메시지 기반 구조는 발행 후 커넥션을 점유하지 않아, 동시 처리량이 중요한 금융 환경에서 유리합니다. (이는 LRA 사양의 결함이 아니라 HTTP 전송 방식의 일반적 특성입니다.)

구분 LRA DevOn SAGA
방식 오케스트레이션 SAGA 오케스트레이션 SAGA
서비스 연결 코디네이터의 REST 콜백(HTTP) 메시지 기반 비동기
자원 부하 전송이 HTTP라 커넥션 점유로 인한 자원 부하 발생 발행 후 커넥션 비점유, 동시 처리에 유리
신뢰성 메시징 사양은 메시징 인프라를 다루지 않음 Outbox·Inbox·중복 수신 방지 기본 제공
보상 실패 복구 구현체에 따라 다름 관리 화면에서 재처리 실행
운영 모니터링 구현체/별도 구성에 의존 진행·실패 SAGA 모니터링 제공
화면 연동 별도 설계 필요 async to sync 제공
강점 표준 사양, 이기종 외부 서비스 연동 Spring 기반 일괄 제공, 운영 편의 기능

어느 쪽이 금융 시스템에 더 맞을까

LRA는 표준이라는 분명한 강점이 있고, 다양한 언어·플랫폼의 외부 서비스를 엮어야 할 때 가치가 있습니다. 다만 표준 사양은 "무엇을 보장할지"를 규정할 뿐, 신뢰성 메시징·보상 재처리·모니터링·화면 동기 연동 같은 실무 요소는 구현체와 추가 구성에 맡겨져 개발팀이 직접 채워야 하는 부담이 남습니다.

DevOn SAGA는 이런 실무 요소를 프레임워크 차원에서 묶어 제공합니다. 덕분에 개발자는 기반 작업을 매번 새로 만들지 않고 업무 흐름과 보상 설계에 집중할 수 있어, 안정성과 운영 가시성이 중요한 국내 금융·Spring 기반 환경에서 이점이 큽니다.

7. SAGA 외의 보상 기능: 동기보상과 트랜잭션 이력 기반 보상

지금까지는 SAGA를 중심으로 살펴봤지만, 현실의 모든 업무가 SAGA로 깔끔하게 풀리는 것은 아닙니다. SAGA로 구현하기 어려운 상황이나, 보상 로직을 일일이 만들기 까다로운 업무도 있습니다. DevOn Boot는 이런 경우를 위해 두 가지 보완 기능을 함께 제공합니다.

동기 API 자동보상

업무 특성상 서비스 간 처리를 비동기가 아니라 동기 HTTP 호출로 해야 하는 경우가 있습니다. 이럴 때는 보상도 동기적으로 이뤄져야 하는데, 이를 SAGA로 구성하기는 쉽지 않습니다. 동기 API 자동보상은 바로 이런 업무를 위한 기능입니다.

사용 방식은 간단합니다. 개발자가 원래의 API(원 API)와 그에 대응하는 보상 API의 정보를 XML로 등록해 두면, 그다음은 프레임워크가 맡습니다. 프레임워크는 동기 API 호출을 가로채(Interceptor·AOP) 호출 이력을 기록하다가, 처리 중 오류가 발생하면 그때까지 정상 처리된 호출들에 대해 등록된 보상 API를 호출 역순으로 자동 실행합니다.

동기 API 자동보상

[동기 API 자동보상]

개발자는 보상의 "내용"(어떤 API가 무엇을 되돌리는지)만 정의하면 되고, "언제 어떤 순서로 보상을 호출할지"는 프레임워크가 처리해 줍니다. 만약 보상 API 호출이 실패하면 오류 내역이 남고, 운영 관리 화면에서 운영자가 직접 재처리할 수 있습니다.

트랜잭션 이력 기반 보상처리

보상 API를 따로 만들기 어렵거나, 되돌려야 할 데이터 변경이 많아 보상 로직을 일일이 구현하기 부담스러운 업무도 있습니다. 트랜잭션 이력 기반 보상처리는 이런 경우에 보상 로직을 직접 만들지 않고도 데이터를 되돌릴 수 있게 해 주는 기능입니다.

원리는 이렇습니다. 프레임워크가 업무 쿼리를 가로채(MyBatis Interceptor) 쿼리 수행 전과 후의 데이터를 이력으로 기록해 둡니다. 이후 보상이 필요해지면, 이 이력을 거꾸로 적용해 데이터를 원래대로 되돌립니다.

트랜잭션 이력 기반 보상처리

[트랜잭션 이력 기반 보상처리]

되돌리는 방식은 직관적입니다. Insert였던 작업은 Delete로, Delete였던 작업은 기록해 둔 이전 데이터로 다시 Insert해서, Update였던 작업은 변경 전 값으로 되돌려서 보상합니다. 즉 CUD(생성·수정·삭제)를 역으로 쳐서 보상하는 것입니다. 개발자가 각 작업의 보상 로직을 작성하지 않아도, 기록된 이력만으로 프레임워크가 반대 쿼리를 만들어 실행합니다.

한 가지 주의할 점은, 보상하려는 데이터가 그사이 다른 업무에 의해 바뀌었을 수 있다는 것입니다. DevOn Boot는 이를 위해 Versioning 기능을 제공해, 보상 직전 데이터가 기록된 시점과 달라졌는지 확인하고, 이미 변경된 경우에는 잘못된 보상을 막습니다.

정리

두 기능은 SAGA를 대체하는 것이 아니라, SAGA로 다루기 어려운 영역을 메우는 보완 장치입니다. 동기 호출 기반 업무에는 동기 API 자동보상이, 보상 로직 구현이 부담스러운 업무에는 트랜잭션 이력 기반 보상처리가 선택지가 됩니다. 이처럼 DevOn Boot는 SAGA와 더불어 다양한 보상 방식을 함께 갖추고 있어, 금융 업무의 여러 상황에 맞춰 데이터 정합성을 지킬 수 있도록 돕습니다.

마치며

클라우드와 컨테이너 환경이 성숙하며 MSA가 표준처럼 자리 잡았지만, 그 대가로 하나의 애플리케이션 안에서 당연하던 트랜잭션 정합성이 서비스 경계를 넘는 순간 사라졌습니다. 강한 정합성을 보장하는 2PC는 성능·가용성의 벽에 부딪혔고, 그 대안으로 1987년의 아이디어인 SAGA가 되살아났습니다. SAGA는 즉시 정합성 대신 성능·가용성과 최종적 정합성을 맞바꾼 현실적인 해답입니다.

다만 SAGA는 "보상만 만들면 끝"이 아닙니다. 보상은 직접 설계해야 하는 의미적 역연산이고, 격리성 대책·단계 분류·Outbox·Inbox·상태 관리까지 맞물려야 안전하게 동작하는 종합적인 설계 영역입니다. 이 모든 것을 매번 직접 구현하기는 부담스럽습니다.

LG CNS의 DevOn Boot는 오케스트레이션 방식의 SAGA를 프레임워크 차원에서 제공해, 상태 관리, Outbox·Inbox 기반의 안전한 메시징, 보상 실패 시 운영 화면을 통한 재처리, 모니터링, async to sync까지 한데 갖추고 있습니다. 또한 SAGA로 다루기 어려운 영역을 위한 동기 API 자동보상과 트랜잭션 이력 기반 보상처리까지 더해, 다양한 업무 상황에 맞춰 정합성을 지킬 수 있도록 돕습니다. 안정성과 운영 가시성이 중요한 금융 환경에서 특히 강점이 됩니다.

참고 자료

  • Hector Garcia-Molina, Kenneth Salem, "Sagas", Proceedings of the 1987 ACM SIGMOD International Conference on Management of Data (1987)
  • Chris Richardson, 『Microservices Patterns』 (2018) — Saga 패턴 및 격리성 대책(Chapter 4)
  • microservices.io — Pattern: Saga, Pattern: Transactional outbox
  • Microsoft Learn / Azure Architecture Center — Saga distributed transactions pattern
  • AWS Prescriptive Guidance — Transactional outbox pattern
  • Netflix Open Source (Zuul, Eureka, Hystrix) 및 Spring Cloud Netflix 공개 문서
  • Eclipse MicroProfile LRA (Long Running Actions) 사양 및 Narayana LRA 구현 문서