Architecture(2) - 분산 이메일 서비스



1. 요구사항 정의와 규모 추정

대규모 분산 이메일 서비스 설계란 전 세계 10억 명 이상의 사용자가 어떤 상황에서도 데이터 유실 없이 실시간으로 메일을 송수신할 수 있도록 고가용성, 고확장성, 강력한 데이터 일관성을 보장하는 분산 인프라를 구축하는 아키텍처 기술이다.


1.1. 비즈니스 및 기능구조 도출

  • 규모 및 타깃 유저
    • Q: 얼마나 많은 사람들이 이용하는 제품인가?
    • A: 글로벌 10억 명의 대규모 사용자를 타깃으로 설정한다.
  • 설계 기능 범위
    • Q: 인증부터 스팸 검증까지 아래의 모든 기능을 포함해야 하는가?
      • 회원가입 및 인증
      • 이메일 발송/수신
      • 모든 이메일 조회(메일함 조회)
      • 읽음 여부에 따른 이메일 필터링
      • 제목, 발신인, 본문 내용 기반의 검색 기능
      • 스팸 및 바이러스 방지 필터링
    • A: 대규모 아키텍처 핵심 플로우에 집중하기 위해 유지보수 및 인증 시스템 설계는 제외하고 진행한다. 나머지 메일 송수신, 필터링, 고속 검색, 스팸 차단은 핵심 설계 범위에 포함한다.
  • 접근 프로토콜 및 환경
    • Q: 사용자는 메일 서버에 어떻게 연결되는가?
    • A: 전통적으로는 SMTP, POP, IMAP 등 전용 프로토콜을 사용해 접속해 왔고 여전히 널리 쓰이지만 다소 구식인 면이 있다. 여기서는 현대적인 웹 기반 인터페이스와의 연동을 고려하여 HTTP/HTTPS 프로토콜을 메인 소통 창구로 채택한다.
  • 첨부파일 지원 여부
    • Q: 이메일에 첨부 파일도 함께 전송할 수 있어야 하는가?
    • A: 그렇다. 멀티미디어 및 문서 첨부 기능을 지원해야 한다.

1.2. 비기능 요구사항

10억 명의 대용량 트래픽과 데이터 적재를 견디기 위해 아키텍처가 반드시 충족해야 할 4가지 비기능적 속성이다.

  • 안정성(Reliablilty)
    • 사용자의 이메일 데이터는 어떠한 경우에도 유실되어서는 안된다. 데이터 무결성이 최우선이다.
  • 가용성(Availability)
    • 장애 발생 시에도 서비스가 지속되도록 사용자 데이터를 여러 노드에 걸쳐 자동 복제(Replication)해야 한다.
    • 일부 컴포넌트나 데이터 센터에 장애가 나더라도 시스템은 정상적으로 계속 동작해야 한다.
  • 규모 확장성(Scalability)
    • 사용자 수와 메일 트래픽이 아무리 늘어나도 시스템의 전반적인 Latency나 성능이 저하되지 않아야 한다.
  • 유연성 및 유연한 확장성(Flexibility & Extensibility)
    • 기존의 전통적 이메일 프로토콜(POP, IMAP)은 제공 기능이 매우 제한적이다.
    • 스레딩(Threading), 실시간 레이블링 등 현대적인 기능을 유연하게 추가할 수 있도록 새 컴포넌트의 결합이 용이한 맞춤형 프로토콜 및 마이크로서비스 아키텍처 지향성을 가진다.

1.3. 개략적인 규모 추정

이메일 서비스는 텍스트와 대용량 바이너리(첨부 파일)가 결합하여 막대한 저장 용량과 트래픽을 요구하는 대표적인 하이엔드 시스템이다.
수치 분석을 통해 분산 데이터베이스 스토리지 스케일을 예측해보자.

하이엔드(High-End) 시스템

기술적으로 아키텍처 관점에서의 하이엔드 시스템은 컴퓨팅 자원의 한계치에 도전할 만큼 극단적인 스케일의 요구사항(초고처리량, 초대용량, 초고가용성)을 동시에 만족해야 하는 최고 난이도의 엔터프라이즈급 시스템을 의미함


기본 가상 조건

  • 전체 사용자 수: 10억 명(\(10^9\) 명)
  • 1인당 하루 평균 발송 메일 수: 10건
  • 1인당 하루 평균 수신 메일 수: 40건
  • 이메일 1건당 평균 메타데이터 크기: 50KB (첨부 파일 제외, 헤더 및 본문 텍스트 포함)
  • 첨부 파일 포함 메일 비율: 전체의 20%
  • 첨부 파일의 평균 크기: 500KB

시스템 트래픽 및 스토리지 계산

  • 이메일 전송 QPS
    • 하루 24시간을 시스템 계산 편의상 \(10^5초\)(정확히는 86,400초)로 수렴하여 계산한다.
    • \[\text{전송 QPS} = \frac{10^9 \text{ 명} \times 10 \text{ 건}}{10^5 \text{ 초}} = 100,000 \text{ QPS}\]
  • 1년간 메타데이터 저장 공간 요구사항
    • 사용자가 1년간 수신하는 모든 메일의 메타데이터 누적 용량이다.
    • \[10^9 \text{ 명} \times 40 \text{ 건/일} \times 365 \text{ 일} \times 50 \text{ KB} = 730 \text{ PB}\]
  • 1년간 첨부 파일 저장 공간 요구사항
    • 메일 중 20%에만 포함되는 평균 500KB의 첨부 파일을 객체 저장소에 보관할 때 필요한 총 용량이다.
    • \[10^9 \text{ 명} \times 40 \text{ 건/일} \times 365 \text{ 일} \times 20\% \times 500 \text{ KB} = 1,460 \text{ PB}\]

이처럼 1년에 처리해야 할 데이터 용량만 730PB, 첨부 파일 1,460PB로 도합 2EB(엑사바이트)에 육박한다.
초당 100,000건에 달하는 Write 트래픽과 이러한 막대한 데이터를 단일 서버나 단순 RDBMS 스토리지로 감당하는 것은 불가능하다.

따라서 이 시스템을 안정적으로 운영하기 위해서는 반드시 데이터를 쪼개어 저장하는 샤딩 기술과, 데이터 유실을 막는 분산 데이터베이스 솔루션대규모 오브젝트 스토리지 계층이 아키텍처의 필수 전제 조건이 된다.


💡메타데이터 저장 시 발신인과 수신인의 데이터를 따로 계산하지 않는 이유는?

대규모 시스템 설계에서 스토리지를 산정할 때 가장 중요한 원칙 중 하나는 데이터의 중복 계산을 피하는 것이다.
‘발송 데이터’와 ‘수신 데이터’를 각각 이중으로 더해 용량을 부풀리는 실수를 하면 안된다.
이메일은 구조적으로 ‘단일 발송(Write 한번), 멀티플 수신(Read/적재 여러 번)’의 형태를 띈다.
철수가 한 건의 메일을 작성해 영희, 길동이 2명에게 보낸다면, 시스템 계층 내에서 실제로 생성 및 분산되어 저장되어야 할 메일함 인스턴스는 수신자들의 받은 편지함 스페이스(총 2카피)이다.

따라서 여기서 상정하는 ‘하루 평균 수신 40건’이라는 수치는 발신자가 보낸 액션이 시스템 전체 인바운드로 변환되어 최종 도달한 총량을 의미한다.

발신자의 ‘보낸 편지함’ 메타데이터는 수신자 측에 저장된 원본 원시 데이터를 포인터 형태로 참조하거나, 이미 이 수신 기준 총합 데이터 구조 내에 흡수되어 계산된 것이므로 중복 계산을 방지하기 위해 ‘수신 용량’을 기준으로 통합 산정하는 것이 정확하다.


2. 이메일 프로토콜과 전통적 메일 서버

이메일 프로토콜이란 인터넷망을 통해 텍스트 및 멀티미디어 메시지를 안전하게 송수신하기 위해 정의된 표준 통신 규약이다.
메시지를 목적지 메일 서버로 보내는 SMTP와, 사용자가 자신의 클라이언트로 메일을 가져오는 POP/IMAP이 메커니즘의 핵심 축을 이룬다.

대규모 분산 메일 아키텍처를 안정적으로 설계하기 위해서는 수십 년간 글로벌 메일 생태계를 지탱해 온 기본 프로토콜의 동작 원리와, 과거 단일 장비 기반의 전통적 메일 서버가 가졌던 아키텍처적 한계를 정확하게 파악해야 한다.


2.1. 이메일 프로토콜

이메일은 송신과 수신 프로토콜이 철저하게 분리되어 동작하는 구조를 갖고 있다.

  • SMTP(Sender Mail Transfer Protocol)
    • 이메일을 한 서버에서 다른 서버로 보내는 글로벌 표준 송신 프로토콜
  • POP(Post Office Protocol)
    • 원격 메일 서버에 도착한 이메일을 클라이언트 단말로 다운로드하기 위해 사용하는 전통적인 수신 프로토콜
  • IMAP(Internet Mail Access Protocol)
    • 현대 이메일 서비스에서 가장 널리 쓰이는 표준 수신 프로토콜로, 사용자의 모바일, PC 등 다양한 단말과 원격 메일 서버 상태를 실시간으로 동기화한다.
  • HTTPS
    • 기술적으로는 메일 전송 전용 프로토콜이 아니지만, 대화형 웹메일 인터페이스나 모바일 애플리케이션(예: MS 아웃룩의 ActiveSync 프로토콜)이 메일 서버의 사서함에 접속하고 API 통신을 처리할 때 핵심 인프라로 이용된다.

💡POP vs IMAP

POP은 사용자가 메일을 클릭하지 않아도 무조건 메시지가 다운로드되고, 메일 서버에서 바로 삭제된다.
이유는 POP 프로토콜의 핵심 철학은 서버는 임시 보관소일 뿐이며, 메일 데이터의 실제 소유 및 관리는 사용자의 로컬 단말이 전담한다.는 것이기 때문이다.
POP 프로토콜을 사용하는 이메일 클라이언트는 사용자가 특정 메일을 마우스로 클릭하여 ‘열람’하는 액션을 취하지 않더라도, 서버에 접속하는 순간 사서함에 쌓여 있는 신규 메시지 전체를 통째로 로컬 디스크에 백그라운드로 내려받는다.

더 중요한 점은 다운로드가 완료되는 즉시 메일 서버에 있던 원본 데이터를 강제로 삭제한다는 것이다.
이로 인해 다음과 같은 치명적인 한계가 발생한다.

  • 단일 단말 종속성
    • 스마트폰에서 POP으로 메일을 한 번 받아버리면, 원본이 서버에서 지워지기 때문에 사무실 PC나 노트북에서는 해당 메일을 확인할 수 없다.
  • 네트워크 병목
    • 이메일을 일부만 선택해서 읽을 수 없고 사서함 전체를 내려받아야 하므로, 용량이 큰 첨부 파일이 붙은 이메일이 중간에 껴 있으면 전체 메일함으로 확인하는데 엄청난 시간이 소요된다.

반면, IMAP은 메시지를 실제로 열기 전까지는 제목, 발신인 등의 헤더 정보만 가볍게 다운로드하며, 사용자가 메일을 클릭해야 본문과 첨부 파일을 가져온다.
또한 서버의 원본 사본을 지우지 않고 유지하기 때문에 여러 단말에서 동일한 메일 상태를 공유할 수 있어 개인 계정에서 가장 널리 사용된다.

비교 항목POP3IMAP
메시지 다운로드 방식사용자의 클릭 여부와 무관하게 신규 메시지 전체를 로컬로 강제 다운로드클릭하기 전에는 헤더만 다운로드, 클릭 시 본문 및 첨부파일 다운로드
서버 원본 데이터다운로드 직후 서버에서 즉시 삭제 (표준 스펙)클라이언트와 상태를 동기화하며 서버 사본을 안전하게 유지
멀티 디바이스 동기화불가능 (최초로 연결된 단말이 데이터를 독점)완벽 지원 (스마트폰, 태블릿, PC에서 동일한 사서함 공유)
대용량 첨부 파일 처리사서함 전체를 받아야 하므로 초기 로딩 속도가 매우 느림헤더만 먼저 가져오므로 인터넷 속도가 느려도 부드럽게 동작

2.2. DNS MX 레코드

사용자가 이메일을 발송하면, 송신 측 메일 서버는 수신자 도메인(예: gmail.com)의 우편함을 관리하는 실제 SMTP 서버의 IP 주소를 알아내야 한다.
이때 DNS 서버에 MX(Mail Exchange) 레코드에 질의하여 수신처를 파악한다.

다음은 실제 gmail.com의 DNS MX 레코드를 네임서버 조회 도구(nslookup)로 검색한 예시이다.

$ nslookup
> set q=mx
> gmail.com
Server:		210.220.163.82
Address:	210.220.163.82#53

Non-authoritative answer:
gmail.com	mail exchanger = 30 alt3.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger = 10 alt1.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger = 20 alt2.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger = 5 gmail-smtp-in.l.google.com.
gmail.com	mail exchanger = 40 alt4.gmail-smtp-in.l.google.com.

<MX 레코드 우선순위 제어 원리>
조회 결과에서 메일 교환기 주소 앞에 붙은 숫자(5,10,20,30,40)는 우선순위 값을 나타낸다.
이 값이 낮을수록 우선순위가 높아 시스템에서 가장 선호되는 서버임을 의미한다.

  • 송신자 측 메일 서버는 우선순위 값이 가장 낮은(5) gmail-smtp-in.l.google.com. 서버에 최우선적으로 접속하여 메시지 전송을 시도
  • 만일 해당 최우선순위 서버가 트래픽 폭주나 네트워크 장애로 인해 대답하지 않는다면, 시스템은 차선책으로 그다음 우선순위가 높은(10) alt1.gmail-smtp-in.l.google.com. 서버로 연결을 전환(failover)

이러한 메커니즘을 통해 대규모 도메인들은 여러 대의 인바운드 메일 서버를 두고 안정적인 트래픽 분산과 결함 내성을 확보한다.


2.3. MIME

