멀티 테넌트(Multi-tenant) SaaS에서 테넌트별 개별 에이전트 메일함 구축하기
요약
멀티 테넌트 SaaS 환경에서 공유 이메일 주소 대신 테넌트별로 격리된 에이전트 계정을 구축하는 아키텍처를 제안합니다. 이를 통해 전송 신뢰도 오염을 방지하고, 수신 메일 라우팅 및 테넌트별 정책 관리를 효율화할 수 있습니다.
핵심 포인트
- 테넌트별 독립된 발신 ID를 사용하여 전송 신뢰도(Deliverability) 오염 방지
- 고유한 grant_id를 통한 수신 메일의 자동 라우팅 및 격리 구현
- 테넌트별로 상이한 전송 제한(Quota) 및 보관 정책 적용 가능
- 테넌트 이탈 시 권한 삭제만으로 깔끔한 데이터 정리 및 오프보딩 가능
이메일을 전송하는 대부분의 멀티 테넌트(Multi-tenant) SaaS 앱들은 하나의 공유된 ID를 통해 이메일을 보냅니다. notifications@yourapp.com과 같은 주소가 있고, 모든 고객의 메일이 이 주소를 통해 흐르며, 테넌트(Tenant)는 단지 제목에 찍히는 from_name이거나 교체 가능한 푸터(Footer)일 뿐입니다. 이는 문제가 발생하기 전까지는 괜찮습니다. 테넌트 A의 스팸 신고로 인해 테넌트 B의 전송 신뢰도(Deliverability)가 떨어지기 시작할 때, 고객의 답장이 하나의 거대한 수신함(Firehose inbox)으로 쏟아져 들어와 이를 다시 분산시켜야 할 때, 혹은 특정 테넌트가 다른 테넌트보다 더 엄격한 전송 제한(Send cap)을 원하는데 데이터 모델에 그런 기능을 전혀 구축해두지 않았다는 사실을 깨닫게 될 때까지는 말이죠.
그러니 공유하지 맙시다. 모든 테넌트에게 _자신만의 실제 메일함_을 부여합시다. 고객당 전용 **에이전트 계정(Agent Account)**을 제공하여, 각 계정이 고유한 grant_id, 고유한 발신 ID, 고유한 정책 및 제한 사항을 가지며, 자체 워크스페이스(Workspace)로 그룹화되도록 하는 것입니다. 수천 개의 라벨 해킹(Label hacks)을 사용하는 하나의 수신함이 아니라, 구조적으로 격리된 수천 개의 수신함을 만드는 것입니다.
저는 Nylas CLI를 다루고 있으므로, 아래의 터미널 명령어들은 제가 이를 연결할 때 실제로 사용하는 명령어들입니다. 모든 단계는 두 가지 관점, 즉 원시 curl 호출과 동일한 작업을 수행하는 nylas 명령어를 함께 보여줍니다.
왜 테넌트별 방식이 하나의 공유 발신자보다 나은가
공유 발신자 모델은 몇 가지 예측 가능한 지점에서 실패합니다. 테넌트별 에이전트 계정(Agent Accounts)은 이러한 문제들을 각각 해결합니다:
- 전송 가능성 영향 범위 (Deliverability blast radius). 모든 사용자가 하나의 주소로 메일을 보낼 때, 특정 테넌트의 반송률(bounce rate)이나 스팸 신고가 모두가 공유하는 평판(reputation)을 오염시킵니다. 테넌트별 계정 — 그리고 원한다면 테넌트별 도메인(domains) — 을 사용하면 한 고객의 잘못된 행동이 나머지 고객들에게 피해를 주는 것을 방지할 수 있습니다.
- 실제로 누군가에게 속한 수신 메일 (Inbound). 공유 발신자를 사용하면 답장이 하나의 메일함으로 돌아오며, 이를 테넌트와 수동으로 매칭해야 하는 상황에 처하게 됩니다. 각 테넌트가 고유한 권한(grant)을 가지면, 수신된
message.created이벤트는 이미grant_id를 포함하고 있습니다. 라우팅(routing)은 핸들러(handler)가 실행되기 전에 이미 완료됩니다. - 테넌트별 정책 및 제한 (Per-tenant policy and limits). 고객마다 규칙이 다릅니다. 일일 전송량이 낮게 제한된 체험판 테넌트가 있는가 하면, 더 높은 할당량(quota)과 더 긴 보관 기간(retention)을 가진 엔터프라이즈 테넌트가 있을 수 있습니다. 공유 발신자를 사용한다면 이 모든 것을 직접 구축해야 하겠지만, 여기서는 워크스페이스(workspace)에 연결된 정책으로 처리됩니다.
- 깔끔한 오프보딩 (Clean offboarding). 테넌트가 이탈(churn)할 때, 해당 테넌트의 권한(grant)만 삭제하면 됩니다. 해당 테넌트의 메일, 발신 정체성(send identity), 제한 사항 등이 단 한 번의 호출로 모두 사라지며, 안전하게 정리할 수 없는 공유 메일함과 얽히는 일도 없습니다.
권한(Grant)이 추상화의 핵심입니다
여기서 깊이 생각해 볼 점은 다음과 같습니다: 에이전트 계정(Agent Account)은 단지 grant_id를 가진 Nylas 권한(grant)일 뿐입니다. 데이터 평면(data plane)에서 새로 배울 것은 없습니다. 이미 알고 있는 모든 권한 범위 엔드포인트(grant-scoped endpoint) — 메시지(Messages), 초안(Drafts), 스레드(Threads), 폴더(Folders), 첨부 파일(Attachments), 연락처(Contacts), 캘린더(Calendars), 이벤트(Events), 웹훅(Webhooks) — 는 OAuth를 통해 얻은 Gmail 또는 Microsoft 권한에 대해 작동하는 방식과 정확히 동일하게 테넌트의 에이전트 계정에 대해 작동합니다. provider가 google 대신 nylas라는 점이 코드에서 볼 수 있는 유일한 차이점입니다.
따라서 "테넌트당 하나의 메일함"이라는 의미는 테넌트당 새로운 통합(integration)을 구축한다는 뜻이 아닙니다. 이는 동일한 코드 경로에 의해 구동되되, URL에 어떤 grant_id를 넣느냐에 따라 구분되는 N개의 권한(grants)이 존재함을 의미합니다. 테넌트 테이블에는 권한 ID(grant ID)라는 컬럼 하나만 추가될 뿐이며, 이미 구축한 다른 모든 기능은 그대로 작동합니다.
격리(isolation)는 한 단계 위인 두 가지 객체에서 이루어집니다:
- **워크스페이스 (workspace)**는 권한(grants, 보통 이메일 도메인별)을 그룹화하며, 하나의
policy_id와rule_ids배열을 가집니다. 워크스페이스 내의 모든 권한은 이 두 가지를 모두 상속받습니다. - **정책 (policy)**는 해당 정책을 가리키는 모든 워크스페이스 내의 모든 계정에 적용되는 제한 사항(발송 할당량, 스토리지, 보관 기간) 및 스팸 설정을 묶어줍니다.
따라서 아키텍처는 다음과 같습니다: 테넌트당 하나의 권한(grant)이 있고, 이들이 워크스페이스로 그룹화되며, 정책(policies)에 의해 관리됩니다. 이제 이를 구축해 보겠습니다.
시작하기 전에
두 가지가 필요합니다:
- API 키. 모든 요청은
Authorization: Bearer <NYLAS_API_KEY>를 통해 인증되며, 이 키는 귀하의 애플리케이션을 식별합니다. 여기의 예제들은https://api.us.nylas.com을 호출합니다. - 최소 하나 이상의 인증된 도메인. 에이전트 계정(Agent Accounts)은 도메인 상에 존재합니다. 이는 귀하가 등록하고 DNS 레코드를 게시한 커스텀 도메인이거나,
yourapp.nylas.email과 같은 Nylas 체험용 서브도메인일 수 있습니다. 새 도메인은 약 4주에 걸쳐 워밍업(warm up)이 필요하므로, 운영용 도메인은 미리 등록하십시오. DNS 설정 방법은 프로비저닝 문서에 설명되어 있습니다.
nylas init을 실행했다면, CLI는 이미 귀하의 애플리케이션을 가리키고 있습니다.
도메인 결정에 관한 참고 사항입니다. 이는 테넌트 격리(tenant-isolation) 설계를 결정짓기 때문입니다. 두 가지 형태가 있습니다:
- 하나의 공유 도메인, 테넌트당 하나의 주소. 모든 테넌트는
tenant-<id>@agents.yourapp.com을 할당받습니다. 운영하기 가장 간단합니다. 테넌트들은 여전히 격리된 권한, 편지함, 정책을 갖지만, 발송 도메인을 공유하므로 도메인 수준의 평판(reputation)을 공유하게 됩니다. - 테넌트당 하나의 도메인.
acme.youragents.com,globex.youragents.com형태입니다. 최대 수준의 격리를 제공합니다. 테넌트의 도달률 평판(deliverability reputation)은 완전히 해당 테넌트만의 것이 됩니다. 관리해야 할 DNS가 더 많아지며, 각 새 도메인은 별도로 워밍업됩니다. 테넌트가 자신의 도메인을 가져오거나, 평판 격리가 주요 판매 포인트(selling point)인 경우 이 방식을 선택하십시오.
단일 Nylas 애플리케이션은 등록된 수많은 도메인에 걸쳐 에이전트 계정을 관리할 수 있으므로, 두 방식을 혼합할 수 있습니다. 즉, 일반적인 고객(long tail)을 위해서는 공유 도메인을 사용하고, 필요한 계정에는 전용 도메인을 제공할 수 있습니다.
테넌트를 위한 메일함 프로비저닝 (Provision a mailbox for a tenant)
각 테넌트의 계정은 "provider": "nylas"와 settings.email에 있는 주소를 포함한 하나의 POST /v3/connect/custom 호출로 생성됩니다. 선택 사항인 최상위 name은 해당 계정이 보내는 모든 메일의 기본 From 표시 이름(display name)이 됩니다. 따라서 각 테넌트의 발신 메일에는 해당 테넌트 고유의 브랜드가 담길 수 있습니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
응답으로 data.id가 반환됩니다. 이를 테넌트 행(row)에 저장하세요. 이것이 해당 고객에 대한 이후의 모든 호출에서 사용될 grant_id입니다. 이 매핑(tenant → grant)이 전체 설계의 중추(spine)입니다.
CLI에서는 한 줄로 실행할 수 있습니다:
nylas agent account create acme@agents.yourapp.com --name "Acme Corp"
이 명령은 권한(grant)을 프로비저닝하고 해당 id, 상태(status), 커넥터(connector) 세부 정보를 출력합니다. 만약 애플리케이션에 기반이 되는 nylas 커넥터가 아직 존재하지 않는다면, CLI가 먼저 이를 생성합니다. 또한 API는 계정을 위한 기본 워크스페이스(workspace)와 정책(policy)을 자동으로 생성합니다. 따라서 갓 프로비저닝된 테넌트라도 _무언가_에 의해 관리됩니다. 할당되지 않은 계정은 애플리케이션의 기본 워크스페이스로 할당되는데, 이는 나중에 제한 사항(limits)을 격리할 때 중요하게 작용합니다.
테넌트가 어떤 워크스페이스에 속해 있는지 이미 알고 있다면, POST /v3/connect/custom 본문(body)에 provider 및 settings와 함께 최상위 workspace_id를 전달하여 생성 시 해당 워크스페이스에 계정을 배치할 수 있습니다:
curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
지금 주의해야 할 솔직한 주의사항(gotcha)이 하나 있습니다. API는 생성 시 workspace_id를 받지만, CLI의 agent account create 명령에는 --workspace 플래그가 없습니다. 터미널에서 실행하면 계정은 도메인을 기반으로 특정 워크스페이스에 배치되며(아래에서 더 자세히 설명), 해당 계정에 커스텀(custom) 정책을 적용하고 싶다면 워크스페이스에 별도로 정책을 연결하거나 나중에 계정을 이동시켜야 합니다. 우리는 정확히 그렇게 할 것입니다.
만약 테넌트(tenant)가 IMAP/SMTP를 통해 실제 메일 클라이언트를 연결해야 한다면, 생성 시 --app-password를 전달하세요 (18~40자의 ASCII 문자, 대소문자 혼용, 최소 하나 이상의 숫자 포함). 완전히 자동화된 에이전트(agent)를 구축한다면 보통 이 단계를 건너뛰고 프로토콜 액세스(protocol access)를 꺼둡니다.
새로운 테넌트를 프로비저닝(provisioning)하는 것은 회원 가입 흐름(signup flow)에서 단일 단계가 됩니다. Acme가 가입할 때, 이 작업을 한 번 호출하고 반환된 grant_id를 저장하면 Acme는 활성화된 격리된 메일함을 갖게 됩니다.
테넌트를 워크스페이스(workspaces)로 그룹화하기
권한(grants)의 평면적인 리스트는 격리가 아닙니다. 그것은 단지 메일함의 더미일 뿐입니다. 격리는 계정들을 그룹화하고 이들을 관리하는 정책(policy)과 규칙(rules)을 담고 있는 **워크스페이스(workspaces)**를 통해 이루어집니다. 두 가지 패턴이 있으며, 테넌트들이 도메인을 공유하는지 여부에 따라 선택이 달라집니다.
패턴 A — 도메인 자동 그룹화(domain auto-group). 각 테넌트가 고유한 도메인을 가지고 있다면, auto_group: true를 사용하여 테넌트 도메인당 하나의 워크스페이스를 생성하세요. 이메일 도메인이 해당 워크스페이스의 domain과 일치하는 모든 에이전트 계정(Agent Account)은 생성 시 자동으로 해당 워크스페이스에 합류합니다. 명시적인 할당이나 별도의 장부 기록이 필요 없습니다. 계정을 프로비저닝하기만 하면 올바른 워크스페이스에 배치됩니다.
POST /v3/workspaces를 사용하여 워크스페이스를 생성하세요. 요청 본문(body)에는 name과 domain이 필수이며, auto_group, policy_id, rule_ids는 선택 사항입니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/workspaces" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI는 플래그(flags)로 동일한 필드를 받습니다:
nylas workspace create \
--name "Acme tenant" \
--domain acme.youragents.com \
...
응답으로 workspace_id가 반환됩니다. 이제 생성하는 모든 acme@acme.youragents.com 스타일의 계정은 자동으로 Acme의 워크스페이스에 들어가며 Acme의 정책을 상속받습니다.
패턴 B — 명시적 할당 (explicit assignment). 만약 테넌트들이 동일한 도메인을 공유한다면 (tenant-acme@agents.yourapp.com, tenant-globex@agents.yourapp.com), 도메인 자동 그룹화 (domain auto-group) 방식으로는 이들을 구분할 수 없습니다. 따라서 각 권한 (grant)을 워크스페이스 (workspace)에 수동으로 할당해야 합니다. auto_group: false 설정으로 테넌트별 워크스페이스를 생성한 다음, 권한을 이동시키세요. 계정이 생성된 후 CLI에서 다음 명령어를 실행합니다:
nylas agent account move acme@agents.yourapp.com --workspace <ACME_WORKSPACE_ID>
이 명령어는 내부적으로 워크스페이스 수동 할당 API인 POST /v3/workspaces/{workspace_id}/manual-assign를 사용하며, 자동 그룹화가 설정되지 않은 워크스페이스에 권한을 추가하거나 제거합니다 (요청당 최대 500개의 권한 ID 가능). API를 통한 동일한 호출 방식은 다음과 같습니다:
curl --request POST \
--url "https://api.us.nylas.com/v3/workspaces/<ACME_WORKSPACE_ID>/manual-assign" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
전체 구조를 결정짓는 중요한 세부 사항 하나를 숙지해야 합니다: 워크스페이스의 domain은 생성 후 변경할 수 없습니다. 프로비저닝 (provisioning)을 시작하기 전에 도메인과 워크스페이스 간의 매핑을 결정하십시오. 공유 도메인 패턴을 사용하는 경우 어차피 명시적 할당에 의존하게 되므로 도메인은 주로 외관상의 역할만 하지만, 한 번 계획을 세우면 그대로 유지해야 합니다.
현재 권한이 어떻게 그룹화되어 있는지 확인하려면 CLI에서 워크스페이스와 계정 목록을 조회하십시오:
nylas workspace list
nylas agent account list
또는 API를 통해 GET /v3/grants에 workspace_id 쿼리 파라미터를 필터로 사용하여 특정 테넌트 워크스페이스의 권한만 나열할 수 있습니다:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants?workspace_id=<ACME_WORKSPACE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
테넌트별 제한 사항 및 정책 격리
이 지점이 테넌트별 격리 (per-tenant isolation)가 진가를 발휘하는 부분입니다. **정책 (policy)**에는 발송 할당량 (send quota), 저장 용량 제한 (storage cap), 보관 기간 (retention windows), 스팸 설정 등이 포함됩니다. 정책은 _워크스페이스 (workspace)_에 부착되며, 각 테넌트는 자신만의 워크스페이스를 가지고 있기 때문에, 권한 (grant)을 건드리지 않고도 모든 테넌트에게 서로 다른 제한 프로필을 부여할 수 있습니다.
예를 들어, 체험판(trial) 테넌트에게는 엄격한 일일 전송 제한(daily send cap)과 짧은 데이터 보관 기간(retention)을 부여하고, 유료 테넌트에게는 더 많은 여유 공간을 제공할 수 있습니다. 각 티어(tier)별로 정책(policy)을 생성하세요:
curl --request POST \
--url "https://api.us.nylas.com/v3/policies" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI에서 --name만 사용하면 제한 사항이 없는 '기본(bare)' 정책이 생성되므로, --data (또는 --data-file)와 함께 동일한 JSON 본문을 전달하여 제한 사항 및 스팸 설정을 지정해야 합니다:
nylas agent policy create --data '{
"name": "Trial tenant policy",
"limits": {
...
그런 다음 해당 정책을 테넌트의 워크스페이스(workspace)에 연결합니다. API를 사용하는 경우, 새로운 policy_id를 포함하여 PATCH /v3/workspaces/{workspace_id}를 호출하면 됩니다:
curl --request PATCH \
--url "https://api.us.nylas.com/v3/workspaces/<TENANT_WORKSPACE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
또는 이에 상응하는 한 줄짜리 CLI 명령어를 사용할 수 있습니다:
nylas workspace update <TENANT_WORKSPACE_ID> --policy-id <POLICY_ID>
workspace update 단계는 앞서 언급한 --workspace 플래그가 없는 세부 사항을 해결하는 단계입니다. 제한 사항과 정책은 개별 권한(grant)이 아니라 워크스페이스에 존재합니다. 워크스페이스에 정책을 연결하면 해당 워크스페이스의 모든 계정이 즉시 이를 적용받습니다. 정책을 해제하려면 — nylas workspace update <id> --policy-id "" — 계정들은 귀하의 결제 플랜(billing plan)이 허용하는 최대 제한 값으로 돌아갑니다.
알아두면 유용한 몇 가지 제한 사항이 있습니다 (각각은 선택 사항이며, 하나를 생략하면 해당 플랜의 최대값으로 기본 설정됩니다):
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기