이번 포스팅을 시작으로 이벤트 기반의 MSA를 개선하는 과정에 대해서 시리즈로 작성하려고 합니다.
이번 포스팅에서는 레거시의 구조 및 문제점과 이를 해결하기 위해 어떻게 MSA로 전환했는지 공유드리며, MSA로 전환했음에도 남아있는 문제점들을 해결하기 위해 이벤트 기반의 MSA로 전환하게 된 과정까지 작성해보겠습니다.
MSA 전환 배경
레거시의 구조
저희 서비스 백엔드는 톰캣 웹 애플리케이션 서버에 서블릿 애플리케이션을 배포하여, JDBC를 사용해 DB와 통신하는, 전형적인 모놀리식 아키텍처의 레거시 웹 애플리케이션으로 구축되어 있었습니다.
심지어 서비스 레이어도 없었고, 백엔드 서버는 주로 DB에서 데이터를 읽어와 프론트에 전달하는 역할만 수행하여, 대부분의 비즈니스 로직은 클라이언트 또는 AWS Lambda와 같은 외부 인프라에서 처리되는 상황이었습니다. 이런 구조는 서비스가 작을 때에는 어느 정도 운영이 가능했지만, 규모가 커지면서 여러 가지 문제들이 발생하기 시작했습니다.
레거시의 문제점
우선, 백엔드가 단순히 데이터베이스와 HTTP 요청을 연결하는 일종의 어댑터 역할만을 하다 보니, 요청의 검증이나 데이터를 전후처리 하는 로직들이 모두 클라이언트 있어서 보안에도 취약했고, 디바이스에 따라 클라이언트 사이드에서 병목도 많이 발생했었습니다.
또한 비즈니스 로직이 프론트와 백엔드, 외부 인프라 등 여러 곳에 강결합된 채로 혼재되어 있었습니다. 그래서 한 곳의 변경이 생기면 어려 군대 영향을 미치기 시작했고, 한 시스템의 결함이 여러 시스템으로 전파되기도 했습니다. 또한 기존 비즈니스 로직의 흐름을 추적하기 힘들어 유지 보수가 굉장히 어려웠습니다.
이러한 문제를 개선하기 위해, 초창기에는 비즈니스 로직을 모두 서버 단일 지점에서 처리하도록 레거시 애플리케이션에 도메인 레이어와 서비스 레이어를 추가하여 점진적으로 개편을 시도했습니다. 하지만 단일 애플리케이션에 여러 인프라에서 처리하는 비즈니스 로직들을 이관하다 보니 복잡도가 증가하는 것이 눈에 보이기 시작했고, 유지보수하기 힘든 수준으로 레거시 애플리케이션이 커질 거 같다는 생각을 하게 되었습니다.
그러던 와중 기프티콘 이벤트를 진행하게 되었는데, 이 때 트래픽이 대량으로 한 번에 증가해 서버가 중단되는 사태가 발생했습니다. 당시 단일 인스턴스에 단일 백엔드 애플리케이션을 배포한 형태였기 때문에, 전 서비스가 마비되는 대참사가 일어나게 되었습니다.
모놀리식 아키텍처로 구성되어 있었기 때문에 이벤트와 관련된 부분만 확장을 하지 못했고, 로드밸런서와 같은 부하분산 인프라도 구축된 것이 없었기에, 서버가 배포된 인스턴스의 사양을 높이는 스케일 업 방식으로 확장을 하여 문제를 임시로 해결하게 되었습니다.
MSA 전환 결정
결론적으로 유지보수성과 고가용성, 확장 가능성을 생각해 볼 때, 위 문제들을 해결하기 위해서 기존의 레거시 애플리케이션에 레이어만 추가해서 개편하기에는 한계가 너무 많았습니다. 문제를 근본적으로 해결하려면 백엔드를 아예 새롭게 클라우드 기반의 MSA 환경으로 전환하는 것이 나을 거 같다고 의견이 모아졌습니다.
클라우드 기반의 MSA 환경으로 개편하는 또 다른 이유로는 점진적으로 개편이 가능하다는 점도 있었습니다. 레거시방식으로 처리하던 요구사항을 모두 한 번에 새로운 애플리케이션으로 가져오는 것도 일종의 챌린지라고 생각을 했습니다. 개발해야 하는 범위가 넓어 시간이 오래 걸리기도 하고, 현재 고객을 대상으로 운영중인 서비스이기 때문에 모든 요구사항들을 한 번에 이관했을 경우에 운영환경에서 모든 기능들이 제대로 작동하는 지 검증하는 과정 역시 굉장히 광범위하고 복잡하다고 판단되었습니다.
하지만 클라우드 기반의 MSA 환경으로 개편을 하게 된다면, 기존의 레거시 서버 애플리케이션을 하나의 다른 마이크로 서비스로 보고, 신규로 다른 서비스가 개발이 완료될 때 마다 점진적으로 이관할 수 있다는 점도 큰 장점이라고 생각했습니다. (이와 관련해서 더 자세한 내용은 스트랭글러 패턴을 참조하시면 좋습니다.)
결론적으로, 기존 레거시 방식과 신규 애플리케이션을 모두 운영환경에서 운용할 수 있으며, 레거시 방식으로 처리하던 요구사항들을 점진적으로 신규 애플리케이션에 이관할 수 있고, 유지보수성과 고가용성, 확장가능성을 만족할 수 있는 클라우드 기반의 MSA로의 전환을 진행하게 되었습니다.
MSA 전환 과정
개발 인원이 적고, 빠르게 개발해야 할 신규 기능들도 많았기 때문에, MSA 환경에서 발생할 수 있는 여러 문제점들을 보완할 수 있는 다양한 방식들을 적용시키기에는 시간이 많이 부족했습니다.
따라서 위의 배경에서 언급했던 MSA로 전환하게 된 이유들을 해결하기 위해 1차적으로 달성하고자 하는 목적을 설정했고, 이를 달성할 수 있는 정도의 수준으로 개발을 진행하기로 했습니다. 목적을 정리하면 아래와 같습니다.
1. 단일 애플리케이션을 분리하여 복잡도를 줄이고 유지보수를 편하게 할 것
2. 부하가 증가할 것으로 예상되는 시점에 필요한 부분만 확장 가능해야 할 것
3. 부하가 증가하는 경우, 고가용성이 보장되어야 할 것
1번은 당연한 목적인데, 2, 3 번이 어찌보면 MSA 아키텍처를 구성하는 의미가 있나 싶은 목적이긴 합니다. 하지만 아직 대규모 서비스도 아니고, 평소에는 트래픽이 감당 불가능한 수준은 아니기 때문에 기프티콘 이벤트와 같은 특정 시점에만 확장이 가능하고 고가용성이 보장되면 된다고 결론이 내렸습니다. 사실 그만큼 인프라 비용이 증가하기 때문에, 이 비용을 할당하기에 어려웠던 점이 큽니다.
결론적으로는 하나의 애플리케이션을 분리하여 복잡도를 줄이고 유지보수를 편하게 하는 것, 추후 부하가 증가하는 시점에 해당 서비스에 한해서 고가용성을 보장할 수 있도록 확장 하는 것에 초점을 맞추어 최대한 간단하게 구축하기 시작했습니다. 어떻게 구성했는지 한번 살펴봅시다.
MSA 기반 만들기
우선 Spring Cloud Gateway, Spring Cloud Eureka, Spring Cloud Config를 사용해서 간단하게 MSA를 가동하기 위한 API Gateway, Service Discovery, Configuration Server 을 구성했습니다.
그리고 이벤트 스토밍을 통해 비즈니스 도메인에서 발생하는 이벤트들을 정리한 후 목적별로 그룹핑하여 컨텍스트를 정의했고, 각 컨텍스트 별로 마이크로 서비스를 만들어 각각 하나의 DB 인스턴스와 연동했습니다.
각 서비스들은 컨텍스트에 따라 여러 아키텍처와 라이브러리를 사용하여 구현했습니다. 가령 외부 인프라에 의존도가 높다고 판단되는 서비스들(알림, 결제 등)은 도메인 로직 변경 없이 인프라 변경에 쉽도록 헥사고날 아키텍처를 적용하거나, 단순 DB내의 데이터를 효율적으로 조회해야 하는 서비스는 레이어드 아키텍처로 구성하고 쿼리에 좀 더 자유로운 MyBatis를 사용하였고, AI와 관련된 서비스들은 관련 라이브러리 지원이 많은 파이썬을 기반으로 서비스를 개발하는 등 여러 방식으로 구현했습니다.
레거시의 경우는 API Gateway에서 라우팅만 되도록 했고, 다른 서비스에서 레거시를 의존하지는 않도록 했습니다. 어차피 Legacy의 기능을 다른 서비스로 이관해야 했기 때문에, 새롭게 만들어지는 서비스들이 레거시 애플리케이션을 의존한다면 추후에 다른 서비스를 의존하도록 변경해야 했기 때문이었습니다. 따라서 레거시는 아직 신규 서비스에서 개발되지 않은 기존 비즈니스 로직만 담당하도록 했습니다.
빌드와 배포
빌드는 Github Action을 통해 메인에 PR이 머지되면 빌드를 시켜 테스트까지 모두 완료 된 후 빌드 결과물을 도커 이미지로 빌드하여 ECR로 푸시하였습니다.
빌드가 끝난 후 배포 역시 Github Action을 통해 진행하였고, ECR에 등록된 이미지를 각 EC2 인스턴스로 가져온 다음, 무중단 배포를 위해 Blue-Green 방식으로 이미지를 컨테이너화 하는 Bash 스크립트를 작성해 실행했습니다.
Blue-Green 방식의 무중단 배포였지만 매번 인스턴스를 생성하지는 않았습니다. 앞서 말했지만 서버 트래픽이 과도하게 많지 않았기에 따로 부하분산, 스케일링 관련 설정은 해두지 않았고, 각각의 서비스를 다른 인스턴스에 분리만 하여 배포했기 때문에, Spring Cloud Gateway의 로드밸런싱을 이용해서 같은 인스턴스 내에 다른 포트로 이미지를 실행하는 방식으로 무중단 배포를 구현했습니다.
서비스간 통신
초창기에 서비스간 통신은 Spring Cloud OpenFeign을 사용하여 동기적으로 HTTP 통신을 하였고, 추후 성능상 비동기 호출이 필요한 경우와, 서비스간 필요없이 강결합되어 있는 경우가 자주 발생하여 AWS SNS와 SQS를 사용해 비동기적으로 통신을 하도록 했습니다.
비동기 통신은 공통 이벤트 스펙을 정의하여 하나의 SNS 엔드포인트로 모든 이벤트들을 보냈고, SQS가 이를 구독하여 각각 필터링된 이벤트만 수신받아, 각 서비스들은 필요한 이벤트가 전달되는 SQS 큐를 폴링하여 메시지를 가져오는 구조로 설계하여 구현했습니다.
분산 추적 및 모니터링
부하분산이 따로 적용되어있지 않고, 고가용성이 보장되지 않는 환경이기 때문에 인스턴스의 상태나 애플리케이션 단에서 발생하는 예외에 대해서 지속적인 모니터링과 이에 상응하는 알림을 받는 것은 중요했습니다. 또한 동기적으로 서비스간 결합되어 있는 부분이 많았기 때문에, 분산 환경을 추적할 수 있는 시스템도 마련해야 했습니다.
하지만 백엔드 개발 인력이 부족했고, 시간적으로 여유가 없어 직접 이러한 시스템을 구축하고 운영하는 데에 한계가 있어, 결론적으로 이 역시 목적을 달성할 수 있는 수준으로 간단한 모니터링과 알림 시스템만을 구축하자고 결론이 났습니다.
모든 시스템은 AWS 환경에 배포가 되어있었고, 각 서비스의 로그 역시 CloudWatch로 보내고 있었기 때문에 직접 관리할 필요 없는, 간단한 CloudWatch를 통해 모니터링과 알림을 설정하도록 했습니다.
임계점이 넘어가려는 인스턴스가 있거나, 서비스간 통신에서 예외가 발생할 때 남는 로그가 일정 수준을 증가하는 경우 알림을 받도록 설정하여 이 알림이 발생하면 수동으로 확장을 진행하는 방식을 채택했습니다.
MSA 전환 회고
지금까지 모놀리식 레거시 애플리케이션을 어떻게 클라우드 기반의 MSA로 전환했는지 과정을 살펴보았습니다. 한번 어떤 문제들을 해결했고, 남아있는 문제점이 뭔지, 그리고 이를 해결하기 위해 어떤 방향으로 나아가야 할지 정리해보겠습니다.
해결한 문제들
우선 앞서 설정했던 1차 목표들은 달성했다고 볼 수 있습니다.
1. 단일 애플리케이션을 분리하여 복잡도를 줄이고 유지보수를 편하게 할 것
이벤트 스토밍을 통해 비즈니스 목적별로 컨텍스트를 그룹지어 이를 기준으로 각 서비스를 만들었기 때문에, 요구사항이 바뀌는 경우에는 해당 컨텍스트를 담당하는 서비스만 수정하고 배포하는, 유연하고 유지보수성이 좋은 아키텍처를 구성했습니다.
또한 서비스는 하나의 컨텍스트를 담당하여 개발했기 때문에, 이전보다 복잡도가 줄긴 했습니다. 다만, 문제점 파트에서 후술하겠지만 서비스간 느슨하게 결합되어 있는 형태는 아닙니다. 이전에 비해 결합이 줄었을 뿐이지, 서비스간 동기적으로 통신하고 있기 때문에 아직은 의존도가 높습니다.
2. 부하가 증가할 것으로 예상되는 시점에 필요한 부분만 확장 가능해야 할 것
AWS CloudWatch를 통해 임계점이 넘어가려는 인스턴스가 있거나, 예외가 지속적으로 발생하면 알림을 받도록 했기에 확장해야 할 시점을 알 수 있습니다.
확장은 수직적, 수평적으로 모두 할 수 있습니다. 각 서비스는 독립적으로 다른 인스턴스에 배포되기 때문에, 필요한 인스턴스만 사양을 높일 수 있고, 또한 Spring Cloud Eureka와 Spring Cloud Gateway를 통해 Service Discovery와 API Gateway를 구축했기 때문에, 인스턴스를 늘려 서비스를 배포하여 스케일 아웃 방식의 확장도 가능합니다.
만약 B 서비스의 트래픽이 증가한다면, 새로운 인스턴스에 B 서비스를 배포하여 Service Discovery에 등록하고, API Gateway에서 이를 참고해 로드밸런싱을 진행하여 요청을 분산하게 됩니다.
3. 트래픽이 증가하는 경우, 고가용성이 보장되어야 할 것
Service Discovery에서는 헬스체크를 통해 서비스들의 상태를 확인하고 이를 기반으로 정상적인 인스턴스로만 API Gateway에서 라우팅을 시키기 때문에, 인스턴스를 다중으로 분산하는 경우 하나의 서비스가 중단되더라도 다른 서비스로 요청이 라우팅 됩니다. 따라서 서비스 단에서 고가용성은 보장합니다.
반면 서비스가 다중 인스턴스에 배포된다 하더라도 DB는 단일 인스턴스를 바라보기 때문에 고가용성을 보장하지 못하는 구조이긴 합니다. RDS를 사용하고 있었기 때문에, 다중 AZ 배포를 통해 1개의 대기 인스턴스를 설정하는 방법으로 고가용성을 보장하려고 했습니다. 하지만 그만큼 비용이 두 배로 들게 되고, 현재 상황에서 DB단까지 장애가 생길 가능성은 낮았기 때문에 너무 과도한 조치라고 판단을 내렸습니다. 따라서 CloudWatch로 로그와 모니터링을 활용해 인스턴스의 부하가 위험한 수준에 달성한 경우 알림을 받아 수동으로 조치하는 방법을 대안으로 채택했습니다.
또한 현재 구조로는 API Gateway 단에서도 고가용성이 보장되지는 못합니다. API Gateway 앞 단에 AWS ALB(Application Load Balancer)를 두어 API Gateway가 중단될 경우 새로운 API Gateway 인스턴스를 띄어 로드밸런싱이 가능하도록 설계하려고 했으나, 단순 요청 라우팅만 수행하는 API Gateway 단이 중단될 가능성은 낮다고 판단했고, ALB 역시 요금이 발생하기 때문에 과도한 조치라고 판단하여 구축하지는 않았습니다.
남아있는 문제들
자체 모니터링 및 추적 시스템의 부재
우선 자체 분산 추적과 모니터링 시스템이 구축되지 않았다는 점입니다. MSA는 각 서비스별로 사용하는 기술과 담당 조직도 다르고, 여러 개의 인스턴스에 배치되고, 서비스 간 연계도 많기 때문에 장애 관리를 위해 이를 추적하고 모니터링 하는 것은 중요합니다. 하지만 현재 시스템은 단지 각 인스턴스별로 CloudWatch를 통해 지표를 확인하고, 각 서비스별 로그를 확인하여 알림을 받는 것에 그쳐있습니다.
만약 개선한다면 Micrometer와 OpenTelemetry를 기반으로 매트릭, 트레이스, 로그를 수집하고, Grafana를 통해 통합 모니터링 시스템을 구축하는 것이 좋아보입니다. 대부분의 서비스가 Spring Boot로 개발되어 있기 때문에 Micrometer를 통해 매트릭을 쉽게 수집할 수 있고, 여러 시스템에서 OpenTelemetry Collector 단일 지점으로 매트릭, 트레이스, 로그를 보내 Prometheus, Jaeger, Tempo, Loki 등의 다른 외부 도구로 전송할 수 있으며, Grafana를 통해 외부 데이터 소스들을 입맞에 맞추어 시각화된 화면으로 볼 수 있도록 하고, 알림을 받을 수 있습니다.
하지만 아직까지 인스턴스와 각 트래픽이 엄청 많은 상황은 아니라, 현재 방식을 사용하더라도 크게 문제가 없는 상태이기 때문에 모니터링과 분산추적 시스템의 개선은 우선순위가 조금 낮습니다. 추후에 DevOps팀이 새로 생기거나, 시간적 여유가 있거나, 급속도로 트래픽이 증가하여 세분화된 모니터링이 꼭 필요해지는 경우에 구축할 생각입니다.
배포환경의 유연성 부족
현재는 단일 애플리케이션을 단일 인스턴스에 도커를 통해서 배포하고 있습니다. 인스턴스의 상태에 따라 유연하고 확장성 있게 컨테이너를 조율하는 시스템은 없습니다. 이는 추후에 서비스가 커진다면 쿠버네티스 환경에서 상황에 맞게 컨테이너를 조율할 수 있도록 빌드와 배포 시스템을 구축해야 할 거 같다고 생각은 하고 있습니다. 다만, 사실 분산추적과 모니터링와 같은 이유로 이 상황까지 가기에는 아직 날이 멀지 않았나 싶긴 합니다. 이 역시 우선순위가 낮습니다.
서비스간 강결합
마지막으로 앞서 잠시 언급했던, 각 서비스 간 HTTP를 통해 동기 방식으로 통신을 함으로써 서비스간에 강하게 결합되어 발생하는 문제점입니다. 한번 예시를 들어 살펴보겠습니다.
주문 서비스는 주문이 발생하면 결제 서비스에게 결제를 요청하고, 배송 서비스로 배송을 등록하는 간단한 프로세스로 되어 있습니다. 결제까지 완료된 시점에, 배송 등록에 실패하면 주문 서비스는 이미 처리된 결제를 취소하기 위해 결제 서비스로 결제 취소 요청을 보내어 최종적으로 실패 처리를 하게 됩니다.
이렇게 동기적으로 호출하는 방식의 문제점은 뭘까요? 우선 서비스간 결합이 강하게 되어있다는 점입니다. 실제 주문 하나를 처리하기 위해, 배송 및 결제 서비스의 응답을 기다려야 하기 때문에 그만큼 지연이 발생합니다. 만약 결제 서비스가 높은 부하로 인해 응답이 느리다거나, 재시도를 여러번 수행하게 된다면 그 만큼 사용자는 주문에 대해 지연된 응답을 받게 됩니다. 또한 배송 서비스가 변경되는 경우, 이를 의존하는 주문 서비스 역시 변경이 되어야 하고, 결제 서비스가 변경되는 경우도 마찬가지로 사용하는 모든 곳에서 변경이 이루어져야 합니다.
다음 문제점으로 분산 환경에서 하나의 트랜잭션으로 묶을 수 없기 때문에 데이터의 일관성을 보장하기도 힘들다는 점입니다. 위 예시에서 여러번의 재시도에도 불구하고 결제 취소에 실패한 경우, 주문은 실패 처리가 되어있지만 결제는 성공 처리 되어있게 됩니다. 이 경우에는 결제 서비스는 주문이 취소되었는지 알 수 없기 때문에, 추후 배치를 통해 실패된 주문에 대해서 결제를 다시 취소 처리하는 방식 등으로 데이터의 최종 일관성을 보장하는 추가적인 프로세스가 필요하게 됩니다.
이에 따라 주문 서비스는 각 서비스의 실패에 대해 예외 처리와 재시도 처리와 같은 로직들이 추가되며, 주문 서비스는 도메인에 대해 집중하지 못하고 각 서비스를 조율하는 오케스트레이션과 같은 역할의 비중이 점차 증가하게 됩니다.
지금은 하나의 비즈니스 로직에 대해서 살펴보았지만, 이런 비즈니스 로직이 수백개가 있다고 생각해보면 그 복잡도는 훨씬 증가하게 됩니다. 저 역시 점점 레거시에서 이관한 기능과 신규 개발한 기능들이 많아지자, 동기식 호출에서 오는 문제점들이 가장 크게 피부에 와닿았습니다. 따라서 가장 급하게 해결해야 할 문제라고 판단하게 됩니다.
이벤트 기반 MSA 전환
동기식 호출에서 오는 문제점들을 해결하기 위해, 이벤트 기반의 비동기 통신 방식으로 전환하게 됩니다. MSA 전환 과정에서 언급했듯, 원래 서비스간 하나의 트랜잭션으로 묶이지 않아도 되는 로직들은 모두 SNS/SQS를 활용하여 이벤트 기반 비동기 통신을 하고 있기 때문에 이를 활용하게 됩니다.
우선 위에서 예로 들었던 주문 프로세스를 SNS/SQS를 활용하여 이벤트 기반의 비동기 통신으로 전환한 예시를 살펴보며, 어떻게 전환했는지 설명하겠습니다.
우선 각 서비스 간 결합을 낮추기 위해 서비스에서는 담당하는 도메인에서 발생한 행위에 대해서만 이벤트를 날리도록 했습니다. 주문 서비스는 주문 생성 이벤트만 발행할 뿐, 그 이후에 어떤 작업들이 이루어져야 하는지 전혀 모릅니다. 다른 서비스들도 마찬가지 입니다. 확실히 동기적으로 통신하는 것 보다 서비스 간에 느슨하게 결합되었습니다.
그리고 발행한 이벤트가 지속적으로 소비되지 못하는 경우에는 DLQ(Dead Letter Queue)로 보내 해당 메시지를 다시 리드라이브하여 언젠간 결제 서비스에서 처리될 수 있도록 했습니다. 따라서 동기적으로 통신하는 방식보다 최종 일관성을 보장하기 수월해졌습니다.
여전히 남아있는 문제점
이벤트 기반 비동기로 서비스간 통신을 전환하였으나, 여전히 잔재하는 문제점들이 많습니다. 한번 정리해보겠습니다.
1. 비즈니스 흐름을 제어하는 지점이 역전됨
주문 생성 이벤트가 발행되면 해당 이벤트를 받기 위해 각각의 큐에서 메시지를 필터링하여 구독하게 되는데, 이는 비즈니스의 흐름을 제어하는 지점이 SNS/SQS로 역전된 것이라 할 수 있습니다.
위 예시에서는 주문 생성 이벤트가 발행되었을 때 결제 요청 큐에서 해당 이벤트만을 받기 위해 SNS 구독 시 메시지 필터링을 포함하여 설정하게 됩니다. 즉, 주문 생성 다음에 결제 요청이라는 비즈니스 흐름을 SQS의 구독 설정으로 관리하게 되는 셈입니다.
SNS는 메시지를 게시하기 위한 도구이고, SQS는 메시지를 전달하기 위한 도구일 뿐이지 비즈니스의 흐름을 관리하기 위한 도구는 아닙니다. 어떤 메시지가 필터링 되어 어떤 큐로 들어가는지 직접 큐 각각의 구독 설정을 확인해가며 파악해야 하기 때문에 관리가 더 복잡해지게 됩니다.
2. 큐가 과도하게 생성됨
하나의 메시지는 한번만 소비할 수 있는 메시지 브로커인 SQS의 특성 상, 이벤트에 따른 로직을 처리하기 위해 큐를 각각 하나씩 생성해야 하기 때문에 수 많은 큐가 생기게 됩니다. 만약 결제 완료 이벤트가 발생하는 경우 배송 등록 처리와 더불어 메일 발송도 해야 한다고 하면, 실제 이벤트는 1개만 발행됐지만 이를 처리하는 배송 등록 큐, 메일 발송 큐 두 개를 생성해야 합니다. 따라서 이벤트가 증가하면 증가할수록 큐의 수도 증가하게 되고, 어떤 큐가 어떤 메시지를 받는지, 그리고 해당 큐를 어떤 서비스에서 폴링하고 있는지 파악하기 더 어려워집니다.
물론 저희 서비스가 공통 이벤트 형식을 가지고 하나의 SNS만 사용하여 이벤트를 발행 및 전송해서 이런 문제가 심해진 것일 수도 있습니다. 하지만, SNS를 분리한다고 하더라도 비즈니스의 흐름이 SNS/SQS를 통해 조율된다는 것과 큐가 과도하게 생성된다는 점은 변함 없습니다.
서비스 별로 SNS 채널을 분리한다고 해도, 결국 사용하는 측에서 메시지를 받기 위해 큐를 생성한 다음 해당 메시지가 발행되는 SNS를 구독하도록 설정해야 합니다. 오히려 메시지를 어느 SNS로 보내고, 해당 SNS는 어떤 SQS가 구독하고 있고, 해당 SQS는 어떤 서비스의 어느 지점에서 폴링하고 있는지, 그리고 각각 실패한 메시지에 대한 DLQ도 관리해야 한다는 점에서 복잡한건 마찬가지라고 생각합니다.
3. 이벤트의 발행을 보장하지 못함
앞서 동기 방식의 통신에서 데이터의 일관성을 완벽하게 보장하기는 힘들다고 했습니다. 하지만 비동기 방식의 통신에서는 메시지가 무조건 한 번은 전달되는 것이 보장되고, 지속적으로 서비스로 메시지가 전달되지 못하는 경우 DLQ로 메시지를 전달하기 때문에 언젠가 이를 리드라이브하거나, 직접 폴링하여 처리해 최종 일관성을 보장하기 비교적 수월합니다.
하지만 이는 언제나 이벤트가 메시지로 발행되었다는 전제 아래에 가능합니다. 만약 이벤트가 발행되지 않았다면, 결국 최종 일관성을 보장하기 어렵습니다. 하지만 현재 상황은 이벤트 발행을 보장하는 구조는 아닙니다. 메시지 발행과 DB는 하나의 트랜잭션으로 묶이지 않기 때문입니다.
비즈니스 로직 수행 이후 DB에 변경사항을 커밋한 다음에 이에 대한 이벤트를 발행한다고 한다면, 네트워크의 장애로 SNS에 메시지가 발송되지 않더라도 DB의 변경사항은 롤백되지 않습니다. 이는 반대의 경우도 마찬가지인데, 메시지 발행 로직이 비즈니스 로직보다 선행한다면, 메시지 발행 이후 후속 비즈니스 로직이 수행중에 예외가 발생하는 경우 DB는 롤백되지만 이미 발행한 메시지는 회수하기 어렵습니다.
정리
지금까지 레거시 애플리케이션의 구조와 문제점, 그리고 이를 해결하기 위해 MSA로 전환한 과정들, MSA로 전환했지만 남아있는 문제점들, 그리고 해당 문제점들을 해결하기 위해 이벤트 기반의 비동기 통신으로 전환했지만 여전히 남아있는 문제점들에 대해서 살펴보았습니다.
이벤트 기반의 MSA를 개선하기 전 까지의 히스토리를 전부 담다보니, 글이 좀 장황해졌네요. 앞으로의 포스팅에서 이러한 배경을 참고하여 어떻게 개선하게 되었는지 살펴보면 더 좋을 거 같습니다.
MSA로 전환함에 따라 주는 이점도 분명 있지만, 개발의 세계에서 은탄환은 없듯이.. MSA 역시 그만한 단점을 지니기 마련입니다. 하나의 커다란 시스템이 여러개의 단일 시스템으로 분산된 만큼, 미시적인 관점에서 테스트, 배포, 확장성 등은 이점으로 다가오지만, 거시적인 관점에서는 이를 관리하고 배포하고 통신하는 과정 등에서 단일 시스템이 주던 이점을 포기해야 하는 건 마찬가지입니다. 따라서 이를 보완하기 위해 복잡한 시스템이 구축될 수밖에 없습니다.
하지만 이런 단점들을 커버하기 위해서 비교적 더 정교한 방법들을 고안해 보완하게 된다면, 그만큼 구현 및 운영 복잡도도 증가됩니다. 따라서 필수적으로 보완해야 할 부분과, 포기해도 괜찮은 부분을 정하여, 정교함과 구현 및 운영 편의성 둘 사이의 타협점을 잘 찾아서 적정 수준의 시스템을 구현하는 것도 중요하다고 생각합니다.
앞서 말했듯이 모니터링과 빌드 및 배포 프로세스의 경우에는 더 정교하고 복잡한 시스템을 구축하기엔 이른 시기라고 판단을 했고, 아직까지는 적정 수준이라고 생각합니다.
다만, 분산시스템 간의 통신은 아직 적정 수준으로 구현되지 않았기 때문에 위에서 설명했던 문제들이 생기지 않았나 싶습니다. 이벤트 기반 비동기 통신 방식이 지나치도록 단순하게 구현되어 있고, 이는 MSA로 전환함에 따라 오는 단점을 보완하지 못하고 있다고 생각이 듭니다.
다음 포스팅에서는 이벤트 기반의 MSA의 개념에 대해서 다시 짚어보며, 이벤트 기반의 MSA라 할지라도 발생할 수 있는 문제점들을 분석해보고 이를 문제들을 해결할 수 있는 여러 이론 및 패턴들을 살펴보도록 하겠습니다.