초기의 이메일 시스템(SMTP)은 오직 7비트 ASCII 텍스트만을 전송할 수 있도록 설계되었다.
이 한계를 극복하고 멀티미디어, 바이너리 문서, 이미지 등의 파일 인스턴스를 메시지와 함께 전송하기 위해 탄생한 표준 규격이 바로 MIME(Multipurpose Internet Mail Extensions)이다.

  • Base64 인코딩 체계
    • 이메일 첨부 파일은 네트워크 전송 전 MIME 규격에 따라 바이너리 데이터를 ASCII 텍스트 문자로 매핑하는 Base64 인코딩을 거치게 된다.
    • 이 인코딩 과정을 거치면 원본 파일의 크기가 약 33% 증가하는 특성이 있다.
  • 크기 제한 정책
    • 인코딩 오버헤드와 메일 서버의 메모리/디스크 부담을 줄이기 위해 대부분의 대형 이메일 서비스 사업자는 엄격한 크기 제한을 둔다.
    • 예를 들어 MS Outlook은 20MB, Gmail은 25MB로 첨부 파일 최대 용량을 제한하고 있다.

2.4. 전통적 메일 서버 아키텍처

분산 시스템으로의 전환 필요성을 이해하기 위해, 과거 단일 서버 장비 기반으로 구동되던 전통적인 메일 서버 구조와 이메일 전달 라이프사이클을 살펴보자.

전통적 이메일 서비스

<전통적 메일 시스템의 메시지 라우팅 흐름>

  • 송신 클라이언트 액션
    • 앨리스가 아웃룩 클라이언트를 통해 이메일을 작성하고 ‘보내기’ 버튼을 누르면, 메시지는 SMTP 프로토콜을 타고 아웃룩 메일 서버로 푸시된다.
  • DNS 라우팅 주소 조회
    • 아웃룩 메일 서버는 DNS 질의를 수행하여 수신자 도메인인 지메일의 SMTP 서버 주소를 확인한다.
  • 서버 간 전송
    • 주소를 확인한 아웃룩 메일 서버는 지메일의 인바운드 SMTP 서버에 접속하여 SMTP 프로토콜을 통해 메시지를 전송한다.
  • 수신 서버 적재
    • 지메일 서버는 수신된 이메일을 내부 스토리지에 저장하고 밥이 읽어갈 수 있도록 대기시킨다.
  • 사서함 Pulling
    • 수신자인 밥이 지메일에 로그인하면 클라이언트는 IMAP 또는 POP 서버에 접속하여 새 이메일 데이터를 가져온다.

2.4.1. 전통적 파일 시스템 저장 구조: Maildir

전통적인 메일 서버는 데이터베이스 대신 파일 시스템의 디렉터리 구조를 활용해 사용자 사서함을 관리했다.
대표적으로 사용되는 구조가 바로 Maildir이다.

home
└── home
    ├── user1
    │   └── Maildir
    │       ├── cur  # 사용자가 이미 읽은 메시지 보관 디렉터리
    │       ├── new  # 아직 읽지 않은 신규 메시지 보관 디렉터리
    │       └── tmp  # 전송 중인 메시지가 임시로 머무는 디렉터리
    └── user2
        └── Maildir
            ├── cur
            ├── new
            └── tmp

Maildir 구조에서는 모든 이메일 메시지가 고유한 이름을 가진 하나의 개별 파일로 생성되며, 사용자의 설정과 사서함 상태가 디렉터리 분기를 통해 관리된다.


⚠️단일 장비 Maildir 구조의 치명적인 한계

  • 디스크 I/O 병목 현상
    • 사용자 수가 수백만, 수십억 명으로 늘어나고 한 디렉터리 내에 수천만 개의 이메일 파일이 적재되면 파일 생성, 조회, 삭제 시 극심한 파일 시스템 Lock과 디스크 I/O 병목이 발생한다.
    • 수십억 개의 메시지를 고속 검색하거나 백업하는 것은 사실상 불가능하다.
  • 가용성 및 안정성 결여
    • 데이터가 특정 메일 서버 단일 장비의 로컬 파일 시스템에 종속되어 보관되므로, 디스크에 물리적 손상이 발생하거나 서버 가동이 중단되면 즉시 데이터 유실 및 서비스 마비로 이어진다.
  • 프로토콜의 기능적 한계
    • POP, IMAP, SMTP 같은 전통적 프로토콜은 1960~80년대에 설계되어 현대의 대화형 스레딩(Threading), 스마트 레이블, 복잡한 메타데이터 필터링 등의 고급 기능을 대규모 사용자 환경에서 유연하고 확장성 있게 지원하지 못한다.

전통적인 이메일 시스템은 SMTP, POP, IMAP이라는 견고한 표준 프로토콜과 Maildir 기반 파일 저장소를 바탕으로 성장해 왔다.
그러나 10억 명의 대규모 사용자를 수용하고 페타바이트 단위의 디스크 I/O 병목을 해결하기에는 구조적 한계가 명확하다.

이러한 문제를 해결하기 위해 현대적인 이메일 서비스는 무거운 파일 시스템을 걷어내고, 확장 가능한 분산 캐시와 NoSQL 데이터베이스 계층이 융합된 맞춤형 분산 메일 아키텍처로 진화하게 된다.


3. 분산 메일 서버 아키텍처 개략 설계

분산 메일 서버 아키텍처란 수억 명의 유저가 발생시키는 대용량 메일 트래픽을 단일 장비의 한계 없이 처리하기 위해, 기능별로 컴포넌트를 마이크로서비스화하고 메시지 큐와 분산 저장소를 결합한 고가용성 인프라 시스템이다.
웹 표준 HTTP/HTTPS API와 실시간 푸시 기술(WebSocket)을 융합하여 유연한 규모 확장을 달성하는 것을 골자로 한다.


3.1. RESTful API

현대적인 분산 메일 서비스는 클라이언트와의 통신 유연성을 극대화하기 위해 웹 기반의 RESTful API를 핵심 인터페이스로 사용한다.
여기서는 가장 기본이 되는 4가지 핵심 엔드포인트만 다룬다.


POST /v1/messages

  • 기능: 지정된 수신자들에게 이메일 메시지를 생성하고 발송
  • 주요 파라미터: to[], cc[], bcc[], subject, body, attachments[]

To, Cc, Bcc 헤더의 정의

  • To(수신자)
    • 메일의 주 수신 대상자
  • Cc(참조, Carbon Copy)
    • 메일 내용을 함께 공유받아야 하는 참조자
    • 수신자 전체에게 Cc 명단이 공개됨
  • Bcc(숨은 참조, Blind Carbon Copy)
    • 보안이나 프라이버시를 위해 메일을 받는 다른 사람들에게 노출하지 않고 비밀리에 복사본을 받는 수신자
    • 전송 과정에서 SMTP 프로토콜 전달 규칙에 의해 수신자 목록 헤더에서 Bcc 대상자의 정보는 완벽히 제거되어 발송됨

GET /v1/folders

  • 기능: 사용자의 메일 계정에 존재하는 모든 폴더 목록 반환
  • 반환 데이터 예시:
[
  {
    "id": "fld_inbox_9921",
    "name": "Inbox",
    "user_id": "usr_alice_01"
  },
  {
    "id": "fld_sent_1102",
    "name": "Sent",
    "user_id": "usr_alice_01"
  }
]

GET /v1/folders/{:folder_id}/messages

  • 기능: 주어진 특정 폴더(예: 받은 편지함, 보낸 편지함) 하위에 속한 모든 메시지의 요약 목록을 페이지네이션 형태로 반환

GET /v1/messages/{:message_id}

  • 기능: 사용자가 특정 이메일을 클릭했을 때, 해당 메시지의 본문과 세부 정보 조회
  • 반환 데이터 예시:
    {
      "message_id": "msg_739210",
      "user_id": "usr_bob_02",
      "from": {
        "name": "Alice",
        "email": "alice@outlook.com"
      },
      "to": [
        {
          "name": "Bob",
          "email": "bob@gmail.com"
        }
      ],
      "subject": "분산 아키텍처 회의 일정 안내",
      "body": "안녕하세요, Bob. 내일 오전 10시 분산 시스템 설계 세션이 진행됩니다.",
      "is_read": true
    }

3.2. 분산 메일 서버 아키텍처

전통적인 메일 서버는 단일 장비 위에 모든 프로세스가 올라가 확장하기 어려웠지만, 분산 아키텍처에서는 역할에 따라 완전히 계층을 분리한다.

개략적 설계안

<컴포넌트별 기술적 역할>

  • 웹메일(Webmail)
    • 사용자가 브라우저 및 앱을 통해 사서함과 상호작용하는 웹 프론트엔드 인터페이스
  • 웹서버(Web Server)
    • 클라이언트가 요청하는 모든 HTTP 기반 RESTful API(로그인, 가입, 메일함 조회, 발송 등)를 처리하는 Stateless 게이트웨이 계층
  • 실시간 서버(Real-time Server)
    • 클라이언트가 브라우저 창을 새로고침하지 않아도 새 이메일이 도착하면 화면에 즉시 띄워주는 실시간 이벤트 전송 서버
      • 여기서 말하는 실시간 서버의 클라이언트는 현재 메일 서비스 웹 브라우저나 모바일 앱을 켜놓고 로그인해 있는 상태의 활성 유저(Active User)를 뜻함
      • 새로운 메일이 수신처 계정이 도착하면, 백엔드 워커가 실시간 서버를 통해 해당 웹소켓 세션을 찾아 ‘새 메일이 왔으니 화면을 갱신하라’는 신호를 쏨
      • 만일 유저가 앱을 완전히 종료한 오프라인 상태라면 실시간 서버는 세션이 없으므로 이 단계를 생략하고 저장소에만 보관하며, 추후 유저가 재접속할 때 HTTP API를 통해 리스트를 Pulling 해가게 됨
    • 연결 메커니즘:
      • 지속적인 커넥션을 유지해야 하므로 Stateful 서버 구조를 띔
      • 현대 시스템에서는 웹소켓을 기본 프로토콜로 하되, 구형 브라우저 호환성 및 네트워크 환경을 고려하여 백업 수단으로 롱 폴링(Long Polling) 방식을 결합하는 하이브리드 아키텍처가 결함 내성에 유리함
  • 메타데이터 DB
    • 이메일의 제목, 본문 텍스트, 발신인/수신인 매핑 관계, 읽음 상태 등 정형화되거나 준정형화된 이메일 메타데이터를 저장하는 핵심 데이터 레이어
  • 첨부 파일 저장소
    • 대용량 바이너리 데이터를 안전하고 저비용으로 무한히 확장 보관하기 위해 AWS S3와 같은 분산 객체 저장소(Object Storage)를 채택
  • 분산 캐시
    • 사용자는 일반적으로 생성된 지 얼마 되지 않은 최근 메일(2주 이내 데이터가 전체 읽기 질의의 82% 차지)을 주로 읽음
    • 따라서 최신 메일 사서함 인스턴스를 메모리 기반의 Redis 클러스터에 캐싱하여 디스크 DB의 가중 부담을 경감하고 응답 속도를 극대화 함
  • 검색 저장소(Search Storage)
    • 본문 텍스트, 제목 키워드 등의 초고속 질의를 처리하는 분산 문서 저장소(Distributed Document Store)
    • 텍스트를 단어 단위로 쪼개어 위치를 기록해 두는 역인덱스(Inverted Index) 자료구조를 사용하여 탐색 시간 복잡도를 혁신적으로 줄임

💡첨부 파일 저장소로 카산드라(Cassandra) NoSQL이 부적합한 이유

Apache Cassandra는 대용량 Row/Column 구조의 정형 데이터를 초고속으로 Write/Read 하는데 최적화된 LSM 트리 기반 저장소이다.
최대 2GB 크기의 BLOB 타입을 지원하긴 하지만, 실무 환경에서 1MB 이상의 큰 바이너리 파일을 직접 밀어넣으면 아래와 같은 심각한 문제가 발생한다.

  • 힙 메모리 및 GC 압박
    • 카산드라는 데이터를 디스크에 쓰기 전 Memtable이라는 메모리 버퍼에 먼저 적재한다.
    • 대용량 파일이 들어오면 JVM의 힙 메모리가 순식간에 고갈되어 극심한 GC Stop-the-world 현상이 발생한다.
  • Compaction 지옥
    • 카산드라는 순차 쓰기 후 배경에서 파일들을 병합하고 재정렬하는 컴팩션 작업을 끊임없이 수행한다.
    • 수십 MB짜리 레코드가 섞여 있으면 컴팩션 과정에서 엄청난 디스크 읽기/쓰기 증폭이 일어나 전체 클러스터의 I/O 성능이 마비된다.
  • 레코드 캐시 오염
    • 큰 파일이 메모리 캐시 레이어를 독접하여 가볍고 빈번하게 조회되어야 할 핵심 메타데이터 텍스트 레코드들이 캐시에서 밀려나는 성능 저하를 야기한다.
    • 따라서 대용량 파일은 무조건 S3 같은 전용 객체 저장소에 넣고, DB에는 그 다운로드 URL 포인터(참조 정보)만 저장하는 것이 정석이다.

4. 이메일 전송 워크플로우와 메시지 큐의 역할

사용자가 이메일 화면에서 ‘보내기’ 버튼을 누른 후, 최종 목적지 서버까지 도달하는 백엔드 파이프라인의 내부 전송 흐름이다.

이메일 전송 절차

  • (①,②)요청 접수 및 처리율 제한
    • 사용자가 웹메일 인터페이스에서 메일을 전송하면 HTTPS 요청이 로드밸런서로 인입된다.
    • 로드밸런서는 특정 악성 유저의 DDoS 공격이나 무차별 스팸 발송을 차단하기 위해 처리율 제한(Rate Limit) 한도를 검증한 후 웹서버로 분산 전달한다.
  • (③)웹서버의 1차 라우팅 및 유효성 검증
    • 웹서버는 사전에 정의된 유효성 규칙(메일 포맷, 첨부파일 크기 한도 등)을 검사한다.
      • 라우팅 조건 분기
        • 이 때 수신자 이메일 주소의 도메인이 송신자 도메인과 같은지(내부 발송) 다른지(외부 발송) 체크하여 도메인이 같다면 메시지 큐 적재를 포함한 이후 단계를 생략한다.
        • 예를 들어 gmail.com 유저가 동일한 gmail.com 유저에게 이메일을 보내는 시나리오라면, 전 세계 인터넷망을 타고 외부 SMTP 서버를 찾아 돌아다닐 필요가 전혀 없다.
        • 웹서버 단계에서 도메인이 일치함을 감지하면, 즉각 내부 메인 데이터베이스와 메모리 캐시 계층을 다이렉트로 업데이트하여 송신자의 ‘보낸 편지함’과 수신자의 ‘받은 편지함’에 메시지를 동시에 다이렉트로 적재한다.
        • 외부 전송용 SMTP 큐(4단계 이후)를 거치지 않기 때문에 네트워크 레이턴시가 거의 제로에 수렴하며 인프라 자원을 크게 아낄 수 있다.
  • (④)메시지 큐 적재
    • 웹서버는 무거운 검증 및 발송 작업을 비동기로 처리하기 위해 메시지 큐를 버퍼로 활용한다.
      • 기본 유효성 검증을 정상 통과한 메시지는 외부 전송 큐에 넣는다. 만일 첨부 파일 크기가 너무 큰 메일이라면 바이너리는 S3 객체 저장소에 따로 저장하고, 큐에 들어가는 메시지 바디에는 해당 S3 파일 주소 참조값만 포함하여 경량화한다.
      • 형식이 잘못 되었거나 유효성 검증에 실패한 메일은 아예 처리 대상에서 제외하고 에러 큐로 분류하여 예외 처리한다.
  • (⑤,⑥)SMTP 작업 프로세스의 검역 및 영속화
    • 외부 전송 담당 SMTP 워커 프로세스들이 큐에서 메시지를 대량으로 Consuming하여 보안 필터를 구동한다.
      • (⑤)이메일 내부 본문 및 첨부 파일을 검사하여 스팸 여부와 바이러스 감염 여부를 2차 정밀 검증한다.
      • (⑥)검역을 통과한 이메일은 비로소 저장소 계층 내 송신자의 ‘보낸 편지함’ 테이블에 안전하게 영속화된다.
  • (⑦)인터넷망을 통한 최종 SMTP 발송
    • 전송 프로세스는 수신자 도메인의 DNS MX 레코드 주소를 조회한 후, 해당 원격지 수신 메일 서버의 인바운드 SMTP 포트로 메시지를 최종 푸시한다.

