FanUP은 비대면 팬미팅 플랫폼입니다.
해당 시리즈는 프로젝트를 진행하며 겪은 설계 고민, 트러블 슈팅 과정에 대해 다룹니다.
FanUP 프로젝트에 적용한 MSA, API Gateway 패턴에 대해 다룹니다.
Why: 왜 도입했는가?
How: 어떻게 적용했는가?
What: 어떤 결과가 도출되었는가?
FanUP 은 비대면 팬미팅 플랫폼으로 기획 단계에서 팬과 아티스트를 위한 다양한 기능을 계획했습니다.
자세한 기능은 아래 그림을 참고해주세요.

위 기능 중 특정 기능(티켓, 채팅)의 경우 대량의 트래픽이 유입되는 상황을 가정하였습니다.
만일 이를 Monolithic Architecture로 구현한다면 각 도메인의 장애는 단일 장애점으로 동작합니다.
예를 들어 A팬미팅에 참여하기 위해 티켓팅을 하는 과정에서 서버가 견디지 못할 정도로 대량의 트래픽이 유입되어 장애가 발생하더라도 이미 진행 중인 B팬미팅에는 영향을 미치면 안됩니다.
장애 위험이 있는 서비스의 코드를 분리하여 별도의 서버에서 처리한다면 티켓팅 과정에서 장애가 발생하더라도 다른 서비스에는 영향을 미치지 않을 것입니다.
초기 기획부터 채팅 서비스는 socket.io를 활용해 기능을 구현하기 위해 별도의 socket 서버를 분리할 예정이었습니다.
티켓팅 서비스 역시 장애가 발생할 상황을 우려하여 서비스 분리를 고려하는 과정에서 '이번 기회에 직접 MSA를 한 번 겪어보며 공부해보자'라고 팀원들과 합의가 되어 MSA를 도입하기로 결정합니다.
1. Monolithic Architecture: 한 개의 서버 코드로 모든 비지니스 요구사항을 처리하는 아키텍처
2. 단일 장애점: 시스템 구성 요소 중, 동작하지 않으면 전체 시스템이 중단되는 요소
3. socket.io: 웹 클라이언트와 서버 간의 실시간 양방향 통신을 가능케 하는 라이브러리
본 프로젝트의 서버파트는 Nest.js 프레임워크를 사용하였습니다. Nest.js에서는 Microservices 내장 모듈을 제공하여 외부 로드밸런서 없이도 MSA를 구축할 수 있었습니다.

대량의 트래픽 대응을 위해 분리할 필요가 있다고 생각했던 채팅 서비스와 티켓팅 서비스를 별도의 서버로 분리합니다.
또한 각 회원/인증 서비스도 분리하였는데 이에 대한 내용은 API Gateway 부분에서 더 자세히 다루겠습니다.
나머지 서비스는 트래픽이 많지 않을 것이라고 가정하여 하나의 서버에 묶어서 분리하였습니다.
(사실 모두 분리하고 싶었지만 시간의 압박으로 인해... 아쉬움이 남는 부분입니다.)
데이터베이스의 경우 각 Server 별로 독립적인 데이터베이스를 구축하고 싶었지만
하나의 데이터베이스를 생성하고, 각 서버 별로 외부 스키마를 분리하여 제공하는 방식을 채택했습니다.
MSA의 도입 결과로
클라이언트-마이크로 서비스 간 직접 통신 대신 API gateway를 고려하는 이유는
이외에도 여러 가지가 있지만 그 중에서도 이번 글에서는 공통 인증 절차 수행에 대해 다루겠습니다.
인증 및 인가는 서비스 전체에 걸처 필요한 공통된 기능입니다.
모든 서비스들은 요청된 명령을 처리하기 위해 해당 요청의 사용자(Client) 인증이 필요하며 이로 인해 인증 서비스와 강한 의존성을 갖게 됩니다.
서비스 간 의존성이 생긴다는 것은 장애가 발생할 경우 의존성이 있는 서비스까지 영향을 받게 된다는 뜻이며 이렇게 된다면 MSA를 도입한 의미가 없어집니다.
인증 과정에서 세션 방식, gateway pattern을 적용하지 않은 토큰 방식의 문제점은 다음과 같습니다.

