실패와 마주하며 깨달은 MSA의 그림자: 트랜잭션, 왜 그렇게 어려울까?
나는 왜 사가를 쓰는가? 100번의 고민 끝에 내린 결론: MSA 트랜잭션과의 사투
마이크로서비스 아키텍처(MSA), 유연성과 확장성을 약속하는 매력적인 구조입니다. 저 역시 한때 MSA에 대한 장밋빛 환상에 젖어 프로젝트에 도입했지만, 현실은 녹록지 않았습니다. 특히 데이터 일관성을 유지하는 트랜잭션 문제는 끊임없이 발목을 잡았습니다. 마치 잘 지은 모래성이 파도에 무너져내리듯, MSA 환경에서의 트랜잭션 관리는 생각보다 훨씬 복잡하고 까다로운 문제였습니다.
MSA 도입, 그리고 예상치 못한 데이터 불일치
MSA로 전환하면서 저희는 각 서비스가 자체 데이터베이스를 가지게 되었습니다. 초기에는 각자도생하는 듯한 모습에 만족했습니다. 하지만 곧 문제가 터져 나왔습니다. 예를 들어, 사용자가 상품을 주문하면 주문 서비스, 결제 서비스, 재고 서비스가 연쇄적으로 업데이트되어야 합니다. 그런데 만약 결제 서비스에서 오류가 발생하면 어떻게 될까요? 주문은 생성되었지만 결제는 실패하고, 재고는 그대로 남는 데이터 불일치 상황이 발생하는 겁니다.
로컬 트랜잭션으로는 이 문제를 해결할 수 없었습니다. 각 서비스는 독립적인 데이터베이스를 사용하기 때문에, 하나의 트랜잭션으로 묶을 수 없었기 때문입니다. 글로벌 트랜잭션(XA)이라는 대안도 있었지만, 성능 저하라는 치명적인 단점 때문에 쉽게 선택할 수 없었습니다. 결국, 분산 트랜잭션의 복잡한 세계에 발을 들여놓게 된 것이죠.
삽질의 연속, 그리고 사가(Saga)와의 만남
분산 트랜잭션을 구현하기 위해 2PC(Two-Phase Commit)나 3PC(Three-Phase Commit) 등 다양한 방법을 시도해봤지만, 만족스러운 결과를 얻지 못했습니다. 복잡한 설정, 성능 문제, 그리고 무엇보다 장애 발생 시 복구 과정이 너무나 어려웠습니다. 밤샘 작업은 일상이었고, 데이터 불일치로 인한 고객 불만은 끊이지 않았습니다.
그러던 중 우연히 사가(Saga) 패턴에 대해 알게 되었습니다. 사가는 여러 로컬 트랜잭션을 연결하여 하나의 분산 트랜잭션을 구현하는 방식입니다. 각 서비스는 자신의 트랜잭션을 수행하고, 다음 서비스에게 이벤트를 전달합니다. 만약 실패가 발생하면, 이전 단계의 트랜잭션을 보상하는 트랜잭션(Compensating Transaction)을 실행하여 전체 시스템의 일관성을 유지합니다.
처음에는 사가 패턴 역시 복잡하고 어렵게 느껴졌습니다. 하지만 기존 방식의 한계를 절감하고 있었기에, 사가를 깊이 파고들기 시작했습니다. 그리고 수많은 시행착오 끝에, MSA 환경에서 트랜잭션 관리를 위한 가장 현실적인 대안은 사가라는 결론에 도달했습니다.
다음 섹션에서는 제가 사가를 도입하면서 겪었던 구체적인 경험과, 사가 패턴의 장단점, 그리고 실제 구현 사례를 통해 MSA 환경에서 트랜잭션을 어떻게 효과적으로 관리할 수 있는지 자세히 이야기해보겠습니다.
사가, 이론만으론 부족하다! 10가지 패턴 직접 써보며 얻은 교훈
나는 왜 사가를 쓰는가? 100번의 고민 끝에 내린 결론: 사가, 이론만으론 부족하다! 10가지 패턴 직접 써보며 얻은 교훈 (2/3)
지난 글에서 사가 패턴 도입의 필요성을 절감하고, 무작정 코드를 치기 시작했던 저의 좌충우돌 스토리를 털어놓았습니다. 하지만 책상에 앉아 이론만 파고드는 것과 실제 서비스에 적용하는 것은 천지차이더군요. 이번 글에서는 제가 직접 겪었던 시행착오를 바탕으로, 다양한 사가 패턴을 어떻게 활용했고, 어떤 교훈을 얻었는지 좀 더 구체적으로 풀어보겠습니다.
보상 트랜잭션, 생각보다 복잡하네?
가장 먼저 시도했던 건 보상 트랜잭션 (Compensation Transaction) 패턴이었습니다. 하나의 트랜잭션이 실패했을 때, 이전 트랜잭션을 롤백하는 방식으로, 비교적 직관적이라고 생각했죠. 예를 들어, 온라인 쇼핑몰에서 주문, 결제, 재고 차감, 배송 시작의 단계를 거친다고 가정해봅시다. 배송 시작 단계에서 문제가 발생하면, 재고를 다시 늘리고, 결제를 취소하는 보상 트랜잭션을 구현하는 것이죠.
처음에는 간단한 시나리오만 생각하고 코딩했지만, 예외 상황이 꼬리에 꼬리를 물고 나타났습니다. 결제 취소 자체가 실패하는 경우, 재고 증가 과정에서 다른 주문과 충돌이 발생하는 경우 등 예상치 못한 문제들이 튀어나왔습니다. 결국, 각 단계별로 보상 트랜잭션의 성공 여부를 추적하고, 재시도 로직을 추가해야 했습니다. 마치 퍼즐 조각을 하나씩 맞춰가는 기분이었죠.
채널 분리, 춤추듯 협업하는 서비스들
다음으로 도전한 것은 코레오그래피 (Choreography) 패턴이었습니다. 중앙 집중형 오케스트라 지휘자 없이, 각 서비스가 이벤트를 발행하고 구독하며 자율적으로 동작하는 방식이죠. 마이크로서비스 아키텍처 환경에서 유용하다고 판단했습니다.
초기에는 마치 오케스트라처럼 서비스들이 유기적으로 연결되어 동작하는 모습에 감탄했습니다. 하지만 시간이 지날수록 문제점이 드러나기 시작했습니다. 각 서비스 간의 의존성이 높아지면서, 하나의 서비스 변경이 다른 서비스에 연쇄적인 영향을 미치는 경우가 발생한 것이죠. 마치 도미노처럼 시스템 전체가 흔들리는 아찔한 경험도 했습니다.
이 문제를 해결하기 위해 이벤트 스키마를 명확하게 정의하고, 각 서비스 간의 계약을 문서화하는 데 많은 시간을 투자했습니다. 또한, 모니터링 시스템을 강화하여 이벤트 흐름을 실시간으로 추적하고, 이상 징후를 빠르게 감지할 수 있도록 했습니다.
경험에서 우러나온 교훈
이처럼 다양한 사가 패턴을 직접 적용하면서 얻은 가장 큰 교훈은, 이론적인 지식만으로는 부족하다는 것입니다. 실제 서비스 환경은 예측 불가능한 변수들로 가득하며, 각 패턴의 장단점을 명확하게 이해하고, 상황에 맞는 최적의 패턴을 선택하는 것이 중요합니다.
물론, 모든 문제를 사가 패턴으로 해결할 수 있는 것은 아닙니다. 때로는 전통적인 트랜잭션 관리 방식이 더 효율적일 수도 있습니다. 중요한 것은 문제 해결을 위한 다양한 도구를 이해하고, 상황에 맞게 적절하게 활용하는 능력을 키우는 것이라고 생각합니다.
다음 글에서는 제가 사가 패턴을 적용하면서 겪었던 구체적인 문제점들을 공유하고, 어떻게 해결했는지 자세히 풀어보겠습니다. 또한, 사가 패턴 도입을 고려하는 개발자들에게 실질적인 도움이 될 만한 팁들을 아낌없이 제공할 예정입니다.
사가는 만능 해결사가 아니다: Trade-off와 한계를 인정하기
사가는 만능 해결사가 아니다: Trade-off와 한계를 인정하기 (2/3)
지난 글에서 사가 패턴 도입의 빛나는 순간들을 이야기했지만, 솔직히 말해 사가는 만능 해결사가 아닙니다. 오히려 도입 후 아, 이거 생각보다 복잡하네? 하는 순간들이 꽤 있었죠. 마치 엄청난 성능을 자랑하는 스포츠카를 샀는데, 좁은 골목길에서는 옴짝달싹 못하는 상황과 비슷하다고 할까요?
성능 저하, 눈물의 측정 데이터
가장 먼저 체감했던 문제는 성능 저하였습니다. 분산 트랜잭션을 처리하기 위해 여러 서비스 간 통신이 빈번해지니, 당연한 결과였죠. 단순히 이론적으로만 알고 있던 내용을, 실제 서비스에 적용 후 측정 데이터를 보니 더욱 뼈저리게 느껴졌습니다. 특정 API의 응답 시간이 평균 20% 이상 늘어나는 것을 확인했을 때는, 밤새도록 튜닝했던 기억이 떠오르네요.
저는 이런 성능 저하를 극복하기 위해 여러 가지 시도를 했습니다. 메시지 큐 최적화, 불필요한 데이터 전송 줄이기, 캐싱 전략 도입 등등… 그중 가장 효과적이었던 방법은 비동기 처리였습니다. 모든 단계를 동기적으로 처리하는 대신, 필요에 따라 비동기적으로 처리하도록 변경하여 전체적인 흐름을 개선할 수 있었습니다. 하지만 이 과정에서 또 다른 문제가 발생했습니다. 바로 일관성 문제였죠.
일관성, 예측 불허의 상황들
비동기 처리 방식을 도입하면서, 데이터 일관성을 유지하는 것이 더욱 어려워졌습니다. 예를 들어, 주문 서비스에서 결제 서비스로의 통신이 실패했을 때, 주문은 생성되었지만 결제는 이루어지지 않는 상황이 발생할 수 있습니다. 이러한 문제를 해결하기 위해 사가아기띠워머 멱등성을 보장하는 코드를 작성하고, 모니터링 시스템을 강화하여 예외 상황을 빠르게 감지하고 대응할 수 있도록 했습니다.
하지만 완벽한 일관성을 보장하는 것은 현실적으로 불가능했습니다. 결국 최종적 일관성(Eventual Consistency)을 받아들이고, 사용자에게 발생 가능한 문제 상황을 미리 고지하고 양해를 구하는 방향으로 선회했습니다. (물론, 핵심적인 데이터는 최대한 빠르게 일관성을 유지하도록 노력했습니다.)
모니터링, 복잡도의 정점
사가 패턴은 여러 서비스에 걸쳐 로직이 분산되기 때문에, 모니터링 또한 매우 복잡해집니다. 각 서비스의 로그를 일일이 확인하는 것은 비효율적이죠. 그래서 저는 중앙 집중식 로깅 시스템과 분산 추적 시스템을 구축하여 전체적인 흐름을 한눈에 파악할 수 있도록 했습니다. 또한, 사가 패턴의 각 단계를 시각적으로 보여주는 대시보드를 만들어, 문제 발생 시 빠르게 원인을 파악할 수 있도록 했습니다.
이러한 노력에도 불구하고, 완벽한 모니터링 시스템을 구축하는 것은 여전히 어려운 과제입니다. 예상치 못한 예외 상황이 발생하거나, 특정 서비스의 장애가 전체 시스템에 미치는 영향을 정확하게 예측하기 어렵기 때문입니다.
결론적으로, 사가 패턴은 분명 강력한 도구이지만, 모든 문제를 해결해 주는 만능 해결사는 아닙니다. 성능 저하, 일관성 문제, 모니터링 어려움 등 다양한 Trade-off를 고려해야 하며, 상황에 따라 다른 기술과의 조합을 고려해야 합니다. 다음 글에서는 사가의 한계를 극복하기 위한 구체적인 전략과, 사가 패턴과 함께 사용하면 좋은 기술들을 소개하겠습니다.
그래서 나는 사가를 쓰는가? 100번의 고민 끝에 얻은 나만의 해답
나는 왜 사가를 쓰는가? 100번의 고민 끝에 내린 결론: 나만의 해답 (完)
지난 몇 번의 연재를 통해 사가를 도입하기 전 겪었던 좌충우돌과, 실제로 프로젝트에 적용하며 느꼈던 희로애락을 솔직하게 풀어놓았습니다. 마치 숙제를 끝내듯 속 시원하면서도, 한편으로는 그래서 사가를 써야 해, 말아야 해?라는 질문에 명쾌한 답을 드리지 못한 것 같아 마음 한구석이 찜찜했습니다. 그래서 오늘은, 100번의 고민 끝에 얻은 저만의 결론, 즉 나는 왜 사가를 쓰는가?에 대한 최종 해답을 공유하고자 합니다.
사가, 언제 써야 할까? 결국 선택의 문제
결론부터 말씀드리자면, 사가는 만병통치약이 아닙니다. 모든 상황에 적용 가능한 정답이 아니라, 여러 선택지 중 하나일 뿐입니다. 저는 다음과 같은 상황에서 사가를 선택하는 것이 효율적이라고 판단했습니다.
- 복잡한 트랜잭션, 롤백이 생명일 때: 여러 마이크로서비스에 걸쳐 데이터 정합성을 유지해야 하고, 실패 시 롤백이 필수적인 경우, 사가는 빛을 발합니다. 예를 들어, 온라인 쇼핑몰에서 주문, 결제, 배송 서비스가 연동되어 있을 때, 하나의 서비스에서 오류가 발생하면 전체 주문을 취소하고 결제를 환불해야 합니다. 이때 사가를 사용하면 각 서비스의 상태를 추적하고, 오류 발생 시 역보상 트랜잭션을 실행하여 데이터 정합성을 유지할 수 있습니다. 제가 진행했던 프로젝트에서는, 복잡한 금융 거래 시스템에서 자금 이체, 계좌 업데이트, 감사 로그 기록 등의 작업을 사가 패턴으로 구현하여 데이터 무결성을 확보했습니다.
- 장애 격리, 독립적인 서비스 운영: 특정 서비스의 장애가 전체 시스템에 영향을 미치지 않도록 격리하고 싶을 때, 사가는 좋은 선택입니다. 각 서비스는 독립적으로 동작하며, 사가 코디네이터를 통해 트랜잭션 상태를 관리합니다. 따라서 하나의 서비스에 문제가 발생하더라도, 다른 서비스는 정상적으로 운영될 수 있습니다.
사가 도입 전, 반드시 고려해야 할 사항
물론, 사가를 도입하기 전에 반드시 고려해야 할 사항들이 있습니다. 가장 중요한 것은 복잡성입니다. 사가는 분산 시스템의 복잡성을 증가시키고, 디버깅을 어렵게 만들 수 있습니다. 따라서 프로젝트의 규모, 복잡도, 개발팀의 숙련도 등을 종합적으로 고려하여 사가 도입 여부를 결정해야 합니다. 또한, 사가 패턴은 결국 최종 일관성을 보장하는 방식이기 때문에, 강한 일관성이 필요한 경우에는 적합하지 않을 수 있습니다.
사가 외의 대안, 그리고 미래
사가 외에도 분산 트랜잭션을 관리하는 다양한 방법이 있습니다. 2PC (Two-Phase Commit)나 TCC (Try-Confirm-Cancel) 같은 방법도 고려해 볼 수 있습니다. 하지만 저는 개인적으로 사가의 유연성과 확장성을 높이 평가합니다. 앞으로는 사가 패턴을 더욱 발전시켜, 다양한 비즈니스 요구사항에 맞춰 유연하게 적용할 수 있도록 노력할 것입니다. 특히, 이벤트 소싱, CQRS (Command Query Responsibility Segregation) 등 다른 아키텍처 패턴과 결합하여 더욱 강력한 시스템을 구축하는 데 관심을 가지고 있습니다.
결론: 사가는 선택이다
결국, 사가는 정답이 아니라 선택입니다. 프로젝트의 특성과 요구사항을 고려하여, 가장 적합한 방법을 선택하는 것이 중요합니다. 저는 앞으로도 다양한 기술들을 경험하고, 끊임없이 고민하며, 저만의 해답을 찾아나갈 것입니다. 그리고 그 과정에서 얻은 경험과 지식을 여러분과 공유하며 함께 성장하고 싶습니다. 긴 글 읽어주셔서 감사합니다.
꿈꿔왔던 사가, 현실은 스파게티 지옥?
코드 퀄리티 폭망 직전! 사가 사용, 이렇게 하니 오히려 독이 되더라
꿈꿔왔던 사가, 현실은 스파게티 지옥?
마이크로서비스 아키텍처(MSA) 도입, 많은 개발자들이 꿈꾸는 이상적인 구조일 겁니다. 저 역시 그랬습니다. 각 서비스는 독립적으로 개발, 배포되고 특정 서비스의 장애가 전체 시스템에 미치는 영향을 최소화할 수 있다는 점이 매력적이었죠. 하지만 꿈은 꿈일 뿐, 현실은 냉혹했습니다. 특히 분산 트랜잭션 처리라는 난관에 부딪히면서 사가(Saga)라는 이름의 구원투수를 등판시켰지만, 결과는 참담했습니다. 마치 잘 끓인 줄 알았던 스프에서 머리카락 한 올이 발견된 듯한 기분이랄까요?
저희 팀은 MSA 전환 초기, 서비스 간 데이터 정합성을 유지하기 위해 사가 패턴을 적극적으로 활용하기로 했습니다. 이론적으로는 각 서비스의 로컬 트랜잭션을 묶어 하나의 큰 트랜잭션처럼 관리하고, 실패 시 보상 트랜잭션을 통해 롤백하는 방식이었죠. 문제는 바로 초기 설계 미흡이었습니다.
처음에는 간단한 시나리오에만 집중했습니다. 예를 들어, 주문 생성 사가의 경우, 주문 서비스에서 주문을 생성하고, 결제 서비스에서 결제를 진행한 후, 재고 서비스에서 재고를 차감하는 순서로 진행되는 흐름을 생각했죠. 각 서비스는 메시지 큐를 통해 서로 통신하며, 실패 시 보상 트랜잭션을 호출하도록 구현했습니다.
하지만 시간이 지나면서 복잡한 비즈니스 요구사항들이 쏟아져 나오기 시작했습니다. 주문 취소, 부분 환불, 배송지 변경 등 다양한 시나리오가 추가되면서 사가의 복잡도는 기하급수적으로 증가했습니다. 각 서비스는 서로 얽히고 설킨 메시지로 가득 찼고, 사가의 흐름을 한눈에 파악하기 어려워졌습니다. 마치 스파게티 면처럼 꼬여버린 코드를 보면서 한숨만 나왔습니다.
가장 큰 문제는 보상 트랜잭션이었습니다. 각 서비스의 보상 트랜잭션은 데이터의 일관성을 유지하기 위해 신중하게 설계되어야 했습니다. 하지만 시간이 부족하다는 핑계로 보상 트랜잭션을 제대로 구현하지 않았고, 결국 데이터 불일치 문제가 빈번하게 발생했습니다. 밤새도록 데이터 복구 작업을 해야 했던 날들이 떠오르네요.
결과적으로 사가 패턴을 도입했음에도 불구하고, 분산 트랜잭션 관리는 더욱 어려워졌고, 코드 퀄리티는 눈에 띄게 저하되었습니다. 새로운 기능을 추가하거나 버그를 수정할 때마다 전체 시스템에 미치는 영향을 고려해야 했고, 개발 속도는 점점 느려졌습니다. 이상과 현실의 괴리를 뼈저리게 느꼈습니다.
다음 섹션에서는 저희 팀이 겪었던 구체적인 문제점과 함께, 이를 해결하기 위해 어떤 노력을 기울였는지 자세히 공유하도록 하겠습니다.
삽질 연대기: 왜 사가는 우리의 기대를 배신했을까?
삽질 연대기: 왜 사가는 우리의 기대를 배신했을까? (2) 코드 퀄리티 폭망 직전! 사가 사용, 이렇게 하니 오히려 독이 되더라
지난 글에서 사가 패턴 도입의 낭만적인 꿈과 현실의 괴리를 이야기했습니다. 이번에는 그 꿈이 어떻게 악몽으로 변해갔는지, 코드 퀄리티를 나락으로 떨어뜨린 주범이 되었는지 낱낱이 파헤쳐 보겠습니다. 솔직히 말해서, 그때의 저는 사가를 너무 쉽게 봤습니다. 보상 트랜잭션? 그거 그냥 역순으로 호출하면 되는 거 아냐? 라는 안일한 생각이 모든 문제의 시작이었죠.
제가 몸담았던 팀은 MSA(Microservices Architecture) 환경에서 결제 사가를 구축하기로 했습니다. 얼핏 보면 간단해 보였습니다. 결제 -> 재고 차감 -> 배송 시작. 롤백은 배송 취소 -> 재고 복구 -> 결제 취소. 그런데, 현실은 훨씬 복잡했습니다.
가장 큰 문제는 바로 단순한 보상 트랜잭션이라는 착각에서 비롯됐습니다. 결제 서비스는 잘 돌아갔지만, 재고 서비스와의 연동에서 문제가 발생했습니다. 동시에 여러 사용자가 결제를 시도하면서 재고 차감 요청이 몰렸고, 결국 데드락이 발생한 겁니다. 저는 재고 서비스가 낙관적 락(Optimistic Lock)을 사용하고 있다는 사실을 간과했습니다. 사가 내에서 재시도 로직을 추가했지만, 근본적인 해결책은 아니었습니다.
또 다른 함정은 이벤트 메시지 불일치였습니다. 각 서비스에서 발생하는 이벤트 메시지의 형식이 통일되지 않다 보니, 사가 코디네이터에서 이벤트를 해석하고 처리하는 과정이 복잡해졌습니다. 심지어 어떤 이벤트는 누락되기도 했습니다. 결국 저는 각 서비스의 이벤트 메시지를 일일이 확인하고, 예외 처리 로직을 추가해야 했습니다. 코드는 점점 더 복잡해지고, 유지보수는 악몽이 되어갔죠.
결정적으로, 롤백 시나리오 부재는 저를 깊은 절망에 빠뜨렸습니다. 예상치 못한 오류가 발생했을 때, 어떻게 롤백해야 할지 제대로 정의하지 않았던 겁니다. 예를 들어, 배송 서비스에서 오류가 발생했을 때, 재고 복구와 결제 취소를 어떤 순서로 진행해야 할까요? 만약 재고 복구에 실패하면 어떻게 해야 할까요? 저는 이러한 예외 상황들을 고려하지 않았습니다.
결국, 저는 사가 패턴을 적용하면서 얻는 이점보다 관리해야 할 복잡성이 훨씬 더 크다는 사실을 깨달았습니다. 코드는 스파게티처럼 얽혀버렸고, 배포는 지옥이었습니다. 사가를 제대로 이해하지 못하고, 단순한 보상 트랜잭션만 고려한 안일한 설계가 낳은 참담한 결과였습니다. 이러한 경험을 통해 https://search.daum.net/search?w=tot&q=saga , 저는 사가 패턴이 만병통치약이 아니라는 것을 뼈저리게 느꼈습니다. 다음 글에서는 이 문제를 어떻게 해결했는지, 그리고 사가 패턴을 성공적으로 적용하기 위한 조건은 무엇인지 자세히 이야기해보겠습니다.
구원투수 등장! 리팩토링으로 사가, 다시 희망을 쏘다
코드 퀄리티 폭망 직전! 사가 사용, 이렇게 하니 오히려 독이 되더라 (2)
지난 글에서 사가 패턴 도입 초기, 걷잡을 수 없이 복잡해진 코드에 좌절했던 경험을 공유했습니다. 야심차게 도입한 사가가 오히려 독이 되어 돌아온 셈이었죠. 하지만 포기할 순 없었습니다. 문제의 근원을 파악하고, 리팩토링이라는 구원투수를 투입하기로 결심했습니다.
CQRS, 이벤트 소싱… 화려한 조연들의 등장, 그리고 실패
처음에는 CQRS(Command Query Responsibility Segregation) 패턴을 적용해 읽기/쓰기 책임을 분리하고, 이벤트 소싱을 통해 상태 변화를 추적하면 복잡성이 줄어들 거라 기대했습니다. 마치 화려한 조연들을 대거 투입하면 블록버스터 영화가 될 거라 믿는 감독처럼 말이죠.
하지만 현실은 달랐습니다. CQRS는 시스템 전체 구조를 바꿔야 하는 작업이었고, 이벤트 소싱은 예상보다 훨씬 많은 인프라 구축과 학습 비용을 요구했습니다. 게다가 사가 패턴의 복잡성과 맞물려 오히려 코드베이스는 더욱 혼란스러워졌습니다. 마치 엉킨 실타래를 더 엉키게 만든 꼴이었죠. 아, 이건 아니다… 싶었습니다.
핵심은 단순함이었다: Before & After 코드 비교
결국, 복잡한 패턴들을 섣불리 도입하는 대신, 사가 패턴 자체의 문제점을 파악하고 단순화하는 데 집중하기로 했습니다. 핵심은 각 트랜잭션 단계를 명확히 분리하고, 보상 트랜잭션을 체계적으로 관리하는 것이었습니다.
예를 들어, 기존에는 하나의 거대한 사가 클래스 안에 모든 트랜잭션 로직이 뒤섞여 있었습니다. 이를 각 트랜잭션 단계를 담당하는 작은 클래스로 분리하고, 각 클래스는 특정 비즈니스 로직만 수행하도록 변경했습니다. (아래는 가상의 코드 예시입니다.)
Before:
public class OrderSaga {
public void processOrder() {
// 주문 생성, 결제 처리, 재고 감소 등 복잡한 로직
if (/* 결제 실패 */) {
// 주문 취소, 결제 환불, 재고 복구 등 복잡한 보상 로직
}
}
}
After:
public class CreateOrderTransaction {
public void execute() { /* 주문 생성 로직 */ }
public void compensate() { /* 주문 취소 로직 */ }
}
public class PaymentTransaction {
public void execute() { /* 결제 처리 로직 */ }
public void compensate() { /* 결제 환불 로직 */ }
}
// ... (다른 트랜잭션 클래스들)
public class OrderSaga {
private List<Transaction> transactions;
public OrderSaga(List<Transaction> transactions) {
this.transactions = transactions;
}
public void processOrder() {
for (Transaction transaction : transactions) {
try {
transaction.execute();
} catch (Exception e) {
compensate(transaction);
<a href="https://www.sa-ga.kr/" target="_blank" id="findLink">saga</a> throw e; // or handle the exception appropriately
}
}
}
private void compensate(Transaction failedTransaction) {
// 역순으로 보상 트랜잭션 실행
for (int i = transactions.indexOf(failedTransaction) - 1; i >= 0; i--) {
transactions.get(i).compensate();
}
}
}
이처럼 트랜잭션 단계를 분리하고, 각 단계의 성공/실패에 따른 보상 로직을 명확히 정의함으로써 코드의 가독성과 유지보수성을 크게 향상시킬 수 있었습니다. 아, 이거였어! 마치 퍼즐 조각이 맞춰지는 듯한 느낌이었습니다.
테스트 자동화, 삽질을 막아주는 든든한 방패
리팩토링 과정에서 테스트 자동화의 중요성을 뼈저리게 느꼈습니다. 기존에는 수동 테스트에 의존했기 때문에, 작은 코드 변경에도 전체 시스템에 미치는 영향을 파악하기 어려웠습니다.
그래서 JUnit, Mockito 등을 활용하여 단위 테스트 및 통합 테스트를 자동화했습니다. 테스트 코드를 작성하는 과정은 고통스러웠지만, 자동화된 테스트는 코드 변경에 대한 두려움을 없애주고, 잠재적인 버그를 조기에 발견하는 데 큰 도움이 되었습니다. 마치 든든한 방패를 얻은 기분이었죠.
교훈: 사가는 만병통치약이 아니다
이번 리팩토링을 통해 얻은 가장 큰 교훈은 사가 패턴이 만병통치약이 아니라는 것입니다. 사가는 분산 트랜잭션 문제를 해결하는 강력한 도구이지만, 복잡성을 수반하며, 잘못 사용하면 오히려 시스템을 더 망칠 수 있습니다. 사가를 성공적으로 적용하기 위해서는 문제에 대한 깊이 있는 이해, 명확한 설계, 그리고 지속적인 리팩토링이 필수적입니다.
다음 글에서는 사가를 성공적으로 적용하기 위한 핵심 전략, 그리고 앞으로 개선해야 할 부분에 대해 더 자세히 이야기해 보겠습니다.
교훈과 다짐: 사가, 제대로 쓰면 약, 잘못 쓰면 독!
코드 퀄리티 폭망 직전! 사가 사용, 이렇게 하니 오히려 독이 되더라 (2)
지난 글에서 사가 패턴 도입의 빛과 그림자를 살짝 보여드렸죠. 오늘은 좀 더 깊숙이 들어가, 제가 직접 겪었던 뼈아픈 실패 사례를 바탕으로 사가를 독이 아닌 약으로 만드는 방법에 대해 이야기해볼까 합니다. 솔직히 말해서, 처음엔 저도 사가 만능론자였어요. 분산 트랜잭션? 복잡한 워크플로우? 사가면 다 해결! 이라고 외치고 다녔죠. 하지만 현실은… 처참했습니다.
설계 미스는 곧 재앙: 꼬리에 꼬리를 무는 보상 트랜잭션
가장 큰 문제는 설계 단계에서 발생했습니다. 마이크로서비스 아키텍처(MSA) 환경에서 주문, 결제, 배송 서비스를 연동하는 과정이었는데, 각 서비스의 상태 변화를 꼼꼼하게 정의하지 않고 사가를 도입한 게 화근이었죠. 예를 들어, 주문 서비스에서 주문 생성 후 결제 서비스에서 결제 실패가 발생했을 때, 주문 서비스의 상태를 취소로 변경하는 보상 트랜잭션을 구현해야 했습니다. 문제는 여기서부터 시작이었어요.
결제 실패의 원인이 다양하다는 것을 간과한 거죠. 카드 한도 초과, 은행 시스템 오류, 사용자 실수 등 다양한 이유로 결제가 실패할 수 있는데, 각각의 상황에 맞는 보상 트랜잭션을 제대로 설계하지 않았습니다. 결국, 특정 상황에서는 주문 취소가 제대로 이루어지지 않아 재고가 꼬이는 현상이 발생했고, 고객 불만으로 이어졌습니다.
더 큰 문제는, 이 보상 트랜잭션 자체가 또 다른 문제를 야기할 수 있다는 점이었어요. 주문 취소 과정에서 배송 서비스에 이미 배송 요청이 전달된 경우, 배송 취소 요청을 보내야 하는데, 이 과정에서 또 다른 오류가 발생할 가능성이 존재했습니다. 꼬리에 꼬리를 무는 보상 트랜잭션의 늪에 빠진 거죠. 마치 스파게티 코드를 보는 듯한 악몽 같은 경험이었습니다.
지속적인 코드 퀄리티 관리, 선택 아닌 필수
이러한 실패를 통해 얻은 교훈은 명확합니다. 사가는 만병통치약이 아니며, 오히려 잘못 사용하면 코드 퀄리티를 급격하게 떨어뜨리는 주범이 될 수 있다는 것입니다. 그렇다면 어떻게 해야 할까요?
첫째, 서비스 특성에 맞는 솔루션을 선택해야 합니다. 모든 상황에 사가가 적합한 것은 아닙니다. 때로는 분산 락, 2PC(Two-Phase Commit)와 같은 다른 기술이 더 효과적일 수 있습니다. 서비스의 복잡도, 트랜잭션의 중요도, 성능 요구사항 등을 종합적으로 고려하여 최적의 솔루션을 선택해야 합니다.
둘째, 꼼꼼한 설계는 기본입니다. 각 서비스의 상태 변화를 명확하게 정의하고, 발생 가능한 모든 예외 상황에 대한 보상 트랜잭션을 설계해야 합니다. 이 과정에서 도메인 전문가와의 협업은 필수적입니다.
셋째, 지속적인 코드 퀄리티 관리가 필요합니다. 사가 패턴은 복잡도가 높기 때문에, 코드 리뷰, 테스트 자동화, 모니터링 등을 통해 지속적으로 코드 퀄리티를 관리해야 합니다. 특히, 보상 트랜잭션의 정확성을 검증하는 것은 매우 중요합니다. 저는 개인적으로 테스트 주도 개발(TDD) 방식을 적극 활용하고 있습니다.
앞으로의 다짐: 더 나은 코드를 향하여
저는 이 실패를 통해 많은 것을 배웠습니다. 사가 패턴은 강력한 도구이지만, 신중하게 사용해야 한다는 것을 깨달았습니다. 앞으로는 서비스 특성에 맞는 최적의 솔루션을 선택하고, 꼼꼼한 설계를 통해 안정적인 시스템을 구축하는 데 집중할 것입니다. 그리고 무엇보다, 지속적인 코드 퀄리티 관리를 통해 더 나은 코드를 만들어 나갈 것을 다짐합니다. 여러분도 저와 함께 더 나은 개발자가 되기 위해 노력해봅시다!