4.1. 타 도메인 발송 시의 메시지 큐 규모 제어 및 백오프 전략

도메인이 서로 다르면 외부 전송 SMTP 프로세스와 메시지 큐의 역할이 매우 중요해진다.
수신처 메일 서버(예: 기업 자체 메일 서버 등)은 일시적인 전원 다운이나 네트워크 장애로 인한 먹통이 되는 경우가 빈번하기 때문이다.

  • 지수적 백오프(Exponential Backoff)
    • 외부 전송 SMTP 프로세스는 수신 서버가 응답하지 않으면 즉시 발송 실패로 처리하여 유실시키지 않고, 지수적 백오프(예: 1분 뒤 재시도 → 2분 뒤 → 4분 뒤..) 전략을 사용하여 외부 전송 큐에 메시지를 retry 한다.
  • Consumer Scale-out
    • 특정 대형 도메인으로 메일 발송이 폭증하여 외부 전송 큐의 Queue Depth가 임계치를 넘고 메일 전송 지연이 모니터링되면, 외부 전송 담당 SMTP 워커 인스턴스의 개수를 Scale-out 하여 처리 시간을 단축시킨다.
    • 웹서버와 발송 아키텍처가 큐를 통해 느슨하게 결합되어 있기에 가능한 구조적 장점이다.

5. 이메일 수신 워크플로우와 실시간 알림(WebSocket) 처리

외부의 다른 메일 서버들이 우리 시스템의 유저에게 메일을 보낼 때, 인바운드 트래픽을 안전하게 받아 처리하는 역방향 파이프라인 흐름이다.

이메일 수신 절차

  • (①)SMTP 인바운드 요청 접수
    • 외부 메일 서버가 우리 시스템 도메인의 IP를 향해 보낸 이메일이 SMTP 로드밸런서에 도착
  • (②)인바운드 수락 정책(Policy) 필터링
    • 로드밸런서는 게이트웨이 단계에서 엄격한 이메일 수락 정책을 실행함
    • 존재하지 않는 유저 아이디(@앞의 주소 무효)로 발송되었거나 블랙리스트 IP에서 인입된 메일은 TCP 연결 단계에서 즉각 거부(반송 처리)하여 내부 시스템 자원 보호
  • (③,④)첨부 파일 분리 및 수신 큐(Inbound Email Queue) 적재
    • 수락 레이어를 통과한 이메일 중 대용량 첨부 파일은 S3 객체 저장소로 즉시 우회 적재하며, 이메일 바디와 메타데이터는 수신 이메일 큐에 전달
      • 버퍼의 역할: 이 인바운드 큐는 외부에서 스팸 공격이나 마케팅 대량 메일 폭탄이 순간적으로 급증하더라도, 뒤의 메일 처리 서버들이 부하를 받지 않도록 트래픽을 흡수하는 버퍼 역할을 수행함
  • (⑤)메일 처리 워커의 정밀 검역 및 저장
    • 메일 처리 작업 프로세스(Worker)들이 수신 큐에서 데이터를 가져와 스팸 메일 필터링, 악성 URL 탐지, 바이러스 백신 검사 등의 무거운 보안 검증 작업을 백그라운드에서 실행
  • (⑥)스토리지 영속화 및 캐싱
    • 정밀 검증을 마친 이메일 인스턴스는 하부 저장소 계층의 메타데이터 DB(받은 편지함 테이블)에 저장되고, Redis 캐시에 최신 데이터로 업데이트됨
  • (⑦,⑧,⑨)실시간 푸시 알림 트리거
    • 메일 처리 워커는 수신자 유저가 현재 브라우저에 연결되어 있는 온라인 상태인지 세션 매니저를 통해 조회함
      • (⑦,⑧)온라인 상태인 경우:
        • 이메일 이벤트를 실시간 서버(WebSocket)로 즉각 보내고, 실시간 서버는 유지하고 있던 웹소켓 커넥션을 통해 유저의 화면에 ‘새 메일 도착’ 알림과 함께 메일 리스트를 동기화함
      • (⑨)오프라인 상태인 경우:
        • 실시간 푸시를 생략하고 저장소 계층에만 보관 상태를 유지함
        • 이후 사용자가 PC나 스마트폰을 켜고 웹메일에 다시 접속(HTTP RESTful API 호출)하는 시점에 웹서버가 저장소 계층에서 신규 메일을 조회해서 화면에 반환함

💡메일 처리 워커의 검증 작업(스팸, 바이러스 등)을 통과하지 못한 이메일들은 어떻게 처리될까?

위의 ⑤단계를 통과하지 못한 이메일들은 그 위험도와 정책 점수(Score)에 따라 철저하게 격리 분기 처리가 이루어진다.

  • 1단계: 완전 매립 및 Drop(명백한 악성 바이러스/랜섬웨어)
    • 첨부 파일이나 본문 코드에서 확실한 악성코드 식별자가 검출된 경우, 사용자의 안전을 위해 메일을 아예 노출시키지 않고 영구 삭제(Drop)하거나, 최고 관리자 격리 저장소로 격리 이송함
    • 송신자에게 오류 메일조차 보내지 않는 경우가 많음(악성 해커에게 ‘이 주소가 살아있다’라는 힌트를 주지 않기 위함)
  • 2단계: 스팸함 자동 비정규화 적재(의심스러운 광고성 메일)
    • 바이러스는 없으나 스팸 점수(텍스트 분석, 발신 IP 평판 점수 등)가 임계치를 초과한 경우 시스템은 이 메일을 수신자의 일반 ‘받은 편지함’ 테이블이 아닌 ‘스팸 메일함’ 폴더 식별자(folder_id)로 태깅하여 메타데이터 DB에 격리 저장
    • 유저는 스팸 폴더를 직접 클릭하기 전까지는 메인 화면에서 이 메일을 보지 않게됨
  • 3단계: 발송처 거부 응답 반송(SMTP 5xx 에러 리턴)
    • 검증 워커 구동 이전 혹은 정책 필터 초기 단계에서 완전히 악성 도메인으로 확인된 경우, 수신 큐에 넣기 전 SMTP 규격에 따라 550 악성 메시지 차단(Message rejected as spam)과 같은 프로토콜 오류 코드를 송신 측 서버에 Echo하여 발송 자체를 거부 처리함

10억 명의 사용자를 수용하는 분산 이메일 서비스 아키텍처의 핵심은 ‘느슨한 결합’과 ‘역할의 격리’이다.
클라이언트와 소통은 경량화된 HTTP API와 실시간 웹소켓으로 처리하고, 무거운 전송 및 수신 검역 연산은 분산 메시지 큐 배후의 워커 프로세스들에게 비동기로 위임한다.
데이터 레이어 역시 텍스트 메타데이터, 대용량 첨부파일, 고속 검색 인덱스 저장소를 철저히 분리 운영함으로써 무한한 Scale-out의 기반을 완성한다.


6. 메타데이터 DB 선정 및 NoSQL 데이터 모델링

이메일 메타데이터 데이터 모델링이란 이메일의 헤더, 본문, 상태 값 등 대규모 준정형 데이터를 디스크 I/O 병목 없이 초고속으로 조회/갱신하기 위한 분산 NoSQL 데이터베이스의 구조(파티션 키 및 클러스터 키)를 최적화하고 테이블을 비정규화하는 아키텍처 설계 기법이다.

10억 명의 사용자가 유발하를 하루 수백 테라바이트의 데이터를 안정적으로 다루기 위해서는 분산 메일 서버의 핵심인 저장소 계층의 상세 설계가 완벽해야 한다.
대형 이메일 서비스 사업자들이 어떤 특성을 기반하여 데이터베이스를 선정하고 모델링하는지에 대해 알아보자.


6.1. 이메일 메타데이터의 특성

이메일 데이터는 일반적인 가상 커머스나 SNS 데이터와 비교했을 때 독특한 액세스 패턴과 제약 조건을 가진다.

  • 비대칭적 크기와 빈도
    • 이메일 헤더(발신인, 수신인, 시간 등)는 크기가 작지만 목록 조회 등을 위해 매우 빈번하게 액세스된다.
    • 반면 이메일 본문은 크기가 수KB에서 수백KB까지 다양하지만, 사용자가 메일을 한 번 읽은 후에는 거의 다시 조회하지 않는 낮은 빈도의 패턴을 보인다.
  • 완벽한 유저 격리성
    • SNS 게시글은 사방으로 공유되지만, 이메일은 특정 사용자별로 데이터가 완전히 격리되어 수행된다.
    • 즉, 내가 받은 메일은 오직 나만 읽고, 쓰고, 삭제할 수 있어야 한다.
  • 극단적인 데이터 신선도 편향
    • 사용자는 주로 최근 메일만 읽는다.
    • 통계에 따르면 생성된 지 16일 이하인 최신 데이터에 발생하는 읽기 질의 비율이 전체 질의의 82%에 달한다.
    • 데이터 동적 캐싱이 매우 효과적인 이유이다.
  • 무조건적인 데이터 안정성
    • 금융 시스템과 마찬가지로 이메일은 단 한 건의 데이터 유실도 용납되지 않는 높은 무결성을 요구한다.

6.2. 올바른 데이터베이스 선정

하루에 수억 건씩 쏟아지는 메일 적재 트래픽을 감당하기 위해 기성 데이터베이스들의 장단점을 비교 분석해보자.


6.2.1. RDBMS

인덱스를 활용한 구조화된 질의와 검색에 유리하다.
그러나 대규모 이메일 시스템에서는 메일 본문에 HTML, 이미지 등이 섞여 쉽게 100KB를 넘어간다.
이를 RDBMS의 BLOB(Binary Large Object) 자료형으로 처리하려고 하면 비정형 BLOB 자료형 데이터에 대한 검색 질의 성능이 좋지 않기 때문에 치명적인 성능 저하가 발생한다.


💡RDBMS BLOB 저장의 구조적 병목 원인

MySQL(InnoDB)이나 PostgreSQL 같은 관계형 데이터베이스는 고정된 크기의 데이터 페이지(Page, 보통 4KB~16KB) 단위로 디스크 I/O를 수행한다.
100KB가 넘는 이메일 본문이나 대용량 BLOB 데이터가 들어오면, 이 데이터는 단일 페이지에 담기지 못하고 여러 개의 오버플로우 페이지를 포인터로 연결하여 디스크 외부에 따로 저장한다.

결과적으로 해당 컬럼에 접근할 때마다 고속 메모리 버퍼 풀을 거치지 못하고 수많은 디스크 오버플로우 페이지를 뒤지는 무거운 무작위 디스크 I/O가 발생한다.
이로 인해 트래픽 폭주 시 RDBMS 전체의 IOPS(Input/Output Operation Per Second, 초당 입/출력 연산 빈도) 한계치를 초과하여 DB 서버가 마비되는 현상이 벌어진다.


💡정형(Structured) BLOB vs 비정형(Unstructured) BLOB

  • 정형/준정형 BLOB
    • 겉보기에는 바이너리 형태 데이터이지만, 내부에 특정 규칙이나 스키마가 있는 상태를 의미한다.
    • 예를 들어 애플리케이션 단에서 객체를 Protocol Buffers, Thrift, 혹은 직렬화된 JSON 형태로 가공하여 RDBMS BLOB 컬럼에 저장한 경우이다.
    • 파싱 프로세스를 거치면 내부 필드 구조를 명확히 복원할 수 있다.
  • 비정형 BLOB
    • 내부 규칙이 전혀 없는 순수 바이너리 스트림이다.
    • 사용자가 업로드한 압축 파일(.zip), PDF 문서, 이미지 파일(.png), 혹은 원시 오디오 스레드 등이 이에 해당한다.
    • 데이터베이스 입장에서는 안을 열어봐도 구조를 파악할 수 없는 거대한 바이트 덩어리일 뿐이다.

6.2.2. 분산 객체 저장소(AWS S3 등)

수 페타바이트의 파일 데이터를 매우 저렴하고 안정적으로 보관할 수 있다.
하지만 이메일의 읽음 상태 변경, 특정 키워드 필터링 등의 빈번한 업데이트와 세밀한 메타데이터 질의를 구현하기에는 API 제약과 Latency 장벽이 너무 높다.


6.2.3. 분산 NoSQL 데이터베이스(구글 빅테이블, 카산드라 등)

지메일의 경우 구글 내부의 거대한 분산 NoSQL 데이터베이스인 빅테이블(Bigtable)을 저장소로 사용한다.
대규모 무중단 Scale-out과 고속 쓰기를 완벽히 지원하기 때문이다.
오픈소스 진영에서는 아파치 카산드라(Cassandra)가 훌륭한 대안이 될 수 있다.


6.2.4. 강한 일관성

분산 데이터베이스는 네트워크 장애 상황에서 일관성가용성 중 하나를 타협해야 한다.(CAP 이론)

CAP 이론에 대해서는 추후 다룰 예정입니다.

인스타그램의 ‘좋아요 수’는 조금 뒤늦게 동기화되어도(Eventual Consistency) 괜찮지만, 이메일은 다르다.

