폴리글랏이란 무엇인가
아니 근데
왜 다들
요즘 서비스 아키텍처 구상하면서 큰 의문이 들었습니다. 규모가 조금이라도 큰 조직이면 자바나 스프링 강박증이라도 없었다면 높은 확률로 각자가 쓰고 있는 기술 스택이 다를 것입니다. 제가 느낀 위화감이 그것이었습니다. NodeJS, PHP, Java, Go, C++(?) 등으로 이루어진 API 및 웹소켓 서버들이 있습니다. 이 서버들에 대한 공통된 모듈에 대해 항상 큰 의문이 있었습니다.
같은 솔루션에 대한 접근이라면 같은 모듈을 써도 되는 거 아니었을까?
특정 언어의 클라이언트 라이브러리가 비교적 우수한데, 그걸 공통적으로 쓸 수 없을까?
그래서 말인데
솔직히 저는 NodeJS에 대해 성능적 회의를 느끼고 있습니다. 아무리 NodeJS가 성능이 좋다고 하더라도, NestJS가 생산성이 좋다고 하더라도, 결과적으로 제 눈에 보인 성능들은 상당히 좋지 않았습니다. 그리고 그 원인으로 노드 진영에서 자랑하는 IO 성능을 차치한 다른 부분, 컴퓨팅과 같은 단일 스레드 이벤트 큐 구조에서 발생하는 태생적 한계라고 보고 있습니다.
그걸 해결하기 위한 방법에는 무엇이 있을까? 왜냐면 전 NestJS가 가지는 특유의 생산성을 부정하고 싶지 않습니다. 전 비록 Go나 Rust, 하다못해 asp.net core로 간단하게 HTTP API를 작성하는 게 더 편하긴 합니다. 그럼에도 NestJS가 가지는 API 작성 측면에서 가지는 편의 기능들과 GraphQL에 대한 지원, 그리고 CLI 툴의 사용성은 좋은 편이죠.
그리고 무엇보다 Go의 rueidis나 pgx가 상당히 매력적입니다. Rust의 kafka 클라이언트도 그렇구요. 여기에 더해 Go와 Rust는 C++를 제외한 다른 스택과 달리 훨씬 적은 리소스를 사용하여 사이드카로 붙이기에 부담이 적다는 장점이 있습니다.
그래서
어떻게 하자고요?
사이드카를 고려해봅시다. 별건 아니고, 하나의 API 서버에서 특정 역할을 하는 모듈을 별도로 분리하여 컨테이너로 옆에 띄우거나 별도 프로세스로 띄우는 겁니다. 그리고 Unix Socket을 통해 OS 레벨에서 API 서버와 사이드카가 요청과 응답을 주고 받도록 합니다. 그러면 성능을 크게 헤치지 않고 비교적 무겁거나 더 좋은 기능, 혹은 두번 만들기 어려운 로직 등을 한번만 개발 및 배포해서 사용할 수 있게 되죠.
내가 대신 통신할게!
앰버서더(Ambassador) 패턴은 특정 대상과의 통신을 대신 치루는 사이드카입니다.
- Kafka 로그 앰버서더: 카프카로 전송할 로그들을 대신 받아서 처리합니다. 이 앰버서더는 단순히 카프카에 대한 커넥션 풀만 관리하지 않고, 필요에따라 재전송과 배치 전송 등의 부차적인 고급 기능을 담당합니다.
- PostgresQL 쿼리 엠버서더: 포스트그레에 요청할 쿼리를 대신 받아서 처리합니다. 이 엠버서더 또한 단순히 단건 SQL 요청에 대한 요청과 응답만 프록싱하는 것이 아니라 필요에 따라 싱글플라이트, 응답 캐시, 배치 인서트 등의 기능을 구현해서 추가할 수 있습니다.
- Redis 쿼리 엠버서더: 레디스에 요청할 쿼리를 대신 받아서 처리합니다. 이 엠버서더는 Go의 Rueidis를 의식한 엠버서더입니다. Auto Pipeline, Client Side Cache 등을 적극적으로 활용하여 레디스에 가해지는 부하를 상당히 줄일 수 있을 것입니다.
내가 대신 연산할게!
오프로더(Offloader) 패턴은 특정 CPU에 무거운 연산을 대신 해주는 사이드카입니다.
- Image Resizer 오프로더: 이미지 업로드를 수행할 때 높은 확률로 이미지 리사이징이 필요합니다. 이때 노드나 파이썬으로 수행하면 생각보다 이 작업이 오래 걸립니다. 하지만 Go나 Rust로 하면 생각보다 금방하죠.
- Password Hash 오프로더: 유저 인증을 위해 비밀번호를 해싱할 때, 저희는 bcrypt같은 무거운 해시 연산을 사용합니다. 이 작업을 노드 환경에서 그냥 호출하게 되면 전체 레이턴시가 밀리게 되죠. 대신 Go나 Rust로 작성한 오프로더에 전달하고 결과만 IO를 통해 받으면, 노드 입장에선 CPU 집약적 작업이 IO 집약적 작업으로 전환되어 전체 레이턴시에 긍정적인 효과를 불러올 수 있습니다.
그럼 어떻게 통신하죠?
기 작성했듯 Unix Socket을 이용하면 OS 내에서 커널까지만 갔다오면 되기에 Loopback TCP보다 빠르게 동작할 수 있습니다. 그리고 gRPC는 다행히 Unix Socket을 통해서도 이용할 수 있죠. 즉, gRPC와 protocol buffers로 여러 언어에 동일한 API 스펙과 DTO를 정의할 수고를 덜 수 있습니다. 그러면 특정 사이드카에 대한 서버(엠버서더나 오프로더)와 함께 각 언어에 대한 gRPC 생성물을 제공하는 레포를 만들면 쉽게 여러 언어에 제공할 수 있는 셈이죠. 또한 거기에 Buf를 통해 protocol buffers 파일을 관리하고, 각 언어별(Node, Go, Rust, PHP 등) SDK 코드를 자동으로 생성하도록 하여 생성 자체의 번거로움도 줄일 수 있죠. 이에 대해선 제가 만들어 놓은 템플릿 레포를 참고해주세요.
마지막으로
개인적으로 이 사이드카 형태가 조직 구조에도 영향을 줄 수 있을 것같습니다. API 서버 및 비즈니스 로직 개발자와 인프라 및 데이터베이스 관련 개발자가 각자의 전문 분야에서 더 나은 형태의 모듈을 만들어 붙이는 형태를 도모하여 조직 전체의 전문성 향상과 상호 자극에 좋은 영향을 주지 않을까 개인적으로 기대되는 부분입니다.