회사에 들어와서 처음 맡은게 멀티 태넌시로 이루어진 모듈이었다.
일단 멀티태넌시라는 말을 처음 들어봤고, SaaS도 처음 하는 거라서, 부담감이 있었다.
그렇게 인수인계를 받고 모듈 작업을 했다.
좋은 경험이였다.
설계를 정리 해서 사내에 공유를 했다. 입사하고, 꼭 글을 공유하는게 목표였는데 드디어 이루었다.
내용을 블로그에도 올려본다. 누군가는 도움을 받겠지.
+ 자세한 내용을 못 적는 점은 양해 바랍니다
맡은 서비스는 다양한 타서비스에서 서비스를 제공받게 모듈화를 하는 멀티 태넌시 아키텍처를 가진 SaaS 모듈이다.
기존에는 클라이언트와 서버가 1:1 = 클라이언트:서버
로 서비스를 유지했다면, 멀티 태넌시 아키텍처에서는
N : 1 = 클라이언트:모듈
의 형태로 하나의 모듈에 다양한 형태의 서비스가 붙을 수 있습니다.
여기서 클라이언트가 될 서버, 서비스를 총칭하여 태넌트(tenant)라고 칭합니다.
태넌트는(tenant)가 임차인 임을 인지하면, 이해하기가 쉬울 것 같습니다.
하나의 아파트 아래, 여러 사람이 세를 들어 살 수 있는 것처럼, 하나의 모듈에 여러 클라이언트가 붙는 구조입니다.
1. 인프라 아키텍처
[아키텍처 요구 사항]
멀티태넌시 아키텍처에서 서버는 각 태넌트를 독립적으로 처리해야 합니다.
예를 들어, 네이버나 구글이 저희 서비스를 받길 원한다면, 이들이 태넌트가 될 것입니다.
이때 각 태넌트의 데이터는 철저히 분리되어야 합니다. 네이버의 유저 데이터를 다른 태넌트에서 조회할 수 있어서는 안 됩니다.
AWS는 좋은 예시입니다. 같은 리전에서 물리적 인프라를 제공하지만, 클라우드에서는 각 클라이언트의 인프라가 철저히 분리되어 있습니다. 이는 같은 아파트에 살지만, 옆집 사람의 일상을 알 수 없는 것과 비슷합니다.
[SaaS 모듈의 아키텍처]
멀티 태넌시가 인프라를 구성하는 방법에는 Pooled/Silo 두가지 방식이 있습니다.
Pooled 방식은 모든 태넌트가 하나의 인프라를 공유합니다. 같은 EC2, 같은 RDS서버를 사용합니다.
Silo 방식은 각 태넌트마다, EC2, RDS등 인프라 자체를 독립적으로 분리하는 것입니다.
2. 런타임에서 어떻게 멀티 태넌시를 구현할 수 있을까?
NestJS에서는 기본적으로 모든 프로바이더가 싱글톤으로 설정됩니다. 하지만 싱글톤 프로바이더는 애플리케이션 전역에서 공유되기 때문에 멀티태넌시 환경에서는 각 태넌트가 독립된 인스턴스를 가져야 합니다.
이를 위해 Injection Scope을 Request로 설정하여 각 요청마다 새로운 인스턴스를 생성할 수 있습니다. [공식문서]
👉 A new instance of the provider is created exclusively for each incoming
request The instance is garbage-collected after the request has completed processing.
SaaS 모듈은 요청이 들어올 때, 태넌트를 식별하여 해당 태넌트에 맞는 인스턴스를 매핑 해야합니다.
태넌트 식별을 위해, 2가지 방법을 사용합니다.
[태넌트 식별 및 인스턴스 매핑]
- 태넌트 id
- 태넌트마다 id를 부여하여 api 요청 시 header에 id를 넣어달라고 요청합니다.
- 태넌트가 헤더에 id 를 넣어서 request합니다.
- 모듈은 id에 해당하는값에 해당하는 인프라 주소를 Injection을 할 때, 전달합니다.
- 태넌트마다 다른 DI tree를 만들 수 있습니다
- proxy api
모듈은 클라이언트(웹 프론트) 또한 멀티태넌트로 이루어져있습니다.
클라이언트가 api마다 고정된 id를 넣을 수 없습니다 (코드에 switch문 지옥을 이루지 않는 이상..)
이를 위해, 각 태넌트 서버는 다음과 같은 역할을 하는 api /proxy 를 생성합니다.
만약 모듈의 /login을 호출하고 싶다면
1. 모듈 프론트가 GET {태넌트 서버 URL}/proxy/login로 호출
2. 태넌트 서버를 우선 거치게 됩니다.
3. 이때 id를 헤더에 붙여서 {모듈 서버 URL}/login 으로 모듈 서버로 다시 보내줍니다.
- 태넌트마다 id를 부여하여 api 요청 시 header에 id를 넣어달라고 요청합니다.
2. Hostname
또 다른 태넌트 식별 방법은, request 객체의 Hostname을 사용합니다.
hostname은 어떤 주소로 요청을 받았는지를 의미합니다.
예를 들어, www.naver.com으로 request 요청을하면 네이버 서버에서 우리의 request 객체의 hostname을 파싱할 경우 naver가 나올 것입니다.
1. 와일드 카드 도메인 등록
우리는 모듈의 도메인을 *.module.com 로 등록했습니다.
2. 태넌트 별로 다른 도메인 부여
태넌트에게 서버 도메인을 알려줄 때 각기 다른 도메인을 알려줍니다. 예를 들어 Naver와 구글이 우리 서비스를 연동하고 싶다면, 다음과 같이 알려줍니다.
- 구글 : google.module.com
- 네이버 : naver.module.com
3. 서버에서 태넌트 식별
이제 nestjs에서 request객체를 파싱하면, hostname으로 google,naver 가 나올 것입니다.
이를 위에서 부여한 id와 매핑한 테이블을 서버에 저장하면 어떤 태넌트인지 식별할 수 있습니다.
HOSTNAME_TO_TENANTID = {
"naver": "naver-id",
"google": "google-id"
}
TMI
회사에서는 그림도 넣고, 코드도 넣고 했는데 아무래도 개인 블로그에서는 막상 다 빼니깐 되게 글이 빈약해보인다.ㅋㅋ
나는 기반이 잘 된 모듈에 고도화를 했다고 보면 되는데, 정말 인계자 분이 초반 설계를 잘 해놓으셨었다. 대단하다고 생각한다.
여기엔 쓰지 않았지만, 어떤 서비스와 연동하는 아키텍처를 설계할 떄 거의 2-3일을 소요했는데,
결국 CTO님이 조언주신 것은, 가장 단순한 책임을 갖게 설계해라였다.
결국 함수든, 서비스든, 단일 책임 원칙은 어디나 국룰임에..ㅋㅋ
단순하고 뻔한 아키텍처가 결국 모두가 이해하고 유지보수가 잘되고,...
다양한 경험을 하면서 고민을 하고 공유할 자리가 많았으면 좋겠다
'회사에서 한 일' 카테고리의 다른 글
팬아웃 아키텍처를 활용한 320만개 쿠폰 안정적으로 배포하기 (9) | 2024.12.30 |
---|---|
DynamoDB에서 Redis로: 26억 건 데이터 마이그레이션과 비용 최적화 (5) | 2024.11.10 |
docker-compose로 테스트 환경 구축하기 (4) | 2024.09.08 |
DynamoDB 인덱스 개선으로 비용 절감하기 (3) | 2024.09.01 |
Multi Tenancy 아키텍처에서 Serverless로 Cron Job 수행하기 (0) | 2024.04.10 |