유저가 중요한 비즈니스 메일을 읽고 ‘읽음’ 처리를 했는데 새로고침했을 때 다시 ‘안읽음’으로 바뀐다거나, 방금 받은 메일이 다른 복제본 노드로 접속했을 때 보이지 않는다면 서비스의 신뢰도는 완전히 낮아진다.
따라서 이메일 아키텍처는 데이터 정확성을 최우선하는 CP(Consistency & Partition Tolerance) 시스템을 지향한다.

  • 단일 주 사본(Single Primary Replica) 원칙
    • 이를 달성하기 위해 분산 메일 시스템은 특정 사용자의 사서함에 발생하는 모든 읽기/쓰기 질의를 반드시 단 하나의 ‘주 사본(Primary/Master) 노드’를 통해서만 처리하도록 강제한다.
    • 보조 사본(Secondary)들은 오직 백업 및 주 사본 장애 시의 대기 조로만 활용된다.
  • 장애 발생 시의 가용성 희생 메커니즘
    • 만일 특정 유저의 사서함을 담당하는 주 사본 노드에 장애가 발생하면 어떻게 될까?
    • 클라이언트는 다른 보조 사본 노드에 대고 즉시 메일을 쓰거나 읽는 작업을 수행할 수 없다.
    • 보조 사본이 새로운 주 사본으로 승격(Failover)되거나 원래의 주 사본이 완벽히 복원되어 데이터 동기화 정합성이 검증될 때까지, 해당 유저의 사서함 동기화 및 메일 갱신 작업을 일시적으로 차단된다.

정합성이 깨진 데이터를 보여주느니 시스템이 계속 가동되는 것보다, 잠시 서비스를 멈추더라도 데이터의 완벽한 정확성을 보장하는 것이 이메일 서비스의 핵심이다.


6.2.5. 결론

시중에 나와있는 기성 솔루션을 그대로 써서는 이 규모를 만족할 수 없다.
따라서 대형 메일 사업자들은 자체 커스텀 DB 인프라를 구축하거나 분산 NoSQL을 극도로 튜닝하여 아래의 5가지 조건을 충족하는 스토리지 계층을 완성한다.

  • 단일 컬럼/로우의 크기가 수 MB 수준이어도 성능 저하가 없어야 함
  • 사용자 관점의 강력한 데이터 일관성 보장
  • 디스크 무작위 I/O를 최소화하도록 설계(순차 쓰기 지향)
  • 장애 노드가 발생해도 서비스가 중단되지 않는 초고가용성
  • 데이터 복구를 위한 증분 백업(Incremental Backup)의 용이성

💡증분 백업(Incremental Backup)이란?

증분 백업이란 매번 시스템의 전체 데이터를 백업(Full Backup)하는 대신, 가장 최근에 수행된 백업 시점 이후로 ‘새롭게 변경되거나 추가된 데이터’만 선택해서 백업하는 방식이다.

앞서 1.3. 개략적인 규모 추정에서 산정했듯이, 10억 명 규모의 시스템은 연간 2.2EB(엑사바이트)의 데이터를 저장한다.
하루에 추가되는 데이터만 해도 수 테라바이트에서 페타바이트 스케일이다.
매일 전체 데이터를 백업하는 것은 네트워크 대역폭과 스토리지 비용 측면에서 불가능하다.

분산 NoSQL(예: 카산드라)은 데이터를 디스크에 쓸 때 기존 파일을 수정하지 않고 SSTable이라는 불변 파일로 순차 적재한다.
증분 백업을 실행하면, 마지막 백업 이후 새롭게 디스크에 생성된 신규 SSTable 파일들만 그대로 복사해 오면 되기 때문에 시스템 부하를 최소화하면서 페타바이트 급 백업을 실시간으로 완료할 수 있다.


6.3. NoSQL 데이터 모델링: 파티션 키와 클러스터 키 설계

분산 NoSQL 환경에서는 데이터를 어떤 노드에 배치하고 어떻게 정렬할 것인지 결정하는 기본키(Primary Key) 설계가 시스템 성능의 성패를 가른다.
NoSQL의 기본키는 크게 파티션 키(Partition Key)와 클러스터 키(Clustering Key)의 복합 구조로 구성된다.

  • 파티션 키(Partition Key)
    • 데이터를 여러 물리 노드(서버)에 균등하게 분산(샤딩)시키는 해시 키 역할을 한다.
    • 동일한 파티션 키를 가진 레코드들은 물리적으로 완전히 같은 노드에 모여 저장된다.
  • 클러스터 키(Clustering Key)
    • 같은 파티션 노드 내부에 저장된 데이터들을 디스크 상에서 정렬하는 기준이 된다.

💡클러스터 키의 상세 메커니즘

NoSQL 저장소 내부에서 클러스터 키는 디스크 정렬의 물리적 기준선이 된다.
예를 들어 클러스터 키를 DESC로 설정하면, 새로운 데이터가 들어올 때 디스크의 연속된 물리적 공간에 정렬된 상태로 쌓인다.

이 구조 덕분에 특정 사용자의 메일 리스트를 가져올 때, 디스크 이곳저곳을 찾는 게 아니라 정렬되어 모여 있는 특정 물리 구역만 통째로 긁어오는 순차 읽기(Sequential Read)가 가능해져 쿼리 속도가 매우 빨라진다.


💡user_id 단일 파티션 키가 유발하는 ‘핫스팟 및 Wide Partition’ 문제

‘사용자별 격리 시스템이니까 user_id 만 파티션 키로 쓰면 특정 유저 메일이 한 곳에 모여서 좋은 것 아닌가?’ 라고 생각할 수 있다.
메일 양이 적은 일반 유저라면 괜찮다.
하지만 메일을 하루에 수천 건씩 주고받는 기업 계정, 인플루언서, 스팸 공격을 받는 헤비 유저의 경우 user_id 하나에 수백만 건의 메일 메타데이터 레코드가 매핑된다.

NoSQL에서 단일 파티션 크기가 너무 커지면(보통 수백 MB 이상), 해당 파티션을 관리하는 특정 물리 노드의 메모리와 디스크가 터져버리는 Wide Partition 현상이 발생하고, 해당 노드로만 트래픽이 몰리는 핫스팟(HotSpot) 장애로 이어진다.
또한, 메일함 공유 관점에서도 특정 대용량 공지 메일을 여러 사용자 파티션에 매번 통째로 복사해서 저장해야 하므로 스토리지가 낭비된다.

이 문제를 방지하기 위해 시스템은 user_id 뒤에 사서함 종류를 뜻하는 folder_id를 결합한 <user_id, folder_id> 형태의 복합 파티션 키를 채택한다.
이렇게 하면 한 유저의 데이터라 할지라도 받은 편지함, 보낸 편지함, 스팸함 등이 서로 다른 물리 노드로 분산 적재되므로 단일 파티션이 비대해지는 리스크를 회피할 수 있다.


6.4. NoSQL 질의 패턴

대규모 분산 메일 아키텍처가 실제로 지원해야 하는 주요 데이터베이스 질의 패턴과 NoSQL 테이블 가상 설계 스키마이다.

질의 1. 특정 사용자의 모든 폴더 목록 조회

사용자별 폴더 목록

  • 파티션 키: user_id
  • 사용자가 로그인했을 때 좌측 사이드바 메뉴를 구성하기 위해 사용자의 모든 폴더 구조 조회

질의 2. 특정 폴더 내의 이메일 목록 최신순 출력

폴더별 이메일

  • 복합 파티션 키: <user_id, folder_id>
  • 클러스터 키: email (TIMEUUID, 내림차순)

💡TIMEUUID 데이터 타입이 정렬에 필수인 이유

일반적인 정수형 자동 증가 ID는 단일 RDBMS 환경에서만 순서가 보장될 뿐, 서버 수천 대가 분산되어 작동하는 NoSQL 환경에서는 동기화가 불가능해 사용할 수 없다.
그렇다고 일반 UUID를 쓰자면 완전히 무작위 문자열이기 때문에 시간순 정렬이 불가능하다.

이를 해결하는 게 바로 TIMEUUID(version 1 UUID)이다.

TIMEUUID는 기기의 고유 MAC 주소와 함께 1582년 10월 15일 이후 100ns 단위로 흘러간 정밀 타임스탬프 값을 내부 비트 구조의 상위에 내장하여 생성된다.

결과적으로 중앙 서버의 통제 없이 수많은 분산 워커들이 동시다발적으로 ID를 생성하더라도 전 세계에서 유일한 고유성을 완벽히 만족함과 동시에, 바이트 비교 만으로도 ms 미만의 정밀한 시간 역순 정렬이 디스크 레벨에서 자동으로 이루어지게 만드는 분산 아키텍처의 필수 컴포넌트이다.


질의 3. 이메일 상세 내용 조회 및 첨부 파일 데이터 조회

사용자별 이메일

사용자가 리스트에서 특정 메일을 클릭했을 때 본문과 첨부파일 포인터를 조회하는 테이블 구조이다.
대용량 쿼리 효율을 위해 유저별 메일 상세 테이블(emails_by_user)와 첨부 파일 매핑 테이블(attachments)을 분리하여 운영한다.


6.5. 읽음/안읽음 메일 필터링과 NoSQL 테이블 비정규화

RDBMS 환경에서는 메일의 읽음 상태 필터링을 아래와 같이 간단한 SQL 조건절로 수행할 수 있다.

/* RDBMS 환경의 조건절 질의 (대규모 환경에서는 풀스캔 유발로 부적합) */
SELECT * FROM emails_by_folder
WHERE user_id = :user_id AND folder_id = :folder_id AND is_read = false
ORDER BY email_id DESC;

<위 쿼리에서 풀스캔이 일어나는 이유>
RDBMS에는 쿼리를 어떻게 실행할지 결정하는 옵티마이저가 존재한다.
옵티마이저는 인덱스를 탈지, 아니면 풀스캔을 할지 Cost를 계산하는데, 이 때 가장 중요하게 보는 것이 Cardinality(값의 종류가 얼마나 다양한가)이다.

  • 낮은 Cardinality로 인한 옵티마이저 동작
    • is_read 컬럼은 true/false 딱 두 가지 값만 가진다.
    • 만일 어떤 헤지 유저의 받은 편지함에 100만 건의 메일이 있고, 그 중 90만 건이 읽지 않은 메일(is_read=false)라고 가정해보자.
    • 옵티마이저 입장에서는 ‘어차피 100만 건 중 90만 건(90%)이나 찾아야 하는데, 인덱스를 타고 디스크를 왔다 갔다(Random I/O)하느니 그냥 풀스캔(Sequential Scan)이 훨씬 빠르겠다‘라고 판단해 버린다.
    • 결과적으로 인덱스를 만들어 두었더라도 옵티마이저가 이를 의도적으로 무시하고 풀스캔을 하는 현상이 발생한다.
  • 복합 인덱스 범위 지정 후 ‘폴더 내 전수 조사’
    • 만일 테이블에 (user_id, folder_id) 순으로 복합 인덱스가 아주 잘 걸려있다고 가정해보자.
    • 데이터베이스 엔진은 인덱스를 보고 WHERE user_id = :user_id AND folder_id = :folder_id 구역까지는 단 몇 번의 탐색(B-Tree 탐색)만으로 빠르게 좁혀 들어간다.
      • 여기까지는 풀스캔이 아니다.
    • 문제는 그 다음이다. 해당 유저의 특정 폴더 ‘안’에 들어왔는데, 정작 인덱스 구조 안에는 is_read에 대한 정보가 없다.
    • 결국 엔진은 그 폴더 안에 존재하는 수만~수백만 건의 메일 레코드 데이터를 디스크 페이지에서 일일히 풀스캔하여 is_read = false인 데이터가 맞는지 하나하나 필터링해야 한다.
      • 폴더 범위를 좁혔을 뿐, 그 내부에서는 사실상 풀스캔과 다름없는 무거운 무작위 디스크 I/O가 발생하는 것이다.
  • ORDER BY 부하와의 최악의 시너지
    • 여기에 ORDER BY email_id DESC 까지 붙어있다.
    • 만약 is_read 조건 때문에 인덱스를 제대로 타지 못하고 풀스캔했는데, 데이터베이스는 이 필터링 된 수많은 데이터들을 시간 역순으로 다시 정렬해야 한다.
    • 메모리 공간(Sort Buffer)이 부족하면 디스크에 임시 테이블을 만들어서 정렬하는 File Sort까지 발생하여 CPU와 디스크 I/O를 동시에 마비시키는 최악의 부하가 발생한다.

요약하자면 이 문제가 가장 치명적인 이유는 ‘Boolean 필터링은 인덱스 효율이 극도로 떨어지며, 대규모 사서함 환경에서는 특정 폴더 내의 수백만 건을 전수 조사하게 만들기 때문’이다.


NoSQL 인덱스의 치명적인 제약 조건과 아키텍처적 우회 방안

Apache Cassandra를 비롯한 대부분의 고성능 분산 NoSQL 데이터베이스는 오직 파티션 키와 클러스터 키 컬럼에 대한 조건문(WHERE)만 허용한다.
키가 아닌 일반 컬럼인 is_read (Boolean) 상태 값에 대고 조건 질의를 날리면 NoSQL 엔진은 쿼리를 거부하거나, 내부 모든 파티션 노드를 풀스캔하는 최악의 병목(ALLOW FILTERING)을 유발한다.

ALLOW FILTERING은 Apache Cassandra와 같은 분산 NoSQL 데이터베이스에서 기본키(파티션 키 및 클러스터 키)로 인덱싱되지 않은 일반 컬럼을 WHERE 절에 사용할 때, 예측 불가능한 성능 저하(전수 스캔)을 감수하고서라도 쿼리를 강제 실행하도록 데이터베이스 엔진에 승인하는 명시적 옵션이다.


💡ALLOW FILTERING

ALLOW FILTERING 의 메커니즘은 아래와 같다.

클라이언트 쿼리 요청: WHERE user_id = 'alice' AND is_read = false ALLOW FILTERING
                              │
                              ▼
[1단계: 파티션 키(user_id)를 해싱하여 해당 데이터가 있는 특정 분산 노드로 진입]
                              │
                              ▼
[2단계: 노드 디스크에서 해당 유저의 사서함 레코드 '전체(Full Partition)'를 메모리로 로드]
                              │
                              ▼
[3단계: JVM 힙(Heap) 메모리 단에서 루프를 돌며 'is_read == false'인 데이터만 필터링]
                              │
                              ▼