세션은 서버에 정보를 저장하기 때문에 stateful하다는 특징이 있습니다.
하지만 이는 클라우드 환경에서 유연한 스케일 인/아웃이 이루어짐에 따라 로드밸런싱하는 MSA 환경에서 적용하기 어렵습니다.
sticky session을 사용한다고 하더라도 특정 서버에만 트래픽이 몰리는 문제가 발생할 수 있습니다.

반면 클라이언트에 정보를 저장하는 토큰방식은 stateless합니다.
요청 처리에 필요한 정보를 클라이언트가 제공하기 때문에 어느 서버로 요청을 보내더라도 문제 없습니다.
하지만 위 그림의 경우 각 마이크로서비스에서 토큰의 유효성을 판단하는 로직이 필요합니다.
즉, 각 마이크로서비스는 jwt를 decode하기 위한 secret key를 가지고 있어야하며 중복되는 인증 관련 코드를 가지게 됩니다.
Nest.js에서는 Microservices 내장 모듈을 제공하여 각 마이크로 서비스를 연결할 수 있습니다.
해당 방식을 사용하여 gateway 단에 인증 로직을 추가하여 권한이 필요한 요청에 대해 인증 작업을 수행하게 했습니다.
로그인을 하는 경우 아래 그림처럼 Auth Server로 요청을 전달하여 사용자에게 jwt를 발급합니다.

클라이언트가 요청을 전송한다면 gateway 단에서 jwt 인증 로직을 수행하게 됩니다.
만일 jwt가 유효하다면 인증된 사용자 정보를 요청에 추가로 첨부하여 뒷단 서비스에게 전달합니다.
뒷단 서비스는 사용자의 유효성을 걱정할 필요 없이 비지니스 로직에만 집중할 수 있게 됩니다.
(인증되지 않은 사용자라면 gateway가 필터링해주기 때문이죠.)

API gateway pattern을 도입하게 되어 인증 로직을 중앙 집중화하여 관리할 수 있었습니다.
또한 gateway에서 공통된 인증 절차를 수행하면서 뒷단의 서비스들은 인증 방식으로부터 완전히 독립되어 인증 서비스와 의존성이 사라지고, 비지니스 로직에만 집중할 수 있게 되었습니다.

하지만 모든 마이크로서비스가 API gateway를 통해 연결되므로, API gateway는 시스템에서 단일 장애점으로 동작할 수 있습니다. 이러한 상황을 방지하기 위해서는 이중화 작업이 필요합니다. API gateway를 두 개 이상으로 복제하고, 부하 분산을 수행하여 장애 발생 시에도 다른 gateway가 요청을 처리할 수 있도록 추가적인 조치가 필요할 것입니다.
사실 프로젝트에 MSA를 도입하는 것은 분명 오버엔지니어링이라는 생각이 들었습니다. 하지만 경험을 쌓으며 공부해보자는 취지가 더 컸고, 지금까지 접해보지 않은 내용을 구현하는 것이기에 어느정도의 난관은 예상했습니다.
실제 프로젝트를 진행해보니 monolithic architecture로 서버를 구성했을 때보다 훨씬 많은 부분을 신경써야 했습니다.
또한 개발에는 명확한 정답이 있는게 아니다보니 많은 의문이 생겼고, 현업에서는 이를 어떻게 처리하고 있을지 궁금해졌습니다.
rest api나 grpc 등의 동기식 통신을 사용하면 결국 서비스 간 의존성이 생기는 것 아닌가?아직까지 위 의문에 대한 자신만의 답을 찾지는 못했지만 (서비스 특징과 트래픽의 규모에 따라 다르겠죠..?) 그래도 많은걸 배울 수 있었습니다.
요즘 유행으로 많은 기업에서 MSA를 적용하는 모습을 보이는데, 프로젝트를 진행하며 여러 아티클을 접하다 보니 MSA를 도입하기 앞서 해당 기술 도입이 꼭 필요한지 파악하는 것이 우선이라는 내용을 많이 볼 수 있었습니다.
MSA 뿐만 아니라 다른 기술에 대해서도 단순히 유행만 따르기보다는 해당 기술을 본인의 프로젝트에 적용했을 때, 어떤 효과를 얻을 수 있을지 장단점을 명확히 따져보는 자세가 중요할 것 같습니다.
좋은글 재밌게 읽었습니다 :)