최근 MSA 환경을 접하고 공부하면서 느꼈던 생각을 정리한 글입니다.
당연히 저는 MSA의 모든 내용을 공부하지 못했으며, MSA를 제대로 이해하고 있지 못합니다.
학습 과정에서 느낀 내용을 정리 차원에서 적은 글이니 가볍게 읽어주시면 감사하겠습니다.
(참고: 이 글은 책 - 마이크로서비스 아키텍처 구축의 내용을 많이 담고 있습니다.)
다양한 자료를 찾아보고 이를 공부하는 단계에서 제가 느낀 MSA의 특징은 모든건 자신의 환경에 맞게 선택하라
입니다.
물론 개발은 본질적으로 더 나은 기술에 대한 고민과 선택
이 동반되는 작업입니다. 하지만 지금까지 공부했던 내용은 어쩌면 선택
보다는 정답
에 가까운 내용이 많았습니다.
OOP를 준수하려면 SOLID원칙을 준수해야 해
중첩된 코드는 가독성이 좋지 못하니 개선해야 해
트랜잭션은 이렇게 동작하기 때문에 이러한 내용을 알고 사용해야 해
Spring에서 어떤 데이터를 처리하기 위해서는 어떤 기능을 써야 해
등등
하지만 MSA의 경우 A기술은 이런 경우에 좋고 B기술은 이런 경우에 좋다
의 내용이 많았습니다. 특히 정합성
과 고가용성
사이의 trade-off를 고려해야 하는 경우가 많았습니다.
그래서 MSA시스템을 설계할 때 자주 마주하는 선택사항들에 대해 정리해 봤습니다.
MSA의 도입 역시 하나의 선택
입니다.
모든 시스템이 반드시 MSA구조를 가져야 하는 건 아니며, MSA를 도입하기만 하면 기존 시스템의 여러 문제들이 마법처럼 해결되는 건 더더욱 아닙니다.
잘 키운 모노리스 하나 열 마이크로서비스 안 부럽다 (박용권) 에서는 오히려 MSA환경으로 만들어진 서비스를 모놀리식 구조로 변경했던 경험을 공유해 주시기도 합니다.
MSA는 서비스 사이에 느슨한 결합
을 가지게 하며 네트워크를 이용한 통신
으로 각 서비스들이 상호작용 하는 구조입니다. 이를 통해 하나의 모듈에서 발생한 장애가 전체 시스템의 장애로 전파되는 문제를 방지할 수 있으며(높은 회복 탄력성), 각 모듈별로 확장(Scale-out)을 진행할 수 있어 유연한 시스템 운영이 가능해 집니다.
하지만 MSA는 분산 트랜잭션에 대한 적절한 처리가 필요하며 네트워크를 이용한 통신은 단일 시스템의 메서드 호출이나 DB의 JOIN연산보다 성능이 좋지 못합니다.
결국 MSA시스템을 도입한다는 건 느슨한 결합
을 통한 고가용성
을 획득하기 위해 개발의 복잡성을 높이고 약간의 시스템 성능 저하를 감수하는 선택이라고 할 수 있습니다.
MSA를 도입한다면 어떤 방식으로 서비스들이 통신할지
를 선택해야 합니다. 대표적인 방법으로 요청 및 응답
과 이벤트 기반
이 있습니다.
요청 및 응답 방식은 하나의 마이크로 서비스가 다른 마이크로서비스에게 작업 요청
을 보내고 해당 요청에 대한 결과(응답)
를 받기를 기대하는 방식입니다.
이때 응답을 기다리는 동기식 블로킹
방식으로 작업을 진행할 수도 있고, 호출 수신 여부와 무관하게 계속 작업을 처리하고 있는 비동기식 논블로킹
방식으로 작업을 진행할 수도 있습니다.
동기 방식은 두 마이크로서비스 사이에 커넥션
이 생성됩니다. 해당 커넥션은 응답하는 마이크로서비스가 응답을 완료할 때까지 커넥션이 열린 상태로 유지됩니다. 이러한 방식은 예상치 못하게 커넥션이 끊어지면 문제가 발생할 수 있습니다.
비동기 방식은 통신하는 각자가 상대방을 알고
있습니다. 비동기 통신을 위해 webFlux가 아닌 메시지 브로커를 사용하더라도 전송되는 상대에 대한 정보
를 알고 있다면 이는 이벤트 기반
이 아니라 여전히 비동기 방식의 요청 및 응답
이 됩니다.
마이크로서비스는 다른 마이크로서비스에 수신 여부가 보장되지 않는 이벤트
를 발행합니다.
여기서 말하는 이벤트는 발생한 일에 대한 진술(Statement)
입니다. 이벤트의 발신자는 무엇을 할 지 결정하는 것을 수신자에게 맡기고 있습니다.
이벤트가 무언가를 해 달라는 요청
이 아니라, 자신이 어떤 일을 했다는 진술
이라는 점이 핵심입니다.
이벤트 발신자는 이벤트를 사용하려는 다른 마이크로서비스가 존재한다는 사실조차 인식하지 못합니다. 필요할 때 이벤트를 발행하면 그 책임을 다한 것
입니다. 이처럼 이벤트 발신자는 수신자를 알지 못하기 때문에 훤씬 더 느슨한 결합
이 가능해 집니다.
요청 및 응답을 비동기적으로 처리하는 방식
과 이벤트 기반 방식
은 모두 메시지 브로커
, 즉 카프카를 사용할 수 있습니다. 카프카는 큐 기반 브로커
와 토픽 기반 브로커
를 모두 제공합니다. 큐는 point to point로 하나의 메시지를 생산하고 하나의 컨슈머만 해당 메시지를 소비하는 1:1구조, 토픽은 pub-sub로 생성자가 생산한 메시지를 여러 컨슈머가 동시에 소비하는 1:N구조입니다. (일반적으로 이렇게 엄격한 구분을 진행하지는 않습니다)
이는 단순히 카프카
를 사용한다고 해서 이벤트 기반 방식
이 되는 건 아니란 얘기입니다. 핵심은 발신자가 수신자에 대한 정보
를 알고있는지, 메시지가 수신자에 대한 요청이 아닌 발신자의 상태를 진술
하는 것인지, 해야 할 행동에 대한 책임을 수신자
가 가지고 있는지 여부입니다.
더 느슨한 결합
을 원하면 요청 및 응답 방식보다 이벤트 기반 방식을 선택하는 게 좋습니다.
하지만 전체 시스템이 반드시 요청 및 응답 또는 이벤트 기반 중 하나만 선택해야 하는 건 아닙니다. 적절한 사용으로 둘을 혼합해 하나의 시스템을 구성해도 전혀 문제되지 않습니다.
느슨한 결합을 위해 메시지 브로커(카프카)의 사용을 선택했다면 어떤 데이터를 전달할지
선택해야 합니다. 가령 발신자가 어떤 데이터를 생성했다는 걸 알릴 때 ID만 전달하는 방법
과 자세한 이벤트를 전달하는 방법
중 선택을 고민할 수 있습니다.
ID만 전달하는 방법은 수신자가 자신의 역할을 수행하기 위해 발신자에게 Id를 기준으로한 추가적인 데이터를 요청(HTTP통신)
해야 합니다. 그렇기 때문에 수신자가 발신자를 알아야 한다는 추가적인 도메인 결합
이 생기며, 발신자가 많다면 순간적으로 수신자에게 요청이 몰리게
될 수 있습니다
자세한 이벤트를 전달하는 방법(가령 DTO를 발신하는 방법)은 발신자가 수신자의 존재를 알 필요가 없습니다. 그저 통에 담긴 데이터를 꺼내서 쓰면 되기 때문이죠. 결과적으로 ID만 전달하는 방법보다 더 느슨한 결합
을 달성할 수 있습니다.
하지만 이벤트와 관련된 데이터가 크다면 이 역시 부담될 수 있습니다. 또한 이벤트에 자세한 데이터를 포함하는 방식은 곧 외부 세계와의 약속이 되기 때문에 함부로 변경하지 않아야 합니다.
그리고 하나의 메시지 브로커를 여러 수신자가 구독 중이라면 수신자에게 필요하지 않은 정보가 노출돼 정보 은닉이 지켜지지 않을 수
있습니다.
이를 방지하기 위해 언제, 어떤 회원이(식별자) 무엇을 하여(행위) 어떤 변화(변화 속성)가 발생했는가
와 같이 일반화된 이벤트를 설계하고 활용하는 방법이 존재합니다. 또는 발신자는 여러 메시지 브로커에 값을 전달하고, 수신자는 자신이 얻으려는 메시지에 맞는 메시지 브로커를 수신하도록 하는 방법도 존재합니다. 하지만 이 경우 하나의 메시지 브로커에는 데이터를 넣었지만, 두 번째 메시지 브로커에 데이터를 넣는 과정에서 에러가 발생한다면 문제가 될 수 있습니다.
더 느슨한 결합
을 원한다면 ID만 전달하는 것보다 자세한 이벤트를 전달하는 방식을 선택하는 게 좋습니다.
MSA를 도입하면 데이터베이스 역시 도메인별로 분리되어 별도로 관리될 가능성이 큽니다. 그 결과 하나의 DB가 제공하는 트랜잭션을 이용할 수 없게 되므로 분산 환경에서 트랜잭션을 보장하는 방법에 대해 고민해야 합니다.
2단계 커밋(2PC)은 투표 단계와 커밋 단계로 나뉩니다. 투표 단계에서 중앙 조정자(coordinator)는 트랜잭션에 참여할 모든 워커(worker)에 연락해 상태 변경이 가능한지 여부를 확인 요청합니다. 모든 워커가 요청받은 상태 변경이 가능하다면 알고리즘은 커밋 단계로 넘어갑니다. 하나의 워커라도 요청받은 상태 변경이 불가능하다면 전체 연산은 그대로 종료됩니다.
2PC에서는 워커가 상태 변경이 가능하다는 걸 알려준 직후에 즉시 변경 사항이 적용되지 않습니다.
대신 워커는 미래의 어느 시점에 그 변경을 수행할 수 있음을 보장
하고 있습니다. 이러한 보장이 가능하려면 워커는 해당 레코드를 잠궈둬야
합니다. 이는 성능적인 측면에서 좋지 못하며, 작업의 소요시간이 길어질수록 자원을 더 오래 잠궈둬야 합니다.
또한 2PC는 개별 프로세스별로 커밋이 수행되는 시점이 다르다
는 문제가 존재하며 이로 인해 격리성
이 깨질 수 있습니다. 격리성이란 여러 트랜잭션이 간섭 없이 동시에 작동할 수 있음을 의미하고, 이를 위해 어떤 트랜잭션이 진행되는 과정에서의 중간 상태 변경을 다른 트랜잭션이 확인할 수 없어야 합니다. 하지만 2PC는 개별 프로세스별로 커밋이 수행되는 시점이 달라 전체 트랜잭션이 완료되지 않은 시점에 그 중간상태를 다른 트랜잭션이 볼 수 있습니다.
TCC는 분산 트랜잭션을 HTTP통신으로 다루는 방법입니다. 개인적으로 2PC와 통신 방법만 다를 뿐 전체적인 작업 방법 및 장단점에서 뚜렷한 차이를 느끼지 못해 이 글에서는 상세한 설명을 생략하겠습니다.
SAGA는 트랜잭션으로 묶이는 여러 작업 중 특정 작업이 실패했을 때 이전에 발생한 작업들을 상쇄하는 보상 트랜잭션
을 통해 분산 트랜잭션의 원자성을 보장하는 패턴입니다. SAGA는 요청이 왔을 때 곧바로 요청을 처리하기 때문에 2PC처럼 자원을 잠궈둘(lock)
필요가 없습니다.
SAGA에서의 원자성은 우리가 아는 일반적인 DB 트랜잭션의 ACID관점의 원자성이 아닙니다. DB에서의 롤백은 커밋 전에 발생하며 롤백이 일어나면 트랜잭션이 전혀 시작하지 않은 것처럼 되돌려집니다. 하지만 SAGA에서는 이미 트랜잭션이 발생
했습니다. SAGA의 보상 트랜잭션을 통한 롤백은 의미적 롤백
입니다. 한마디로 SAGA는 우리가 생각하는 완벽한 롤백을 해주지 못합니다!
부모님이 아끼는 도자기를 실수로 깨버렸을 때 시간을 되돌려 도자기를 깨지지 않은 것으로 만든다면 이것은 '롤백', 깨진 도자기를 정성스럽게 붙여 겉으로 봤을 땐 깨지지 않은 것과 같이 만든다면 이는 '의미적 롤백'이 아닐까요
때로는 작업의 흐름을 재정의하는 것으로 롤백을 줄일 수 있습니다. 실패할 가능성이 가장 높은 단계를 앞으로 당기고 해당 프로세스를 더 일찍 실패하면 보상 트랜잭션을 줄일 수 있습니다.
지금까지 얘기한 보상 트랜잭션을 통한 롤백
은 역방향 복구(Backward Recovery)
입니다. SAGA는 역방향 복구뿐 아니라 정방향 복구(Forward Recovery)
도 얼마든지 활용할 수 있습니다. 가령 실패한 내용이 성공한 부분에 비해 굉장히 사소하거나, 보상트랜잭션이 아니라 실패에 대한 알림을 주는것 정도로 타협할 수 있다면 실패를 복구하지 않고 계속 작업을 진행하는 정방향 복구
도 얼마든지 가능합니다.
SAGA의 또 다른 특징은 SAGA가 복구할 수 있는 실패가 비즈니스적인 실패
를 의미한다는 것입니다. SAGA는 기반 구성 요소들이 제대로 동작하고 있다고 가정
하며 네트워크의 실패와 같은 기술적인 실패
에 대한 보상작업은 일반적으로 진행되지 않습니다.
SAGA는 구현하는 방법에 따라 Orchestration saga와 Choreography saga로 나눌 수 있습니다.
중앙 조정자(Orchestration)을 사용해 실행 순서를 정의하고, 중앙 조정자가 필요한 보상 조치 또한 트리거합니다. 중앙 조정자는 연산을 수행하는 데 어떤 서비스가 필요한지 알고 있으며
, 언제 해당 서비스를 호출해야 할지를 직접 결정합니다.
이러한 방식은 중앙 조정자가 다수의 서비스를 알고 있으므로 결합도가 높은
방식입니다. 또한 서비스에 전달돼야 할 로직이 중앙 조정자에게 흡수되기 시작하면서 SPOF가 될 수 있습니다.
단일 팀이 전체 SAGA의 구현을 담당할 경우 현실적으로 더 편리한 방법입니다. 또한 이벤트 기반의 협업이 어렵게 느껴지는 조직에 적합한 방법입니다.
SAGA운영에 대한 책임을 분산
시키는 걸 목표로 합니다. 서비스 간 협업을 위해 이벤트
를 사용하며 덕분에 병렬 처리
가 용이합니다. Choreography saga는 발신자가 이벤트를 발행하면 그 이벤트에 관심이 있는 수신자들이 이벤트를 수신하고 그에 따라 적절히 동작하는 방식
을 취합니다. 모든 서비스가 상대 서비스에 대해 모른다
는 게 큰 장점입니다.
독립적인 운영으로 인해 SAGA에서 어떤 일이 일어나는지 파악하기 어려워 질 수 있습니다. SAGA의 상태가 어떤지를 정확히 파악하지 못해 적절한 보상을 취할 기회를 놓쳐버릴 수도 있습니다.
SAGA구현에 대한 책임을 각 팀에게 분배하기 편리하기 때문에 여러 팀이 시스템에 관여하고 있을 때 유용한 방법입니다. 때로는 느슨한 결합으로 얻는 이점보다 미미한데 SAGA의 진행상황의 추적하기 위한 작업의 복잡성으로 얻는 손해가 더 클 수 있습니다.
일관성과 정합성이 중요하다면 2PC를, 효율성이 중요하며 데이터가 결과적 일관성 수준으로 관리되어도 괜찮다면 SAGA를 사용하는 게 적합합니다. SAGA를 사용할 때도 조금 더 느슨한 결합을 원한다면 Ochestration saga보다 Choreography saga방식이 더 적합합니다.
Monolithic 방식
이 아닌 MSA 방식
을 선택하는 건 시스템의 확장성
과 고가용성
을 얻기 위한 선택입니다.
요청 및 응답 방식
이 아닌 이벤트 기반 방식
을 선택하는 건 더 느슨한 결합
을 얻기 위한 선택입니다.
ID만 전달하는 방식
이 아닌 자세한 이벤트를 전달하는 방식
을 선택하는 건 더 느슨한 결합
을 얻기 위한 선택입니다.
2PC방식
이 아닌 SAGA방식
을 선택하는 건 더 느슨한 결합
을 얻기 위한 선택입니다.
Orchestration saga
가 아닌 Choreography saga
를 선택하는 건 더 느슨한 결합
을 얻기 위한 선택입니다.