[4단계: 필터링된 최종 결과 몇 건만 클라이언트에 반환 (나머지 대량의 로드 데이터는 버림)]

즉, 메모리로 무식하게 다 퍼 올려서 수작업으로 골라내기를 수행하는 것이다.

데이터가 몇십 건 없는 개발 환경에서는 수 ms 만에 끝나기 때문에 아무 문제 없어 보이지만, 여기서 설계하는 10억 명 규모의 하이엔드 시스템에서는 이야기가 완전히 달라진다.

  • ⚠️극심한 읽기 증폭
    • 사용자가 원하는 데이터는 단 5건의 ‘안 읽은 메일’일 지라도, 그 사서함에 쌓인 10만 건의 메일 원시 데이터를 읽기 위해 디스크의 수많은 불변 파일(SSTable)을 다 뒤져서 메모리로 가져와야 한다.
    • 단 몇 건을 위해 수천배의 디스크 I/O 대역폭을 낭비하는 것이다.
  • ⚠️JVM GC Hell
    • 초당 수만 건의 목록 조회(High QPS)가 들이치는데, 매 요청마다 수만 줄의 로우 데이터를 노드 메모리에 적재했다가 버리는 행위가 반복되면 Heap 메모리에 엄청난 가비지 객체가 쌓인다.
    • 결국 GC가 구동되면서 서버가 일시적으로 완전히 마비되는 Stop-the-world 현상으로 이어진다.

아래는 GC Hell 이 발생하는 흐름이다.

[폭발적인 QPS 인입] 
       │
       ▼
[노드마다 수백만 개 로우를 JVM 힙 메모리로 로드] 
       │
       ▼
[메모리 임계치 초과 및 극심한 GC(가비지 컬렉션) 발생] 
       │
       ▼
[Stop-the-World (서버 일시 마비)] 
       │
       ▼
[클라이언트 타임아웃 및 재시도 요청 폭주 (도미노 붕괴)]

<ALLOW FILTERING을 써도 되는 유일한 예외>
이 옵션이 무조건 나쁜 것은 아니다.
아티텍처 관점에서 딱 한가지 안전하게 쓸 수 있는 시나리오가 있다.

‘파티션 키 조건을 극도로 좁혀서, 필터링할 대상 로우 수가 확실하게 통제될 때’

예를 들어 쿼리에서 파티션 키와 클러스터 키를 완벽하게 지정하여 이미 디스크 상에서 단 10~20줄의 레코드만 읽어오도록 바운더리가 쳐진 상태라면,
그 안에서 인덱스 없는 컬럼 한 두개를 ALLOW FILTERING 으로 걸러내는 것은 메모리나 디스크에 부담을 주지 않으므로 안전하고 유용하다.

구분일반 인덱스 쿼리ALLOW FILTERING 쿼리
성능 예측 가능성데이터 규모가 커져도 항상 수 밀리초(ms) 이내 보장파티션 내 데이터양에 비례하여 기하급수적으로 느려짐
주요 병목 지점없음 (최적화된 탐색)극심한 디스크 읽기 증폭 및 노드 메모리(GC) 압박
대규모 시스템 적합성적합부적합 (대형 장애의 주원인)

결론적으로 대규모 분산 아키텍처를 설계할 때 ALLOW FILTERING을 써야만 쿼리가 돌아가는 상황을 맞이했다면, 그것은 쿼리의 문제가 아니라 데이터 모델링(스키마 설계) 단계부터 잘못되었다는 강력한 경고 시그널이다.

이를 해결하기 위해 저장 공간을 조금 더 희생하더라도 아래에 나올 내용처럼 읽음/안읽음 전용 테이블을 따로 만들어서 데이터를 복사 적재하는 비정규화 전략을 취하는 것이 하이엔드 분산 시스템을 구축하는 정석이다.


💡NoSQL이 파티션 키와 클러스터 키를 활용하여 쿼리를 처리하는 예시

분산 NoSQL인 Cassandra에서 인덱스 제약을 우회하고 대규모 질의 속도를 초고속으로 유지하기 위해 사용하는 실제 CQL(Cassandra Query Language) 구동 메커니즘이다.

/* 1. NoSQL의 올바른 복합키 검색 예시 (디스크 순차 탐색으로 매우 빠름) */
SELECT * FROM emails_by_folder 
WHERE user_id = 'usr_alice' AND folder_id = 'fld_inbox'
AND email_id < max_timeuuid_tracker 
LIMIT 20;

위 쿼리는 파티션 키로 정확한 서버 노드를 찾고, 디스크를 시간순으로 정렬된 키인 email_id의 특정 구간만 정확히 오프셋 스캔하므로 10억 명 규모에서도 수 ms 안에 연산이 종료된다.

하지만 읽음/안읽음 상태 필터링을 위해 이 스키마를 고집하면 대규모 트래픽에서 병목이 생기므로, 여기서는 테이블 비정규화 전략을 사용하여 구조를 완전히 분리한다.
주어진 폴더에 속한 모든 이메일을 조회한 후 애플리케이션 단에서 필터링해도 되지만 그 방안은 대규모 서비스에는 그다지 적합하지 않다.

읽은 메일과 읽지 않은 메일을 위한 테이블

위 이미지에서 보듯 사서함을 read_emailsunread_emails라는 두 개의 독립된 테이블로 복사 분할해 버리는 것이다.

  • 상태 변경 시 애플리케이션 단의 듀얼 트랜잭션 제어 흐름
    • 유저가 ‘안 읽은 메일’ 목록에서 특정 메일을 클릭하는 순간, BE 애플리케이션은 아래와 같이 NoSQL 쓰기 파이프라인을 작동시킨다.
      • unread_emails 테이블에서 해당 email_id 로우 삭제(DELETE)
      • read_emails 테이블에 해당 메일 메타데이터 로우를 새롭게 삽입(INSERT)

쓰기 작업이 증가하고 코드는 다소 복잡해지지만, NoSQL의 핵심 장점인 ‘쓰기 비용은 매우 싸다’를 적극 활용하여 대규모 환경에서 고속 필터링 조회 성능을 취하는 트레이드오프 설계 방식이다.

비교 항목[Bad] 일반 컬럼 필터링 스키마[Good] 읽음/안읽음 테이블 분리 스키마
디스크 I/O 방식파티션 내 모든 SSTable을 뒤지는 무작위 스캔필요한 테이블(unread_emails)의 특정 구간만 순차 읽기
메모리(JVM Heap) 부하전수 조사를 위한 대량의 데이터 로드로 GC 위험 높음딱 보여줄 페이지 분량(예: 20건)만 로드하므로 메모리 매우 안정적
대규모 QPS 감당 여부불가능 (디스크 및 CPU 병목으로 노드 다운)완벽 지원 (쓰기 비용을 활용해 읽기 성능 극대화)
애플리케이션 복잡도단순함 (상태값만 갱신하면 끝)복잡함 (상태 변경 시 Delete와 Insert를 동시에 제어해야 함)

6.6. 이메일 스레드(Threads) 대화 타래 재구성 원리

현대적인 이메일 클라이언트가 제공하는 가장 핵심적인 UX는 연관된 답장 메일들을 하나의 타래로 묶어 보여주는 스레딩(Threading) 기능이다.
분산 환경에서는 대형 메일 클라이언트의 구동 원리인 JWZ 스레딩 알고리즘을 주로 활용한다.

이 알고리즘은 이메일 발송 시 메시지 내부에 규격으로 포함되는 아래 3가지 핵심 헤더 포인터를 추적하여 메모리상에서 트리 구조를 동적으로 재구성한다.

{
  "headers": {
    "Message-Id": "<msg_root_7BA04B2A@gmail.com>",
    "In-Reply-To": "<msg_root_7BA04B2A@gmail.com>",
    "References": [
      "<msg_root_7BA04B2A@gmail.com>",
      "<msg_reply_991A2B3C@gmail.com>"
    ]
  }
}
핵심 헤더 (Header)아키텍처적 역할 및 기능 설명
Message-Id해당 이메일 메시지가 생성될 때 발신 클라이언트가 부여하는 전 세계 유일한 식별자
In-Reply-To현재 메일이 어떤 부모 이메일 메시지에 대한 답신(Reply)인지 직전의 부모 Message-Id를 기록함
References최초의 시작 메일부터 현재 메일에 이르기까지 대화 타래에 엮인 모든 조상 및 형제 메시지 식별자 리스트를 체인 형태로 누적 보관함

이 필드들이 존재하면 이메일 클라이언트는 메일 DB에서 특정 유저의 메일 목록을 조회한 후, BE 메모리단에서 이 조상-부모 포인터 체인을 엮어 복잡한 관계형 Join 연산 없이도 트리 형태의 대화 타래를 브라우저에 렌더링할 수 있다.


6.7. 요약

대규모 분산 이메일 시스템의 상세 설계 핵심은 기성 RDBMS의 무거운 랜덤 I/O 잠금을 과감히 포기하고, 분산 NoSQL 기반의 복합키 모델링과 과감한 테이블 비정규화를 감행하는 것이다.
복합 파티션 키로 사서함의 크기 팽창을 막고, TIMEUUID 기반 클러스터 키로 디스크 순차 읽기를 보장하며, 읽음 상태별로 테이블을 쪼개어 배치함으로써 10억 명 규모에서도 지연 없는 초고속 데이터 레이어를 완성할 수 있다.


7. 스팸 메일함을 피하기 위한 이메일 전송 가능성 극대화 전략

이메일 전송 가능성이란 발송된 이메일이 수신측 ISP(Gmail, Outlook 등)의 스팸 필터에 걸리지 않도록 하고, 사용자의 ‘받은 편지함’에 안전하게 도착하는 비율을 의미한다.
statista 사에서 수행한 연구에 따르면 메일 가운데 50%가 스팸으로 분류된다.
대규모 분산 메일 시스템에서는 아무리 인프라가 견고해도 이 전송 가능성이 확보되지 않으면 무용지물이 되므로, 아키텍처 설계 단계에서 보안 프로토콜과 IP 평판 제어가 완벽히 융합되어야 한다.


7.1. 이메일 인증 프로토콜: SPF, DKIM, DMARC

현재 이메일 생태계에서 수신측 서버는 신원이 불분명한 IP의 메일을 즉시 차단한다.
자신이 신뢰할 수 있는 합법적인 발송자임을 증명하기 위해 아래 3가지 핵심 인증 메커니즘을 DNS 레벨에 필수적으로 구성해야 한다.

  • SPF(Sender Policy Framework)
    • 개념: 도메인 소유주가 ‘우리 도메인의 메일은 이 IP 주소 목록에서만 발송된다.’고 DNS TXT 레코드로 명시해두는 화이트리스트 규격이다.
    • 검증 방식: 수신측 메일 서버는 메일이 인입되면 발신인의 도메인 DNS를 조회하여, 메일을 보낸 실제 서버 IP가 SPF 레코드 목록에 포함되어 있는지 대조한다.
  • DKIM(DomainKeys Identified Mail)
    • 개념: 발신 메일의 헤더에 암호화된 디지털 서명을 포함하여, 전송 과정에서 메일 내용이 위변조되지 않았음을 증명하는 기술이다.
    • 검증 방식: 발신 서버가 비대칭키 쌍의 Private Key로 메일 헤더를 부호화해서 보내면, 수신 서버는 발신 도메인의 DNS에 공개된 Public Key를 가져와 서명을 해독하고 유효성을 검증한다.
  • DMARC(Domain-based Message Authentication, Reporting and Conformance)
    • 개념: SPF와 DKIM의 검증 결과를 기반으로, ‘만일 두 인증 중 하나라도 통과하지 못하면 이 메일을 어떻게 처리하라’고 수신 서버에게 내리는 최종 가이드라인 정책이다.
    • 정책 레벨:
      • none: 모니터링만 하고 메일은 정상 수신 처리
      • quarantine: 메일을 스팸함으로 격리
      • reject: 메일 수신을 강력하게 Block하고 반송

아래는 지메일 메시지의 헤더 예시이다.
발송자 도메인 @info6.citi.com 이 SPF, DKIM, DMARC의 검증을 거쳤다는 사실이 기록되어 있다.

지메일 헤더 예시

메일 서버를 구성하고 메일을 보내는 것은 쉽지만 특정 사용자의 메일함에 실제로 메일이 전달되도록 하는 것은 어렵다. 이메일이 스팸 폴더에 들어가버리면 수신자가 메일을 읽을 가능성은 아주 낮아진다.


💡SPF와 DKIM이 둘 다 적용되어 있을 때, 하나만 실패해도 메일이 차단될까?

  • SPF와 DKIM의 독립적 관계
    • 기본적으로 SPF와 DKIM은 서로 완전히 독립적인 프로토콜이다.
    • 네트워크 포워딩 환경 등 특이 시나리오에서는 IP가 바뀌어 SPF는 실패하지만 암호 서명은 유지되어 DKIM은 통과하는 경우가 발생할 수 있다.
    • DMARC가 설정되어 있지 않다면, 수신측 ISP(Gmail 등) 자체 알고리즘에 따라 복합 점수를 매겨 통과 여부를 결정한다.
  • DMARC 정렬과 의사결정 구조
    • DMARC는 이 혼란을 정리하기 위해 정렬 개념을 도입한다.
    • 사용자의 눈에 보이는 From 헤더의 도메인이 SPF 검증 도메인 및 DKIM 서명 도메인과 완벽히 일치하는지 체크한다.
    • DMARC 정책이 reject로 강하게 세팅되어 있다면, SPF와 DKIM 검증 중 단 하나라도 실패하고 도메인 정렬이 깨졌을 때 수신 서버는 메일을 차단해버린다.
    • 따라서 대규모 아키텍처에서는 완벽한 무결성을 위해 세 가지 레코드를 체인처럼 엮어 동기화해야 한다.

7.2. IP 평판 관리와 발송 IP 범주화 전략

메일을 발송하는 인프라의 외부 IP 주소가 어떤 평판을 유지하고 있느냐에 따라 ISP 정책 수락이 달라진다.
시스템은 공유 IP와 전용 IP의 장단점을 파악하는 것을 넘어, 이메일의 성격에 따라 발송 인프라를 격리(범주화)해야 한다.


발송 구조별 특징 비교

