마이크로 서비스 인 액션 5장을 읽고 정리해보았습니다.
모놀리식 애플리케이션은 상태를 변경할 때 일관성과 격리를 보장하기 위해 트랜잭션에 의지한다.
마이크로 서비스 애플리케이션에서는 각기 독립적인 서비스가 특정 역량을 담당한다. 각기 데이터 소유권을 가지고 있어 결합을 제거해 자율성을 얻지만 애플리케이션 수준에서 일관성 문제가 있고 데이터 조회를 복잡하게 만든다. 또한 가용성은 애플리케이션 설계에 영향을 준다. 서비스 간의 상호작용이 실패하면 비즈니스 프로세스가 멈추고 시스템이 일관성을 잃은 상태로 남게된다. 이러한 여러 서비스 간에 복잡합 트랜잭션을 조율하기 위해 사가(sagas)패턴과 이벤트 소싱(event sourcing)과 같은 이벤트 기반 아키텍처로 해결할 수 있다.
주식을 매도하는 애플리케이션을 가정해보자.
고객의 입장에서는 모든 과정이 한 번에 일어난다. 가지고 있지 않은 주식을 매도하거나 두 번 이상 매도할 수 없다.
모놀리식 애플리케이션에서는 간단하다. ACID트랜잭션에서 데이터베이스 동작을 감싸고 잘못된 상태를 유발하는 에러는 원래 상태로 되돌아 가면 되기 때문이다.
마이크로 서비스 애플리케이션에서는 주문 서비스가 주식을 예약하고 수수료를 부과하기 위해 수수료 서비스를 호출 했을 때 이 트랜잭션이 실패할 수 있다. 분산된 데이터 소유권이 독립적이고 느슨하게 연결되는 것을 보장하지만 애플리케이션 수준에서 전체적인 데이터의 일관성을 유지하는 메커니즘을 구축할 필요가 있다.
여러 서비스에 걸쳐 트랜잭션을 보장하도록 시스템을 설계 할 수 있다. 일반적으로 two-phase commit이다. 이 방식은 여러 리소스에 걸친 동작을 준비하고 커밋하는 트랜잭션 관리자를 사용한다.
단점
그렇다면 대신 무엇을 사용해야 할까?
비동기 이벤트는 서비스 간의 결합을 느슨하게 하는 데 도움을 주고 저체적인 시스템의 가용성을 높이며 서비스 작성자로 하여금 궁극적 일관성 관점에서 생각하도록 독려한다.
동기식 접근법에서는 주문 서비스가 주문이 마켓에 제출될 떄까지 스텝 순서대로 호출해 다른 서비스의 행동을 조율한다. 어느 스텝하나가 실패하면 주문 서비스는 다른 서비스의 롤백 동작을 실행할 책임이 있다. 이런 유형의 상호작용은 호출 관계가 논리적이고 순차적이어서 추론하기 쉽지만, 감당해야 할 책임 때문에 주문 서비스가 다른 서비스와 단단하게 결합돼 독립성을 저해하고 향후 변경을 어렵게 한다.
각 서비스는 언제 어떤 작업을 해야 할지 알기 위해 관심 있는 이벤트를 구독한다. 이벤트는 가용성에 대해 낙관적인 접근 방식을 취할 수 있게 해준다. 예를 들어 수수료 서비스가 작동하지 않아도 주문 서비스는 여전히 주문을 생성할 수 있다. 각 서비스는 이벤트에 반응해 처리 과정의 전반적인 결과를 인지하지 않고 독립적으로 행동한다. 결국 이런 설계는 다른 서비스와 겹합력을 떨어트리고 독립성을 증가시켜 독립적으로 변경을 반영하기 쉽게 만든다.
사가는 조율된 로컬 트랜잭션의 연속이다. 로컬 트랜잭션은 Atomic하지만 사가 전체는 그렇지 않다. 그래서 개발자는 개별 트랜잭션이 실패하더라도 시스템이 궁극적으로 일관성 있는 상태가 되도록 코드를 짜야한다.
보상 동작은 사가에서 이전 작업을 없던 일로 하거나 시스템을 좀 더 일관된 상태로 돌리기 위해 사용한다. 이 설계 방식은 폭넓은 잠재적 시나리오를 고려할 필요가 있기 때문에 비즈니스 로직을 더욱 복잡하게 만들지만, 분산된 서비스 간의 신뢰할 수 있느 상호작용을 구축하기 위한 훌륭한 도구다.
각 테스크에서 발생한 이벤트에 대해서 보상을 설계한다. 이렇나 형태의 롤백은 시스템의 일관성을 아주 정확하게 유지하는 것이 아니라 의미상 유지하려고 한다. 롤백 동작을 수행한 시스템은 원래의 완전히 동일한 상태로 돌아갈 수 없을 수 있다. 프로세스에 포함된 모든 동작은 하나 이상의 적절한 보상 동작을 가질 수 있다. 이 방식은 시나리오를 예측하고 코딩하고 테스트하는 모든 것에서 시스템을 더 복잡하게 만든다. 특히 더 많은 서비스가 포함될수록 롤백은 더욱더 복잡해질 수 있다. 고립된 운영이 아닌 실 세계의 환경을 반영하는 서비스를 구축할 때 실패 시나리오를 예측하는 것은 중요한 부분이다. 마이크로 서비스를 설계할 때 더 넓은 애플리케이션이 복원력을 발휘하도록 보상 설계를 고려해야 한다.
따라서 비동기 커뮤니케이션 스타일을 선택할 경우 시스템의 실행 흐름을 추적할 수 있는 모니터링과 추적 기능에 투자해야 한다.
서비스가 조율자 역할을 하여 여러 서비스에 걸친 사가의 결과를 실행하고 추적하는 프로세스다. 사가의 참여자와 비동기 이벤트 또는 메시지 요청/응답을 통해 상호작용한다. 가장 중요한 것은 프로세스의 각 단계에서 실행의 상태를 추적하는 것이다. 때때로 이 것을 사가 로그라고 한다.
사가도 실패를 한다. 조율된 사가에서 조율자는 실패한 트랜잭션에 영향을 받은 엔티티를 유효한 일관된 상태로 되돌리기 위해 적절한 보상 동작을 시작할 책임이 있다. 기대한 동작이 실패한다면 보상 동작 또는 조율자 자신도 실패할 수 있다. 보상 동작도 의도치 않은 부작용 없이 재시도할 수 있도록 설계해야 한다. 최악의 경우 롤백 중 반복된 실패로 인해 수동 개입이 필요할 수도 있다. 완전한 에러 모니터링은 이런 시나리오도 잡아내야 한다.
ACID트랜잭션과 다르게 사가는 격리가 없다. 각 로컬 트랜잭션의 결과는 엔티티에 영향을 주는 다른 트랜잭션이 즉시 볼 수 있다. 이런 가시성은 엔티티가 동시에 여러 사가에 엮일 수 있다는 뜻이다. 그래서 서비스가 인티티의 중간 상태를 예상하고 다루도록 비즈니스 로직을 설계해야 한다. 이때 필요한 중첩의 복잡도는 주로 하위의 비즈니스 로직에 달려있다.
이렇게 중첩된 사가를 다루기 위한 3가지 일반적인 전략이 있다.
엔티티 상태에 대한 이벤트를 게시하는 대신, 그 객체에 발생한 이벤트의 연속으로 전체의 상태를 나타낸다. 특정 시각에 엔티티의 상태를 얻기 위해서 그 시각 이전의 이벤트를 통합한다. 전통적인 방식은 주문츼 최신 상태를 저장하지만 이벤트 소싱에서는 주문의 상태를 변경하도록 한 모든 이벤트를 저장한다. 이런 이벤트를 종합해 현재 상태를 나타낼 수 있다.
또한 분산된 데이터 소유권은 데이터 조회를 더욱더 어렵게 한다. 조인같은 데이터베이스 수준 또는 근접한 수준에서 데이터를 집계하는 것이 불가능하기 때문이다.
서비스가 이벤트를 통해 다른 서비스로부터 받은 데이터를 저장하거나 캐시하도록 선택할 수 있다. 표준 데이터를 여러 장소에 유지하면 이 데이터를 갱신하기 위한 비동기식 이벤트가 지연되거나 실패 또는 여러 번 전달될 수 있으므로 궁극적인 일관성 문제와 조회한 복제 데이터가 최신이 아닌 상황에 대비해야 한다. 최신 데이터를 보장하지 않으면서 성공적인 결과를 반환하는 가용성과 최신 상태를 반환하거나 실패하는 일관성 사이에서 선택해야 한다.
시스템에서 일기와 쓰기를 명시적으로 분리한다. 관심사를 합리적으로 분리할 수 있게 해준다.
CQRS
이벤트가 질의 모델을 갱신하기 때문에 누군가 그 데이터를 조회하면 만료된 뷰를 보게된다. 이를 위해 낙관적 갱신이나 폴링, 게시-구독 3가지 전략을 적용할 수 있다.
자바스크립트로 직접 만들면서 배우는 - 자료구조와 알고리즘 강의 바로 가기
실습으로 마스터하는 OAuth 2.0: 기본부터 보안 위험까지 - OAuth 2.0 강의 바로 가기
기계인간 이종립, 소프트웨어 개발의 지혜 - Git 강의 바로 가기
코드숨에서 매주 스터디를 진행하고 있습니다. 메일을 등록하시면 새로운 스터디가 시작될 때 알려드릴게요!