분산 서비스에서의 R&R에 대한 고찰
이것 또한 최근 포스트와 마찬가지로, 최근에 진행했던 프로젝트의 회고입니다.
개요
해당 프로젝트에는 개인적으로 몇가지 이해가 어려운 부분들이 좀 있었습니다. 그 중 R&R에 대해 제가 품은 의문과 그에 대한 제 나름의 해결 방법을 서술하고자 합니다.
본문
어떤 부분이?
- API GW와 서비스만 책임 분리
- API GW와 인증 및 인가 책임
- 각 서비스 간의 역할과 책임 분리
API GW와 서비스간의 책임 분리에 대해
설계 당시부터 지금까지 모든 서비스는 k8s에 pod로써 운영되고 있습니다. 대략적으로 네트워크 요청 트래픽은 ingress -> API GW -> each service
로 흐릅니다. 이 중 API GW는 솔루션이 아니라 자사에서 내부적으로 만든 것입니다. 기능으론 일반적인 API GW와 유사하게 요청에 대해 특정 서비스로의 라우팅과 요청에 대한 인증 토큰 검증 등이 있습니다. 그리고 각 서비스는 특정 데이터를 다루는 서비스와 저장하는 DB를 세트로 구성됩니다. 제가 보는 문제는 이 구조로 인해 생성되는 gray zone이 있습니다.
그래서 데이터 집계는 누가 할건데?
A 서비스가 B 서비스의 데이터가 필요하면, 어떤 식으로 데이터를 요청하고 처리할 건가요? 만약 웹에서는 C 서비스의 데이터까지 필요한데, 모바일 앱에서는 C 서비스의 데이터가 필요없다면, 굳이 요청해서 가져올건가요? 누가 요청자가 필요한 데이터인지 파악하고, 요청을 스케쥴링하며, 종합해서 돌려줄 건가요? 어떤 구조를 추가하고, 어떤 통신 방식을 선택할 건가요?
너무 심플한 구조를 가진 나머지, 위 의문을 해결하는 것에 어려움을 겪을 수밖에 없습니다. 실제 제가 본 케이스에선 API GW에서 데이터를 집계하는 방식과 각 서비스들이 통신해서 데이터를 집계하는 방식, 둘 다 쓰이고 있었습니다. 제가 생각해봤을 때, 이 해답은 크게 2가지로 해결할 수 있어 보입니다.
- aggregator 패턴과 BFF(Backend-For-Front) 패턴을 활용한 레이어 추가
- MQ나 서비스 메시를 활용한 비동기 통신을 위한 방향성을 상하로 제한
aggregator, 혹은 BFF를 사용하게 되면, 필연적으로 네트워크 요청 트래픽은 ingress -> API GW -> aggregator/BFF -> each services
로 흐르게 됩니다.
글을 단순하게 만들기 위해 BFF는 어그리게이터로 통칭하겠습니다.
위에 제가 작성한 모든 문제를 어그리게이터에게 일임하게 되면, 저 질문은 굉장히 단순한 형태를 띄게 됩니다.
- 여러 서비스의 데이터가 필요한 API 콜에 대해서, 어그리게이터가 여러 서비스에 요청을 하면 됩니다.
- 특정 플랫폼에서만 필요한 데이터는 라우팅을 달리해서, 특정 라우트마다 추가 동작을 수행하도록 할 수 있습니다.
- 요청 스케쥴링과 집계는 문자 그대로, 어그리게이터가 수행하면 됩니다.
만약 API GW에서 어그리게이터를 거쳐서 서비스를 호출하는 것이 부담이 된다면, API GW 자체를 여러개로 나누어서 하나의 ALB -> API GW가 아니라 하나의 ALB -> multiple aggregator로 바꾸는 방법도 제안할 수 있습니다. 어떤 방식으로 구현하든, 차후 필요에 따라 MQ나 서비스 메시를 통해서 aggregator와 서비스 간에 요청과 응답을 주고 받을 수 있게 프로토콜을 정의하고 구성하면 될 것같습니다.
API GW와 인증 및 인가 책임
단일 API GW만 존재한다면, 인증 서비스 하나만 있어도 큰 문제가 되진 않을 것입니다. 다만 인증 자체를 ALB에서 하지 않거나, 단일 진입점에서 처리하지 않을 경우, 그리고 네임스페이스가 분리되어 서비스들이 구성되면서 통합되는 형태로 흘러갈 경우에는 인증 서비스가 특정 한 네임스페이스에 일부분으로써 포함되는 것 등에서 문제가 될 수 있습니다.
만약 A 네임스페이스가 인증 서비스를 포함하고, 인가도 수행하며 검증도 하게 된다면, B 네임스페이스는 보안이 필요한 요청에 대한 처리가 필요할 때마다 A 네임스페이스 중 인증 서비스에 직접 어떤 식으로든 통신을 하거나, A 네임스페이스가 내보내준 어떤 인터페이스를 통해 토큰 전체를 제공해줘야합니다.
하지만 토큰을 전부 제공하는 건 중간에 탈취되었을 때의 위험성만 증가시키고, A 네임스페이스와 A 네임스페이스의 인증 서비스에 전체적인 부담만 늘릴 뿐입니다. 그러면 A 네임스페이스의 인증 서비스가 어떤 식으로든 구멍을 뚫어서 B 네임스페이스가 직접 통신하게 되면 어떤가요? B 네임스페이스 전체가, 혹은 다른 네임스페이스들도 A 네임스페이스와 통신하는게 아닌, 자기보다 더 작은 인증 서비스와의 통신으로 그려지게 될 것입니다. 이렇게 되면 서로 다른 레이어 사이의 통신이 되므로, 전체적인 설계의 복잡도가 증가하게 됩니다.
그런 면에서 기존에 있던 방식 중 하나인,
- A 네임스페이스의 인증 서비스가 비대칭키 서명으로 토큰을 생성 및 인가
- 다른 네임스페이스에서 검증이 필요할 경우 A 네임스페이스에 해당 유저와 토큰의 퍼블릭 키 요청
- A 네임스페이스는 내부에 요청을 전달하여 인증 서비스나 DB에서 조회 후 퍼블릭 키 제공
- 다른 네임스페이스에서는 해당 퍼블릭 키로 토큰 검증 후 프로세스 처리
흐름을 생각했습니다. 이를 구성하는 표준안으로 JWK도 있으며, 필요에 따라 protobuf나 flatbuf로 쉽게 구현할 수 있을 것입니다.
각 서비스 간 역할과 책임 분리
개인적으로 스트레스를 많이 받았고, 받고 있는 부분입니다. 기본적으로 모든 라이브 및 VoD 영상에 대한 메타데이터와 파일은 저희 서비스와 DB가 가지고 있습니다. 하지만 별도의 서비스에서 VoD 전체 길이와 VoD 번호를 지정하게 되고, 거기서 파생되는 것들에 대한 정보를 저장하고 있습니다.
다른 케이스로는 RTMP 서버는 저희 서비스에 포함되어 있지만, 별도의 다른 서비스가 publish key를 가지고 있으며, 재생성에 대한 유저의 요청도 처리합니다. 저희 쪽에서 방송 시작 요청이 RTMP로 들어오면, 저희 서비스에서 요청을 보내서 검증을 하게 됩니다.
방송 종료에 대한 처리 또한, 저희 서비스에서도 별도 DB에 기록하며 관리하기도 합니다. 다만 특정 채널에 대해 라이브 중인지 아닌지에 대한 판단을 별도 서비스에서 하기 때문에, 해당 서비스에 메시지를 보내서 방송 상태 변경을 처리하게 됩니다. 이러한 구조 때문에, 방송 상태에 대한 라이프타임 관리와 시청자가 메시지를 받을 때까지의 홉도 늘어나 문제가 발생할 구간도 커지며, 전체적인 구성도 복잡해집니다.
왜 이러한 아키텍처를 가지게 되었는 지는 확실치 않습니다. 저희 서비스 내부에 대한 것 외에는 제가 결정에 참여하지 못 했기에, 어떠한 이유로 이렇게 되었는 지는 모릅니다. 다만 의심가는 부분은 몇가지 있습니다만, 그 중 가장 크다고 느낀 프론트엔드 관점에서 백엔드 아키텍처를 설계에 대해서만 서술하겠습니다.
대체로 서비스들이 ingress -> API GW -> each service
로 라우팅 되고 있습니다. 이는 모놀리스 아키텍처에서 프로세스만 분리해놓은 것과 차이가 없습니다. 그리고 전반적인 라우팅 구성이 프론트엔드의 요청 단위로 분리되어 있습니다. 그렇기 때문에, 프론트엔드에서 publish key에 대한 CRUD, 방송 개설 및 종료 여부에 대한 요청이 들어오니 별도 서비스로 분리
, VoD에 대한 요청은 프론트엔드에서 하므로, VoD를 관리하는 별도 서비스에서 관리하도록 분리
같은 이상한 의사 결정이 들어가지 않았나 예상합니다.
그러면서 자연스럽게 도메인 별로 네임스페이스와 서비스가 분리되지 못 하고, 업무 분장까지 이루어져서 아키텍처가 굳어지게 됩니다. 도메인 별로 아키텍처가 구성되지 못 하여, 어떠한 작업을 할 때도 서로의 영역에 대해 신경을 써야하고, 전반적으로 경직되게 조직이 굴러가게 됩니다. 그를 타파하기 위해 R&R을 애매하게 가져가서 모두에게 모든 걸 하라고 하면, 일부 사람들의 일만 가중되고, 하다보면 내로남불과 같은 케이스가 발생하게 될 수 있으며, 불만이 생기게 되면 조직의 퇴화로 이어지게 될 것입니다.
개인적으로는 프론트엔드의 요청은 API GW에서 생각하고, 백엔드에서의 데이터 구조와 처리에 대해선 도메인 단위로 가져가는 게 좋다고 봅니다. 그 사이에서의 절충안을 어그리게이터(BFF 포함)에서 해결 하는 것이죠.