아키텍처 분류공유 IP (Shared IP)전용 IP (Dedicated IP)
개념여러 유저나 서비스가 동일한 발송 IP 대역을 함께 공유하여 사용우리 서비스(또는 특정 헤비 유저) 전용으로 독립된 발송 IP를 고정 할당
장점초기 비용이 저렴하며, 메일 발송량이 적어도 다른 유저들의 트래픽 덕분에 기본 IP 평판이 유지됨타 유저의 스팸 발송으로 인한 동반 블록 위험이 전혀 없음. 평판 통제권 100% 확보
단점같은 IP를 쓰는 이웃 유저가 스팸 메일을 대량 살포하면 나까지 덩달아 스팸 IP로 낙인찍힘비용이 비싸며, 주기적으로 대량의 메일을 일정하게 발송하여 스스로 평판을 관리해야 함
적합한 타깃스타트업 초기 단계, 혹은 일일 발송량이 수천 건 미만인 경량 서비스10억 유저 감당 시스템, 금융/결제 알림 시스템, 일 발송량 수십만 건 이상의 엔터프라이즈

이메일 성격에 따른 IP 범주화

범주가 다른 이메일은 반드시 물리적으로 분리된 별도의 IP 주소를 통해 발송해야 한다.

  • 마케팅/광고성 이메일
    • 대량 발송 특성을 가지며 수신자가 ‘스팸’이나 ‘프로모션’ 탭으로 분류할 확률이 매우 높다.
  • 트랜잭션(중요 알림) 이메일
    • 비밀번호 재설정, 결제 영수증, 보안 인증 등 사용자가 즉시 받아보아야 하는 필수 메일

만일 중요 알림 메일과 광고성 마케팅 메일을 동일한 서버(IP)에서 발송하면, ISP의 보안 엔진은 마케팅 메일의 높은 스팸 신고율과 낮은 오픈율을 근거로 해당 IP 전체의 평판 점수를 깎아버린다.
그 결과 같은 IP를 공유하던 다른 유저의 비밀번호 변경 인증 메일까지 ISP 단에서 대거 블록 당하거나 스팸함으로 가는 치명적인 연쇄 장애가 발생한다.

새로 구성한 메일 서버가 보내는 메일은 십중팔구 스팸 폴더로 떨어지는데, 인터넷에서 좋은 평판을 쌓을 기회가 전혀 없었기 때문이다. 이메일 전송 가능성을 높이기 위해 아래 요소들을 고려해야 한다.


7.3. IP Warming

대규모 서비스를 위해 새로운 전용 IP(Dedicated IP) 대역을 대량 구매했다고 해서, 첫날부터 1억 명에게 메일을 보내면 100% 확률로 모든 ISP에서 영구 차단당한다.

ISP 보안 엔진은 과거 발송 이력이 전혀 없는 신규 IP(Cold IP)에서 갑자기 대량의 트래픽이 터져 나오는 것을 전형적인 ‘해킹된 봇넷의 스팸 공격’으로 인지하기 때문이다.
이를 방지하기 위한 필수 절차가 바로 IP Warming이다.

IP Warming은 새 발송 IP에 대한 ISP의 신뢰를 구축하기 위해, 수주에 걸쳐 일일 이메일 발송 볼륨을 단계적으로 조금씩 늘려나가는 과정이다.

아마존 SES에 따르면 새로운 IP 주소를 메일 발송에 아무 문제없이 쓸 수 있게 되는 때는 대략 2주가 소요된다고 한다.

[1주차: 극도의 조심성] ──> [2주차: 완만한 성장] ──> [3주차: 본격적인 가속] ──> [4주차: 타깃 스케일 달성]
  - 1일차: 100건            - 8일차: 2,000건          - 15일차: 20,000건        - 22일차: 200,000건
  - 3일차: 500건            - 10일차: 5,000건         - 17일차: 50,000건        - 24일차: 500,000건
  - 7일차: 1,000건          - 14일차: 10,000건        - 21일차: 100,000건       - 28일차: 1,000,000건+

💡IP Warming 기간 동안 발송 속도와 볼륨 제어는 BE 내에서 어떻게 구현할까?

  • 메시지 큐 기반의 처리율 제한(Rate Limit) 연동
    • 인프라 단에서 IP 웜업 스케줄러가 BE 외부 전송 큐의 Consumer 스레드 풀 크기를 동적으로 제어한다.
    • 예를 들어 1일차 제한이 100건이라면, 스케줄러는 당일 총 100건의 메시지만 워커로 넘어가도록 게이트를 제어하고, 초과 유입되는 발송 요청은 버퍼 큐에 묶어두거나 애플리케이션 단에서 ‘내일 발송 예약’ 상태로 Backoff 시킨다.
  • Engagement 기반의 유저 필터링 기법
    • 웜업 성공률을 극대화하려면 1~2주차 초기 트래픽에 ‘가장 메일을 잘 열어볼 것 같은 활성 유저(High Engagement User)’를 배치해야 한다.
    • 최근 1주일 이내에 로그인했거나 메일을 읽었던 유저들의 데이터셋을 Redis 캐시나 분석 DB에서 추출하여 웜업 초기 타깃으로 넣는다.
    • ISP 필터는 ‘이 신규 IP가 보낸 메일은 수신자들이 다들 바로 열어본다’라고 판단하여 평판 점수를 올려주게 된다.

즉, BE 내에서는 외부 전송 큐 배후의 Consumer 워커 스레드 풀 개수를 이 웜업 스케줄러와 연동하여 일일 쿼터를 초과하는 메일을 다음 날로 Backoff 시키는 제어 로직을 구동한다.


7.4. 스팸 필터 레이더 우회 및 피드백 루프 운영

발송 파이프라인의 최종 단계에서는 수신자들의 부정적인 반응을 실시간으로 감지하고 처리하는 시스템이 작동해야 평판 하락을 막을 수 있다.


피드백 루프(FBL, FeedBack Loop) 연동 아키텍처

지메일 등 대형 ISP는 사용자가 메일 화면에서 스팸으로 신고 버튼을 누르면, 발신자 측에 이 사실을 Webhook이나 메일 형태로 즉시 통보해주는 ‘피드백 루프’ 서비스를 제공한다.

  • 수신자가 메일함에서 우리 메일을 ‘스팸 신고’함
  • ISP(예: Gmail)가 우리 시스템의 FBL 수신 엔드포인트(HTTP API)로 신고 이벤트 패킷 전송
  • 우리 백엔드의 FBL 처리 워커가 해당 수신자 이메일 주소를 발송 금지 블랙리스트 테이블에 즉시 영속 적재
  • 이후 마케팅이나 공지 대량 발송 워커가 구동될 때 이 발송 금지 블랙리스트를 먼저 조회하여 해당 유저를 발송 대상에서 원천 배제

사용자가 스팸 신고를 했음에도 불구하고 시스템이 감지하지 못하고 다음 날 또 메일을 보내면, ISP는 해당 발신 IP 대역 전체를 영구 차단 리스트에 올린다.
따라서 FBL 수신 가공은 실시간 스트리밍 아키텍처로 매우 기민하게 움직여야 한다.


Hard Bounce와 Soft Bounce의 격리 처리

아래 그림은 외부 ISP로부터 인입되는 다양한 형태의 피드백(스팸 신고, 영구 실패, 일시 오류)을 수신 큐 배후의 워커들이 어떻게 분기하여 처리하는지 보여주는 아키텍처 내부 흐름이다. 피드백 유형별 처리

메일 발송 후 목적지 서버로부터 에러 리턴을 받았을 때, 에러의 종류를 엄격히 분기해야 파이프라인 오염을 막을 수 있다.

  • Hard Bounce(경성 반송, 5xx 에러 수신 시)
    • 수신자 이메일 주소가 존재하지 않거나 도메인이 완전히 폐쇄된 경우처럼 영구적인 발송 실패를 의미함
    • 유저 계정 상태를 Invalid로 변경 ➔ 영구 발송 차단하여 IP 평판 방어
      • 존재하지 않는 주소로 메일을 계속 발송하는 행위는 스팸 봇의 대표적인 특징이기 때문
  • Soft Bounce(연성 반송, 4xx 에러 수신 시)
    • 수신자의 사서함 용량이 가득 찼거나, 상대 서버의 일시적인 네트워크 마비 등 일시적인 발송 실패를 의미함
    • 메시지 큐의 Retry Queue로 우회 ➔ 지수적 백오프 알고리즘에 의거해 재시도 스케줄링
  • FBL(Spam) 알림 수신 시
    • 수신인이 ‘스팸 신고’ 버튼을 누른 경우
    • 즉시 Suppression List(발송 금지 테이블)에 유저 저장 ➔ 향후 모든 발송 대상에서 제외

이 모든 용어를 기억할 필요는 없고, 이메일이 목적지에 성공적으로 도착하기 어렵다는 사실만 알면 된다. 도메인 지식이 필요한 것은 물론이고, ISP와 좋은 관계를 유지할 필요도 있다.


8. 고성능 검색 엔진 설계

이메일 검색 엔진 아키텍처란 사용자가 사서함 내 수많은 건의 메일 중 본문, 제목, 발신인 등의 키워드를 입력했을 때 수 ms 이내에 정확한 결과를 찾아 반환하는 고속 분산 텍스트 탐색 시스템이다.
메인 저장소인 NoSQL의 부하를 가중시키지 않기 위해 비동기 데이터 파이프라인(Kafka/CDC)을 기반으로 검색 전용 역인덱스(Inverted Index) 클러스터를 독립적으로 운용하는 것이 골자이다.


8.1. 이메일 검색 특성: 극단적인 Write-Heavy

대규모 메일 서비스에서 검색 기능을 설계할 때 가장 먼저 이해해야 하는 시스템적 특성은 Read보다 Write 연산이 압도적으로 많이 발생한다는 점이다.

  • 끊임없는 쓰기(색인)
    • 이메일 검색 엔진은 사용자가 이메일을 발송할 때, 수신할 때, 그리고 메일을 삭제하거나 폴더를 이동할 때마다 실시간으로 인덱싱 작업을 즉각 수행해야 한다.
    • 10억 명 규모에서는 초당 수십만 건의 색인 요청이 쏟아진다.
  • 간헐적인 읽기(검색)
    • 그에 반해 검색(읽기) 연산은 사용자가 ‘검색’ 버튼을 누를 때만 간헐적으로 실행된다.

따라서 이메일 검색 시스템은 ‘초고속 대용량 쓰기(Index Write) 성능을 버텨내면서도 읽기 Latency를 타협하지 않는 구조’로 설계되어야 한다.

구글 검색과 이메일 검색의 차이를 보자.

구분범위 (Scope)정렬 기준 (Sorting)정확도 및 실시간성 (Accuracy)
구글 검색인터넷 전체 문서 세트관련성 및 문서 평판 점수 기준색인에 다소 시간이 걸리므로 신규 항목이 검색 결과에 즉시 나타나지 않아도 수용됨
이메일 검색사용자의 개인 메일함 (Tenant 격리)수신 시각, 첨부 파일 유무, 날짜 범위, 읽음 여부 등의 속성 기준색인 작업이 거의 실시간으로 이루어져야 하며, 내가 방금 보낸 메일도 정확히 검색되어야 함

Tenant

전체 시스템 중 특정 사용자가 독립적으로 사용하는 가상의 공간이자 단위


8.2. 검색 기능을 지원하기 위한 두 가지 선택지

대규모 시스템에서 사서함 검색을 구현하는 방법은 크게 두 가지가 있다.
각 방식의 장단점과 확장성 한계에 대해 알아본다.


8.2.1. 방안 1: 데이터 저장소(NoSQL) 내부의 내장 검색 기능 활용

일부 최신 분산 데이터베이스나 RDBMS는 자체적인 Full-Text Search(전문 검색) 인덱스 기능을 내장하고 있다.

  • 장점:
    • 아키텍처가 단순해진다.
    • 별도의 검색 엔진 클러스터를 띄우고 운영할 필요가 없으므로 데이터 동기화 파이프라인 관리 비용이 제로에 수렴한다.
  • 단점:
    • 검색 인덱싱 연산과 메일 송수신 핵심 연산이 동일한 서버의 CPU와 메모리 지원을 공유한다.
    • 사용자가 대량 검색을 하거나 마케팅 메일 폭탄으로 색인 트래픽이 튀면, 메인 DB의 처리 성능(QPS)이 동반 추락한다.
    • 페타바이트급 환경에서는 확장의 한계가 매우 명확하다.

💡데이터베이스에 내장된 전용 검색 솔루션을 이용한다는 것은 정확히 무엇일까?

외부의 별도 검색 엔진(Elasticsearch 등)에 데이터를 복사하지 않고, 데이터가 저장된 바로 그 메인 스토리지 엔진 내부에서 검색 인덱싱 알고리즘을 밀결합(Tight-Coupling)하여 구동하는 방식을 뜻한다.

  • 실제 아키텍처 예시:
    • MS Outlook의 기반이 되는 Exchange Server가 대표적이다.
    • Exchange 인프라는 내부 메타데이터 스토리지 엔진과 검색 인덱싱 코어를 로컬 서버 노드 단위로 통합하여 처리한다.
  • 또 다른 예로는 Apache Cassandra DB에 내장된 SASI(SSTable-Attached Secondary Index) 기능이 있다.
    • 이는 데이터를 디스크에 쓸 때(SSTable 생성 시) 검색용 역인덱스도 한 장표에 함께 쓰는 방식이다.

💡왜 일반 NoSQL/RDMBS 질의로는 검색이 불가능할까?

대규모 메일 서비스에서 ‘메인 DB 테이블에 인덱스를 잘 걸어서 검색하면 안될까?’ 라는 의문이 들 수 있다.

  • RDBMS의 텍스트 검색 한계
    • RDBMS에서 본문 검색을 위해 LIKE '%공지사항%' 같은 와일드카드 질의를 던지는 순간, 인덱스를 타지 못하고 풀스캔이 발생한다.
    • 페타바이트 단위의 데이터셋에서는 한 번의 쿼리로 서버 전체가 다운된다.
  • NoSQL 키-밸류 구조의 한계
    • 앞서 본 Cassandra는 특정 파티션 키(<user_id, folder_id>) 내부의 정렬을 보장하지만, ‘내가 보낸 메일 중 ‘계약서’라는 단어가 들어간 메일 모두 조회’ 같은 임의의 텍스트 다중 조건 필터링에는 태생적으로 대응이 불가하다.
    • secondary index(보조 인덱스)를 지원하긴 하지만, 분산 노드 간의 극심한 네트워크 분산 스캔(Scatter-Gather) 현상으로 인해 실무에서는 절대 사용 금지 항목이다.

위와 같은 문제를 해결하기 위해 Gmail이나 Outlook 등은 전용 분산 문서 검색 엔진을 스토리지 레이어 옆에 병렬로 배치한다.
오픈소스 진영에서는 Lucene 기반의 Elasticsearch 또는 OpenSearch 클러스터가 표준이 된다.


