project/resistance

웹서버 마이그레이션, MSA 설계하기

downfa11 2024. 11. 27. 11:22

기존의 Flask로 간단하게 구현한 프로토타입 백엔드는 Http 통신으로 계정과 게임 데이터를 불러오거나, 환율 혹은 공지사항, 이벤트 등의 실시간 변동 사항을 적용하는 시스템을 구현했었다.

그러나 점점 볼륨이 커지니까 관리하기가 어려웠고, 공부중인 기술과 아키텍처의 경험도 얻을 겸 MSA로 마이그레이션을 시도했다.

 

k8s로 인기를 얻게된 MSA는 굉장히 어렵고 복잡한 구조로 이해하기 위한 제반 지식이 필요하지만, 나의 경험을 녹아내기 위해 필요한 개념들만 간략하게 소개하면서 진행하겠다.

MSA(Mircro Service Architecture)란?

작고, 독립적으로 배포가 가능한 각각의 기능을 수행하는 서비스(mirco service)로 구성된 아키텍처를 말한다.

기존의 모놀리스(Monolithic) 아키텍처는 단 한 줄의 코드가 수정되어도 모든 애플리케이션의 재배포가 필요했다.

이런 모놀리식 아키텍처는 배포가 간단하고 유지보수가 쉬운 반면에, Scale Out이 어렵고 규모가 커질수록 유지 보수가 어려워진다.

무엇보다도, 장애시 전체 서비스에 영향을 미친다는 가장 큰 취약점이 있다.

반면에 이런 모놀리식으로부터 분해된 micro 서비스들의 느슨한 결합인 MSA는 잦은 변동성에 대한 유연한 대응이라는 목적이자 장점을 가진다. MSA의 핵심은 빠른 대처가 필요한 비즈니스 모델을 위한다는 점

하지만 분산 시스템이 마냥 좋기만 한게 아닌게... 굉장히 구조적으로 어렵다.

1. 서비스간의 동기적(Http, gRPC)이던 비동기적(Message Queueing)인 통신의 어려움

Keep alive, Connection Pool, Connection/Bisiness Logic TimeOut의 차이 등 제반 지식이 요구

2. 트랜잭션의 어려움,,,,,,,,,,,,

3. 서버 모니터링과 부하 테스트의 어려움

Hexagonal Architecture

비즈니스 로직을 격리시켜서 유연하고 테스트가 용이한 구조. 포트-어댑터 아키텍처(Ports and Adpaters Architecture)라고도 함.

이를 위해 핵심 비즈니스 영역은 중앙의 도메인에 위치하며, 입력과 출력을 처리하는 Port와 Adapter를 통해 외부와 소통한다.

이때 모든 비즈니스 로직은 오직 외부에서 내부로만 호출이 가능하며, 경계간의 이동이 제한된다.

나갈때 Port, 들어올때 Adapter

장점 :

유연성 : 외부 시스템이나 인프라의 의존성이 낮아 업데이트가 용이

테스트 용이 : 비즈니스 로직을 독립적으로 테스트할 수 있음

유지보수성 : 코드의 이해와 수정이 용이하며 변화에 빠르게 대응

단점:

구현 복잡성 : 포트와 어댑터를 구성하고 관리하는데 약간의 복잡함

초반 개발 시간 증가 : 처음 아키텍쳐 구축시 시간과 노력이 필요

 

기존의 계층 구조가 아닌 Hexagonal 구조를 선택한 이유?

카프카(Kafka)같은 외부 시스템에 연결할때 특히 유용하기 때문이다.

기존 아키텍처의 경우는 UserService 로직에서 카프카와 연결되면 외부 의존성이 커지지만 외부 시스템인 Kafka를 RabbitMQ로 바꾼다 해도 어댑터를 교체하는 방식으로 로직 수정 없이 대응할 수 있다.

 

 


 

 