8.2.2. 방안 2: 독립된 전문 검색 엔진(Elasticsearch) 도입

메인 저장소 옆에 전용 분산 문서 검색 엔진 클러스터(Elasticsearch 또는 OpenSearch)를 독립적으로 배치하는 전략이다.

  • 장점:
    • 핵심 메일 데이터 레이어와 검색 레이어가 완벽히 분리된다.
    • 검색 트래픽이 아무리 폭주해도 메일 송수신 코어 서버는 아무런 영향을 받지 않는다.
    • 또한 Lucene 기반의 강력한 역인덱스(Inverted Index) 구조를 사용하여 텍스트 토큰 탐색 시간이 복잡도 \(O(1)\) 수준으로 수렴한다.
  • 단점:
    • 메인 데이터베이스와 검색 엔진 간의 데이터를 동기화해야 하는 백엔드 파이프라인을 추가로 구축하고 유지보수해야 하는 운영 복잡도가 증가한다.

8.3. Elasticsearch 기반 분산 검색 레이어 설계

8.3.1. 역인덱스 아키텍처와 유저 사서함 격리

질의가 대부분 사용자의 이메일 서버에서 실행되므로 user_id를 파티션 키로 사용하여 같은 사용자의 이메일은 같은 노드에 묶어야 한다.

Elasticsearch는 기본적으로 문서의 ID 값을 해싱하여 샤드에 분산하지만, 인덱싱 및 검색 시 routing 매개변수를 명시하면 특정 기준에 따라 문서를 강제 그룹핑할 수 있다.

# 이메일 색인 시 user_id를 라우팅 키로 지정
PUT /mail_index/_doc/msg_12345?routing=user_id_999
{
  "user_id": "user_id_999",
  "subject": "프로젝트 계약서 최종본입니다.",
  "body": "안녕하세요, 요청하신 계약서 내용을..."
}

이렇게 설정하면 user_id_999 를 가진 모든 이메일 문서는 클러스터 내의 동일한 특정 루씬 샤드(Lucene Shard)로만 라우팅되어 저장된다.
결과적으로 사용자가 검색을 요청할 때도 ?routing=user_id_999 쿼리를 함께 던지면, 검색 엔진은 클러스터 전체 노드를 다 뒤지는 낭비(Scatter-Gather)없이 해당 유저의 데이터가 모여있는 단 하나의 샤드만 검색하므로 엄청난 Latency 단축 효과를 얻는다.


8.3.2. Kafka를 활용한 비동기 인덱싱 파이프라인

메일이 수신되거나 전송될 때 검색 엔진에 데이터를 넣는 과정을 인덱싱이라고 한다.
이 색인 작업을 메일 송수신 API 스레드에서 동기 방식으로 처리하면, 검색 엔진에 부하가 올 때마다 메일 발송 자체가 실패하거나 지연되는 대형 장애가 발생한다.

따라서 이럴 때는 분산 메시지 큐(카프카) 또는 CDC(Change Data Capture) 기술을 활용해 데이터 레이어를 분리하여 운영한다.

사용자는 검색 버튼을 누른 후 다음 결과가 수신될 때까지 기다리므로 검색 요청은 동기 방식으로 처리되어야 한다.
반면, ‘이메일 전송’, ‘이메일 수신’, ‘이메일 삭제’ 같은 이벤트는 처리 결과를 클라이언트에 즉각 전달할 필요가 없으므로 백그라운드 비동기 작업으로 처리하는 것이 이상적이다.

여기서는 카프카를 활용하여 ‘인덱싱 작업을 시작하는 서비스’와 ‘인덱싱을 실제로 수행할 서비스’ 사이의 결합도를 낮추는 방안을 채택하였다.

Elasticsearch

  • 발송 레이턴시 제로
    • 메일 전송 서버는 메인 DB에 메타데이터를 저장하자마자 유저에게 ‘성공’ 응답을 보낸다.
    • 검색 색인은 뒤에서 돌기 때문에 유저가 체감하는 속도가 극대화된다.
  • Backpressure 조절
    • 블랙 프라이데이 등 마케팅 대량 메일이 폭발하여 초당 수만 건의 수신 트래픽이 몰려도 카프카가 중간에서 버퍼 역할을 한다.
    • 인덱서 워커들은 자신이 감당할 수 있는 속도로 큐에서 데이터를 가져와 가공하므로 검색 엔진 클러스터가 과부하로 터지는 현상을 방어한다.

💡페타바이트 급 검색 엔진에서 인덱스가 오염되거나 스키마가 되었을 때 재색인하는 부하는 어떻게 제어할까?

데이터 규모가 페타바이트 스케일을 넘어가면 단일 검색 인덱스를 통째로 교체하는 것은 원천적으로 불가능하다.
이를 극복하기 위해 대형 아키텍처에서는 두 가지 묘수를 사용한다.

  • 시간 기반 인덱스 샤딩
    • 하나의 거대한 인덱스에 10년 치 메일을 다 넣지 않고, mail_search_2026_Q1, mail_search_2026_Q2 처럼 분기별/월별로 인덱스 자체를 쪼개서 생성한다.
    • 사용자가 검색창에 ‘최근 1달’ 조건을 걸면 시스템은 오직 해당 월의 인덱스만 뒤지므로 연산 낭비가 없다.
  • Alias 기반 제로 다운타임 전환
    • 라우팅 레이어에서는 실제 인덱스명을 직접 바라보지 않고 mail_search_read 와 같은 Alias 포인터를 바라보게 만든다.
    • 시스템 개편으로 재색인이 필요하면 백그라운드에서 완전히 새로운 mail_search_v2 인덱스를 몰래 빌드해 둔 뒤, 완성이 끝나는 순간 포인터의 방향만 스위칭 해주는 방식으로 무중단 마이그레이션을 달성한다.

8.3.3. 주 저장소(NoSQL)과 Elasticsearch 간의 데이터 정합성 동기화

일래스틱서치는 2026.07 기준으로 가장 널리 사용되는 검색 엔진 데이터베이스이며, 이메일 검색에 필요한 텍스트 기반 검색을 잘 지원한다.

그러나 주 이메일 저장소와 동기화를 실시간으로 맞추는 작업은 매우 까다로운 작업이다.


💡주 이메일 저장소와 Elasticsearch 간의 실시간 동기화 맞추기

애플리케이션 레이어에서 메인 DB와 Elasticsearch에 데이터를 동시에 쓰는 ‘이중 쓰기(Dual-Write)’ 방식은 한쪽 네트워크가 다운되었을 때 정합성이 깨지므로 절대 금물이다.
실무에서는 로그 기반의 CDC(Change Data Capture) 아키텍처를 정석으로 삼는다.

  • Cassandra나 MySQL 같은 메인 저장소의 커밋 로그(Commit Log/WAL)를 실시간으로 추적하는 CDC 에이전트(예: Debezium)을 심어준다.
  • 메인 DB에 쓰기가 성공하는 순간, CDC 에이전트가 로컬 디스크 로그에서 변경 분을 가져와서 카프카에 쓴다.
  • 인덱스 워커가 이 로그를 기반으로 Elasticsearch에 반영한다.
    • 만약 Elasticsearch가 일시적으로 다운되더라도 카프카에 데이터가 남아있으므로 복구되는 순간 재처리가 가능하여 최종적 일관성(Eventual Consistency)를 완벽히 보장할 수 있다.

💡역인덱스(Inverted Index)의 기본 구조

검색 엔진은 메일 본문 텍스트를 Tokenizer를 통해 단어 단위로 쪼갠 후, 각 단어가 ‘어느 사용자의 몇 번 메일(message_id)에 위치하는지 역으로 지도를 그려 저장한다.

추출된 단어 (Term)해당 단어가 포함된 이메일 식별자 목록 (Posting List)
아키텍처msg_101, msg_204, msg_509
도서msg_101, msg_302
디자인msg_204, msg_401, msg_509

사용자가 ‘아키텍처 디자인’ 이라고 검색하면, 시스템은 메일 전수를 조회하는 것이 아니라 위 테이블에서 두 단어가 동시에 교집합으로 걸린 msg_204msg_509만 조회하여 반환하므로 탐색 속도가 \(O(1)\) 에 수렴하게 된다.


8.4. 맞춤형 검색 엔진과 디스크 I/O 병목 해결

대규모 이메일 서비스 사업자는 보통 자기 제품의 고유한 요구사항을 만족시키기 위해 검색 엔진을 자체적으로 개발하여 사용한다.
자체 솔루션을 구현할 때 마주하게 되는 치명적인 장벽은 바로 디스크 I/O 병목 문제 이다.


8.4.1. LSM 트리 도입

1.3. 개략적인 규모 추정에서 보았듯이 메일 저장소에 추가되는 메타데이터와 첨부 파일의 양은 페타바이트 수준이다.
인덱싱 프로세스는 다량의 쓰기 연산을 발생시킬 수 밖에 없으므로, LSM(Log-Structured Merge) 트리를 사용하여 디스크에 저장하는 색인을 구조화하는 것이 바람직하다.

LSM 트리

LSM 트리는 빅테이블이나 Cassandra, RocksDB 같은 대용량 데이터베이스의 핵심 자료 구조이다.
새로운 이메일이 도착하면 우선 메모리 캐시로 구현되는 0번 계층(MemTable)에 추가된다.
메모리에 보관된 데이터의 양이 사전에 정의된 임계치를 넘으면 데이터는 디스크의 다음 계층(SSTable)에 순차적으로 병합(Compaction)된다.

전통적인 RDBMS(B+ Tree 구조)는 데이터가 들어오면 디스크의 이리저리 흩어진 주소를 찾아가서 수정하는 무작위 쓰기(Random Write)를 수행한다.
이는 디스크 헤드가 물리적으로 움직여야 하거나(HDD), SSD의 플래시 메모리 페이지를 지우고 다시 써야 하므로 극심한 I/O 병목을 유발한다.

반면, LSM 트리의 순차적 쓰기(Sequential Write)는 디스크 주소를 찾아 헤매지 않는다.
들어오는 모든 새로운 데이터와 수정 정보를 메모리(MemTable)에 모았다가, 디스크에 내릴 때는 기존 파일 뒤에 연속되도록 통째로 이어붙이는 Append-Only 방식을 사용한다.
디스크 포인터가 일직선으로 쭉 나아가며 쓰기 때문에, 무작위 쓰기에 비해 물리적 연산 속도가 수백 배 이상 빠르며, 페타바이트급 쓰기 트래픽을 중단 없이 받아낼 수 있다.


8.4.2. 변하지 않는 이메일과 자주 변하는 폴더 정보의 분리 메커니즘

LSM을 사용하는 또 다른 중요한 이유는 자주 바뀌는 데이터를 그렇지 않은 데이터와 분리하기 위해서이다.
예를 들어 이메일 본문 데이터는 한 번 수신되면 절대 바뀌지 않지만, 이메일 폴더 정보나 읽음 상태(Label)는 상이한 필터링 규칙과 유저의 행동 때문에 굉장히 자주 바뀐다.

따라서 데이터를 두 개의 파트로 나누고, 어떤 요청이 폴더 변경에 관한 것이면 폴더 정보만 바꾸고 이메일 데이터는 내버려 두는 전략이 필요하다.


💡LSM 트리 구조 안에서 데이터를 어떻게 두 개의 파트로 쪼개는 걸까?

메일 텍스트 전체를 들고 있는 ‘불변의 본문 인덱스’와, 폴더 ID 및 상태값만 관리하는 ‘가변의 메타데이터 인덱스’를 물리적으로 분리하여 LSM 테이블을 이원화한다.

  • 사용자가 이메일을 ‘받은 편지함’에서 ‘보관함’으로 이동시키면, GB 단위의 무거운 본문 역인덱스를 다시 생성하는 것이 아니다.
  • 본문 테이블은 그대로 두고, 가벼운 폴더 상태 테이블에 [Message_A] Folder: Archive 라는 변경 이력 사본을 LSM 트리의 Sequential Write 방식으로 저장한다.
  • 이후 검색 엔진이 질의를 할 때는 두 파트의 결과를 레이어 스택처럼 Merge해서 최종 상태를 유저에게 보여주기 때문에 본문 재색인으로 인한 디스크 I/O 폭발을 막을 수 있다.

8.5. Elasticsearch vs 맞춤형 검색 엔진

두 가지 설계 방안은 시스템의 스케일과 엔지니어링 리소스에 따라 명확한 트레이드오프 관계를 가진다.

비교 항목일래스틱서치 (Elasticsearch)맞춤형 검색 엔진 (Custom Engine)
규모 확장성어느 정도까지는 무난히 확장 가능하나 극단적 대용량 시 클러스터 관리 한계 직면이메일 특유의 읽기/쓰기 사용 패턴에 맞춤형 최적화(LSM 튜닝 등)가 가능하므로 극단적 확장 용이
시스템 복잡도두 가지 상이한 대형 시스템(메인 NoSQL 저장소 + 일래스틱서치 클러스터)을 동시에 운영해야 함메인 저장소 아키텍처와 통합된 하나의 거대한 인프라 시스템으로 관리 가능
데이터 일관성메타데이터 저장소와 일래스틱서치 내에 동일 데이터의 사본이 각각 존재하므로 동기화 및 일관성 유지가 매우 까다로움메타데이터 저장소 체계 내에 단 하나의 정합성 사본만 유지되도록 설계 가능
데이터 손실 가능성없음. 색인이 깨지거나 손상되더라도 주 저장소(NoSQL)의 원본 데이터를 역추적해 100% 복구 가능없음.
개발 및 유지비용오픈소스를 활용하므로 초기 통합은 쉬우나, 10억 유저 스케일 시 ES 내부 인프라만 전담하는 전문 엔지니어링 팀이 필수적임개발 난이도가 상상을 초과하며, 프로덕션 레벨의 커스텀 엔진 구축을 위해 엄청난 수의 엔지니어링 리소스 투입 필요

소규모 및 대다수의 이메일 시스템은 Elasticsearch가 좋은 선택지이다. 시스템을 빠르게 통합할 수 있고 인프라 초기 엔지니어링 비용을 획기적으로 아낄 수 있다.

Gmail 규모의 글로벌 초대형 시스템은 독립적인 외부 검색 클러스터를 두기보다는, 디스크 I/O 효율을 극대화하기 위해 앞서 설명한 데이터베이스 엔진 내부에 내장된 전용 맞춤형 검색 솔루션이나 통합 스토리지 인프라를 직접 설계하여 사용하는 것이 장기적인 인프라 비용과 정합성 측면에서 바람직하다.