기존 프로토타입 백엔드에서 구현된 기능들은 다음과 같다.

  1. 어떤 보안 요소도 적용되지 않은 계정과 게임 데이터 관리
  2. 서버에서 통제하는 환율 시스템으로 클라이언트 내 재화 가격 변동을 조정
  3. 복잡한 연관 관계의 친구 기능과 전투 중 함께 할 동료 시스템
  4. 공지사항과 이벤트, 텀블벅 후원자 전용 특전 아이템 제공과 관리 등 실시간 중요 정보 전달

예상되는 하위 도메인 분리 계획

  • Membership 시스템 : Vault와 OAuth2(네이버, 카카오)
  • Logging 시스템
  • Game Business 시스템 : 게임 데이터 관리 (계정 서버와 IPC 통신 필요)
  • Dedicated 시스템 : 환율 제어, 공지사항 CRUD (고도화 이후로 exchange-service, board-service로 분리 계획)

기존의 환율 시스템이나 공지사항 등의 실시간 전달 등의 게임 비즈니스 로직은 dedicate-service에서 담당하도록 설계했다.

 

사실 이 dedicated 모듈은 Aggregate Root에 해당하지 않기에 분리 지침 가이드를 따르지 않는 셈이다. 그러나 외부 서비스의 의존도가 낮은 시스템이기에 충분히 감당 가능하다고 판단했다.

 

MSA 환경에서 이상적인 모니터링 과정

  1. 특정 Metrics에 사전 정의된 Alert를 통해 문제 상황을 감지 (c.f DB 정합성 문제 등)
  2. Log를 통해 어떤 Metric에 대한 문제인지 파악해서 행동 판단

이때 단순 로그로 확인이 어려우면 트랜잭션에 대한 Tracing을 확인해야한다.

이게 여러 서비스를 디버깅하기 어려워서 컨테이너 개념이 도입된 것이다.

MSA 환경에서 바로 k8s를 떠올리게 하는 이유

Kubernetes는 Docker-compose와 유사하지만 훨씬 복잡하고 다양한 기능을 지원하는 Container Ochestration이다.

고도화까지 마치면 k8s까지 맛보고 싶지만 아마 어렵지 않을까. 아직은 Docker-compose로만 개발할 계획

 


 

MSA 구조 설계 중 겪은 어려움과 고민

1. 하위 도메인별 분해

상호간 겹치면 안되고, 배타적인 문제를 해결해야함(Bounded-Context)

단일 책임 원칙(SRP),공동 폐쇄 원(CCP) 등 분리 지침 가이드 충족하는지?

하위 도메인은 도메인 주도 패턴(DDD,Domain-Driven Design)의 요소로 '개별 팀이 상품(products)으로서 서비스를 소유'함은 MSA의 철학과 일치하기에, MSA의 분해 과정에서 자주 이용된다.

 

여기서 비즈니스에 관련된 엔티티의 집합을 Aggregate라 하며, 그 집합에서 도메인의 중심에 해당하는 엔티티를 Root라고 한다.

아 그래서 하위 도메인이 뭔데요?;;;

상호 배타적인 문제를 해결하는 Aggregate간의 관계와 각 Aggregate별로 Aggregate Root로 분리된 Aggregate가 하위 도메인이 된다.

그리고 이때 분명한 Bounded-Context를 가지는 하위 도메인들을 각 서비스로 식별한다.

...

 

하지만 게임 도메인에서 Bounded-Context만으로 분리하기에 너무 자잘한 기능들이 마이크로 서비스로 떨어져 나간다고 판단했고, 별도의 리소스를 들여 마이크로 서비스로 두지 않고 합친게 dedicated-service이다. 

 

굳이 분리하자면 board-service, exchange-service로 둘 수 있지만 공지사항 CRUD는 사용자들의 권한도 없을 뿐더러, 대부분 조회만을 목적으로 한다. 

 

환율 기능을 좀 더 고도화시켜서 복잡한 로직이 나오던 트래픽 부하를 나눌 필요가 생기던 하면 그때 가서야 분리할 계획이고, 진정한 의미에서의 Bounded Context를 가지도록 하위 도메인들을 분리한다고 볼 수 있겠다.

 

2. 마이크로서비스간의 IPC 통신