8.6. 요약

10억 사용자를 위한 이메일 검색 엔진 설계의 본질은 극단적인 쓰기 중심 환경에서 어떻게 디스크 I/O 병목을 지우고 테넌트 간 격리를 달성하느냐에 있다.

초기 빌드와 빠른 기능 제공이 목표라면 Elasticsearch에 routing=user_id 설정을 얹고 카프카 CDC 파이프라인으로 메인 NoSQL과 동기화하는 구조가 바람직하다.
그러나 데이터가 수백 페타바이트를 넘어서는 스케일로 전진할수록, LSM 트리의 순차적 쓰기 메커니즘을 응용하여 불변의 본문 데이터와 가변의 상태 데이터를 분리 제어하는 맞춤형 내장 검색 아키텍처로 전환하는 것이 시스템의 생존을 보존하는 궁극의 전략이다.


9. 가용성 향상

대규모 이메일 시스템의 고가용성이란 특정 데이터 센터의 마비, 물리 노드의 서버 다운 등의 인프라 장애 상황에서도 유저의 사서함 접근성과 메일 송수신 기능을 무중단으로 유지하는 것이다.
데이터 정합성을 양보할 수 없는 이메일의 특성 상, 강한 일관성을 유지하면서도 신속한 장애 복구(Failover)를 달성하는 결함 방지 및 복제 아키텍처가 핵심이다.

여러 데이터센터를 통한 시스템 다중화

데이터를 여러 노드와 데이터 센터가 복제하는 것은 가용성의 기본이지만, 이메일 시스템은 단순 소셜 미디어 피드와 달리 궁극적 일관성을 적용할 수 없는 영역이다.


9.1. 단일 마스터(Single-Primary) 복제 모델을 채택하는 이유

이메일 사서함은 유저가 메일을 읽음 처리하거나, 폴더를 이동하는 등의 행위가 IMAP 프로토콜의 상태 플래그 및 폴더 구조와 밀접하게 맞물려 돌아간다.

  • 다중 마스터(Multi-Primary)의 위험성
    • 만일 유저가 사서함 쓰기 요청을 여러 노드에서 동시에 받아들이면, 네트워크 분할 시 동일 메일에 대해 서로 다른 수정 사항이 발생하는 Merge Conflict나 Split-Brain 현상이 발생하는데 이를 복구하는 비용은 상상을 초월한다.
  • 단일 마스터 모델의 해법
    • 따라서 특정 유저의 메일박스는 분산 클러스터 내에서 오직 단 하나의 주 노드(Primary Node)만 쓰기 연산을 전담하도록 강제한다.
    • 나머지 노드들은 Replica로서 주 노드의 변경 이력(Write-Ahead Log)을 실시간으로 동기화하는 역할만 수행한다.

9.2. CAP 정리 관점에서의 트레이드오프

CAP 정리(Consistency, Availability, Partition Tolerance)에서, 본 이메일 시스템 아키텍처는 네트워크 분할(P) 상황이 발생했을 때 가용성(A)보다 일관성(C)를 우선하는 선택을 한다.

[네트워크 분할 장애 발생]
                             │
            ┌────────────────┴────────────────┐
            ▼                                 ▼
   [일관성(C) 선택 - 본 시스템]        [가용성(A) 선택 - 일반 SNS]
   - 문제 노드의 쓰기 일시 차단        - 우선 양쪽 노드 다 쓰기 허용
   - 정합성 오염 원천 차단            - 나중에 영구적인 데이터 유실/
   - 신속한 Failover로 가용성 복구      충돌 발생 (메일 유실 위험)

주 노드가 다운되었을 때 대기 노드로 마스터 권한을 넘기는 순간(Failover Phase)에는 해당 유저의 사서함 쓰기가 아주 잠깐 제한될 수 있다.
하지만 이는 이메일이 중복 발송되거나 읽음 상태가 꼬이는 비즈니스적 오류를 막기 위한 필수적인 아키텍처적 트레이드오프이다.


💡Split-Brain 이란?

분산 시스템 내에서 네크워크 단절(Partition)로 인해 클러스터가 두 개 이상의 독립된 그룹으로 쪼개졌을 때, 각 그룹이 서로의 생존을 확인하지 못해 스스로를 ‘유일한 리더(Primary)’로 오인하여 동시에 쓰기 연산을 수행하는 장애 상태를 의미한다.

[정상 상태] Node A (Primary) <─── 정상 네트워크 연결 ───> Node B (Replica)

[장애 발생 - 네트워크 단절!]
[좌측 그룹] Node A : "B 노드와 연락이 안 되네? 내가 계속 Primary 역할을 해야지." -> 쓰기 계속 허용
             (뚝!) ⚡ 네트워크 분할 (Network Partition)(뚝!)
[우측 그룹] Node B : "Primary였던 A 노드가 죽었나 봐! 내가 새로운 Primary가 되어야겠다!" -> 스스로 리더 승격 및 쓰기 허용

이메일 아키텍처에서 Split-Brain이 터지면 아래와 같은 일이 발생한다.

  • 메일 상태의 유령화
    • 유저가 Node A에 접근해서 ‘A 이메일 삭제’를 눌렀는데, 동시에 다른 메일 송신자가 보낸 메일은 Node B에 적재된다.
  • 네트워크 복구 시의 데이터 충돌
    • 끊어졌던 네트워크가 다시 연결되었을 때, 두 노드를 하나로 합쳐야 한다.
    • 하지만 한쪽은 메일이 삭제되었다고 하고, 다른 한쪽은 메일이 살아있다고 하기 때문에 무엇이 진실인지 알 수 없다.
  • 결과
    • 유저 입장에서는 분명히 지웠던 메일이 다시 살아나거나, 장애 시간 동안 수신된 중요 메일이 유실되는 최악의 사용자 경험을 겪게 된다.

이런 문제를 해결하기 위해 분산 시스템에서는 아래와 같은 방식을 사용한다.

  • 과반수 투표 체계(Quorum 기반 합의 알고리즘)
    • Raft 같은 분산 합의 알고리즘의 핵심 규칙은 “전체 노드의 과반수(\(N/2 + 1\)) 이상이 동의하고 살아있는 그룹만 리더를 선출하거나 쓰기를 처리할 수 있다”는 것이다.
    • 예를 들어 노드가 5대인 클러스터가 네트워크 단절로 2대와 3대로 쪼개지면, 2대짜리 그룹은 과반수(3대)를 채우지 못하므로 스스로 리더가 되는 것을 포기하고 쓰기는 차단한다.
    • 오직 3대까리 그룹만 정상 가동되므로 Split-Brain이 원천 봉쇄된다.
  • 코디네이션 서비스(ZooKeeper/etcd) 활용
    • 중앙 분산 Lock 시스템을 사용하여, 클러스터 전체에서 ‘Primary 리더’라는 타이틀을 단 한 개만 발행하고 관리하도록 통제한다.

10. 추가 고려사항

10.1. 결함 감지 및 무중단 장애 복구(Failover)

10억 유저 스케일의 인프라에서는 매일 수십, 수백 대의 서버가 노후화나 하드웨어 결함으로 다운된다.
중앙 집중식 하트비트 체크는 그 자체로 병목이 되므로, 대규모 시스템은 탈중앙화된 결함 감지 체계를 가진다.


10.1.1. Gossip Protocol을 통한 상태 관리

Gossip Protocol

분산 시스템에서 중앙 관리자없이, 각 노드가 무작위로 선택한 다른 노드들과 주기적으로 정보를 교환하며 클러스터 전체의 상태(노드 활성화 여부 등)를 동기화하는 탈중앙화 피어 투 피어 통신 규격

각 노드가 무작위로 다른 노드들과 주기적으로 메타데이터를 교환하는 고십 프로토콜을 활용한다.
특정 노드가 응답하지 않는다는 사실이 네트워크 전체로 소문 퍼지듯 빠르게 확산되며, 이를 통해 중앙 제어 장치 없이도 클러스터 전체가 노드의 생존 상태를 정확하게 인지하게 된다.

주 노드의 장애가 감지되는 순간부터 클라이언트가 연결을 회복할 때까지 분산 복구 수순을 다음과 같다.

[Step 1. 장애 감지] ➔ [Step 2. 리더 승격 수행] ➔ [Step 3. 라우팅 갱신] ➔ [Step 4. 커넥션 재연결]
  Gossip 프로토콜로       ZooKeeper/etcd가        라우팅 프록시가       웹소켓 클라이언트가
  Primary 다운 확인       Replica를 새 주노드로    라우팅 장부()를    새 주노드로 튜브 재연결
                          즉각 승격(Promote)       최신 상태로 업데이트   (유저는 장애 체감 못함)

10.2. 글로벌 인프라를 위한 보안, Compliance 및 데이터 격리

글로벌 서비스를 지향하는 메일 시스템은 단순히 기능의 확장성을 넘어, 국가별 법률적 규제인 컴플라이언스를 반드시 아키텍처에 녹여내야 한다.


10.2.1. GDPR 및 데이터 주권(Data Sovereignty) 대응

유럽 연합의 GDPR 등 현대 보안 Compliance의 핵심은 ‘유럽 시민의 개인 데이터는 물리적으로 유럽 영토 내에 위치한 데이터 센터에만 저장되어야 한다’는 데이터 주권이다.

  • 이를 해결하기 위해 분산 NoSQL 데이터 모델링 단계에서 유저의 국가 정보를 기반으로 물리적 클러스터를 논리적으로 완전히 분할(Geo-graphical Sharding)해야 한다.
  • 유럽 유저의 메일 데이터가 미국이나 아시아의 스토리지 노드로 절대 복제되지 않도록 Replication Placement Policy를 엄격히 통제한다.

합법적 감청(Legal Intercept)은 이 분야의 또 다른 대표적 특징이다.
설계 단계부터 감청 통로를 만들어, 법적 절차를 거친 국가 기관이 수사를 위해 통신 내용을 중간에서 열람할 수 있도록, 통신 시스템 자체에 합법적인 데이터 추출 통로를 마련해 두는 기술적·법적 체계이다. 당연히 감청이 실행되는 동안에도 사용자는 이 사실을 전혀 몰라야 하고, 권한이 없는 일반 직원이 오용하지 못하도록 극도로 엄격한 보안 및 로그 시스템이 적용되어야 한다.


10.2.2. 이중 암호화 표준 도입

  • 전송 중 데이터 암호화(Data-in-Transit)
    • 외부 SMTP 통신 및 클라이언트와 API 서버 간의 모든 통신은 TLS 1.3 프로토콜을 강제하여 중간자 공격(MITM, Man-in-the-Middle)을 원천 차단한다.
  • 저장 중 데이터 암호화(Data-at-Rest)
    • 페타바이트급 데이터 레이어에 저장되는 메일 본문과 첨부 파일은 AES-256 알고리즘으로 암호화된다.
    • 키 관리 서비스(KMS)를 연동하여 유저별 마스터 키를 기반으로 데이터 복호화 키를 따로 생성하는 Envelope Encryption 메커니즘을 적용한다.

10.3. 모니터링 및 메트릭 핵심 지표

시스템의 가동 상태를 선제적으로 모니터링하지 않으면 대규모 장애의 파고를 막을 수 없다.
인프라의 가동률 외의 메일 시스템만의 특화 지표를 관리해야 한다.


10.3.1. 종단 간 지연(E2E Delivery Latency)과 Backpressure 관측

메일이 인바운드 SMTP 서버에 도착한 시점부터 유저의 사서함 NoSQL에 적재되고 푸시 알림이 가기까지의 종단 간 지연 시간을 추적한다.
만약 중간 메시지 큐(카프카)의 Consumer Group에서 컨슈밍 지연(Lag)이 발생하면 Backpressure 점검 경보를 띄우고 워커 노드를 Auto-Scaling 해야 한다.

IP 평판 하락과 메일 유실을 실시간으로 방어하기 위해 전송 가능성과 직결되는 수신측 ISP들의 거부 반응을 실시간 모니터링 대시보드(Prometheus/Grafana)의 최우선 알람 지표로 설정해야 한다.

  • 하드 바운스 비율(Hard Bounce Rate)
    • 영구적인 수신 실패 오류(5xx 계열 코드) 비율이다.
    • 이 지표가 1%를 초과하면 스팸 발송자로 간주되어 전체 IP 대역이 블록당할 수 있으므로, 감지 즉시 해당 발송 유저를 격리 조치하는 자동 차단 스크립트가 돌아야 한다.
  • 스팸 신고율
    • 유저들이 메일을 받고 ‘스팸 신고’를 누른 비율이다.
    • 이 지표를 0.1% 미만으로 유지하는 것이 철칙이다.
    • 조금이라도 튀는 징후가 보이면 해당 발송 세그먼트의 Throttling Rate를 즉각 조사해야 한다.
  • RBL(Real-time Blackhole List) 등재 여부
    • 유명 스팸 블랙리스트 기관(Spamhaus 등)에 우리 시스템의 발송 전용 IP 대역이 포함되었는지 실시간 파싱 봇을 통해 주기적으로 감시한다.

11. 요약

  • 프로토콜 & 전송 가능성(Deliverability) 레이어
    • 외부와의 통신은 SMTP로 표준을 준수하되, SPF/DKIM/DMARC 인증과 트랜잭션/마케팅 IP 범주화 격리 및 철저한 4주 간의 IP 웜업을 적용하여 스팸 메일함으로 빠지는 병목 제거
  • 스토리지 레이어
    • 구조화된 메일 데이터는 카산드라(NoSQL)의 LSM 트리 순차 쓰기 특성을 활용하여 초고속 쓰기 성능을 확보하고, 대용량 첨부 파일은 저비용의 Object Storage로 분리하여 비용과 성능 최적화
  • 검색 레이어
    • 극단적인 쓰기 중심 검색 요구사항을 풀기 위해, 메인 DB의 자원을 갉아먹는 내장 쿼리 대신 카프카 CDC 기반의 비동기 파이프라인과 routing=user_id 를 적용한 독립 Elasticsearch 클러스터를 연동하여 준실시간성과 테넌트 격리 달성
  • 가용성 & 보안 운영
    • 사서함의 완벽한 정합성을 위해 단일 마스터 복제 모델과 CAP 정리의 일관성(C) 우선 정책을 고수하며, Gossip Protocol 기반의 무중단 Failover 체계와 GDPR 맞춤형 지리적 파티셔닝을 더해 거대한 글로벌 메일 서비스 지탱

마인드 맵


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 알렉스 쉬, 산 람 저자의 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2를 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10