결국 Async IPC인 Kafka 채택하긴 했지만..

Queue에 Consume하면 message를 다루던 서버가 종료되는 경우, 큐는 성공 여부를 알 수 없어서 중복된 메시지 수신하는 단점이 있다. 정상적인 데이터가 온게 맞는지 모듈간의 통신을 IPC로 추가 구현해줘야하는데 사실 자신은 없다

 

모놀리식 멀티모듈 환경과 다르게 모듈 각자가 하나의 서비스(프로세스)이기에 함수 호출로 통신할 수 없다.

IPC 패턴 - 동기적 패턴

가장 간단하게 구현하는 방법은 Http 방식과 gRPC 방식이 있다.

평범하게 RESTful하게 통신하는게 가장 마음은 편하다.

하지만 MSA 환경에서는 POST 메소드의 경우에도 멱등성(Idempotence)를 염두해야함을 명심해두자.

다른 서비스로부터 동일한 요청이 여러 번 전송되는 상황을 가정할때, POST 요청의 경우 리소스의 상태를 보존하지 않기에 치명적인 문제점으로 이어질 수 있다.

gRPC(google Remote Procedure Call, 원격 프로시저 호출 프레임워크)는 Protocol Buffer 기반의 서버간의 호출을 돕는다.

깊게 알아보진 못했지만 빠른 속도와 위험성이 수반한다고만 알아두고 있다.

IPC 패턴 - 비동기적 패턴

메시지 큐잉을 활용해서 Produce-Consumer 방식으로 데이터 통신하는 기법. (MQTT, AMQP)

리소스 소모가 많은 작업, 한정된 리소스를 가진 경우는 비동기적 패턴을 이용하는게 더 성능적 이점을 살릴 수 있다는 장점이 있다.

하지만 큐잉의 특성상 누락될 일은 없는 대신에 중복된 메시지를 받을 일이 빈번해진다.

이미 Consume한 데이터를 다루던 서버의 장애 발생시, 큐 입장에서 결과를 알 수 없다는 단점이 있다.

이 부분을 해결해보고 싶어서 Kafka를 선택한건데 아직 모르겠다.

성공.

 

가장 편하게 모듈간의 IPC 통신을 구현하고 각 모듈간의 오류 로깅을 위해 Logging 기능만 제공하는 서비스를 생성했다.

간단하게 특정 topic에 해당하는 message를 Poll 방식으로 불러오는 구조로 Kafka-ui를 통해 간단하게 시각화해서 확인했다.

해당 로깅 서비스를 구축한 이유로는 노드 상태뿐만 아니라 서비스간의 호출 상태를 파악해야 장애 상황 발생시 tracking이 쉽다.

Latency나 메소드 호출 증감을 확인해서 사용자 데이터 분석에도 용이함

Not a managed type: class java.lang.Object 오류

 보통 정상적으로 Entity가 등록안돼서 생기는 오류라 @Entity 기입해주면 해결되는걸로 알던 녀석.

나같은 경우는 Spring 2.x에서 3.x 마이그레이션도 동시에 진행하면서 javax 문법들을 jakarta로 바꾸지 않고 그대로 의존성 주입해서 사용했기에 생긴 오류였다.

 

 

3. Transaction 관리

가능하면 테크니컬하게 해결하는게 중요함은 충분히 인지하고 있으니, 구현된 기능 마이그레이션 과정에서 다시 포스팅하겠음.

 

사실 굉장히 무섭다!!!! 가능하면 피하고 싶은 주제일만큼 어렵다!!!

 


 

 

 

결국 기능 구현에 초점을 맞춘 프로토타입의 고도화 과정에서 본격적인 서비스로 발돋음시키는 기회가 되었다. 클라이언트 기능 구현은 엣저녁에 다 했는데.. 디자인도 미뤄둔게 많고 상용 수준의 출시 준비도 미비하다.

더뎌지는 가장 큰 이유는 마케팅 전략 부재와 수많은 작품들에 밀려 다운로드 수 0에 쳐박힐지 모른다는 두려움