우리 팀은 여러가지 시스템 중에, 또한 가장 중요한 시스템인 '메일 시스템'을 담당하고 있다.

현재 메일 시스템은 도미노(Domino) 기반으로 구축되어 있고, 어쩌다보니(?) 내가 메일 시스템을 담당하고 있다.


메일 시스템이 이미 구축되어 있기 때문에 대부분의 업무는 운영 중심의 업무다.

예를 들면 메일 용량 증설, 메일 회수, 메일 박스 관리 등의 루틴 업무가 있다.


하지만 "내가 맡은 업무는 깊게 알아야한다"라는 나의 신념때문에 이번 기회에 도미노 + 메일 시스템을 한번 제대로 뜯어보기로 했다.

메일 시스템

처음에는 '메일이 뭐가 복잡하겠어?'라고 생각했다.

하지만 메일 시스템은 생각보다 훨씬 깊고 복잡한 세계였다. 그리고 특히 우리 회사에서 사용하는 도미노 기반 메일 시스템은 평범하지 않았다!


메일은 우리가 매일 쓰는 필수 기능이지만, 정작 그 메일이 어떻게 전달되고 어떤 시스템이 밑에서 움직이는지 알고 있는 사람은 드물다.

(나조차도 당연히 업무를 담당하기 전에는 몰랐다)


본격적인 메일 시스템 구조를 살펴보기전에 도미노를 간단하게 설명하자면.

HCL Domino(도미노)는 메일, 캘린더, 결재, 워크플로우 등 사내 시스템 전반을 제공하는 슈퍼(?) 앱이다.

클라이언트 프로그램은 Notes(노츠), 서버는 Domino(도미노), 데이터베이스는 nsf로 구성되어있다.



회사에서 사용하고 있는 노츠

도미노 메일 시스템은 5개의 핵심 컴포넌트로 구성되어있다.

  • mail.box : 메일을 쌓아두는 대기장소로, SMTP로 나가거나 nsf로 들어가기전에 거치는 버퍼

  • router : 메일을 실제로 처리하는 백엔드 서비스

  • names.nsf : 전사 주소록 + 라우팅 정보 저장소

  • mail/[user].nsf : 임직원 개별 메일 DB

  • SMTP Task / smtp.box : 외부 메일 전송 및 수신 역할

공식문서를 참조하면,

메일 서버가 메일 클라이언트, 다른 Domino® 서버의 라우터 또는 애플리케이션에서 메시지를 수신하면,  Domino® 서버의 메일 라우팅이 시작됩니다. 메시지는 서버의 특수 Notes® 데이터베이스인 MAIL.BOX로 전송됩니다. 서버는 모든 수신 및 발송 메일을 MAIL.BOX 데이터베이스에 임시로 저장합니다.

이 모든 구조의 중심에는 router가 있다.

router는 도미노 메일 시스템의 백본으로, 모든 메일 흐름을 조율하는 컨트롤 타워다.


사용자가 노츠에서 메일을 작성하고 전송하면 아래의 흐름으로 진행된다.


(1) 메일 작성 → (2) mail.box 저장 → (3) router 처리 → (내부) mail/상대방.nsf 저장 | (외부) SMTP task 통해 외부 전송

SMTP

메일 전송의 전체 흐름을 추상화된 레벨로 살펴보면, 크게 3가지 핵심 컴포넌트로 구분할 수 있다.

  • User Agent(유저 에이전트)

  • Mail Server(메일 서버)

  • SMTP(Simple Mail Transfer Protocol)

image.png

STEP1. 팀장님에게 메일을 전송한다

유저 에이전트는 사용자가 메일을 읽고, 답장하고, 포워딩하고, 저장하고, 작성할 수 있게 해주는 인터페이스다. 예를 들면 Outlook, Apple Mail, Gmail, Notes가 유저 에이전트다.

내가 노츠에서 팀장님께 "휴가를 보내주세요"라는 메일을 작성하고 전송 버튼을 누르면, 이 메일은 나의 메일 서버로 전달되고 메일 서버의 발송 큐에 저장된다.

STEP2. 팀장님의 MailBox로 배송한다

이제 내 메일 서버는 메일 배송을 준비한다.

SMTP 프로토콜의 클라이언트 역할을 하며 팀장님의 메일 서버에게 TCP 연결을 시도한다.

연결이 성립되면 SMTP 클라이언트는 TCP 소켓을 통해 내가 작성한 메일을 팀장님의 메일 서버로 전송하고, 팀장님의 메일 서버는 이 메일을 받아서 팀장님의 메일 박스에 넣어둔다.

이제 팀장님은 노츠를 통해 내가 작성한 메일을 읽을 수 있다.


처음에는 메일 서버와 메일 박스가 비슷하게 느껴져셔 '똑같은 건가?'라고 생각했는데 메일 서버와 메일 박스는 비슷해보이지만 역할이 다르다.


메일 서버는 말 그래도 메일을 처리하고 저장하는 서버 시스템이다. 메일을 전송하고, 수신하고, 보관하고, 재전송을 시도하는 모든 메일 관련 기능을 '서버'단에서 담당한다.

메일 서버는 메일을 전송할 때는 SMTP 프로토콜의 클라이언트로 참여하고, 메일을 수신할 때는 SMTP 프로토콜의 서버로 참여한다.


메일 박스는 메일 서버안에 있는 사용자별 저장 공간이다.

예를 들어 내 메일 주소가 leo@sbs.co.kr이라면 이 주소에 해당하는 공간이 나의 메일 박스다.

내 메일 주소로 수신된 메일은 최종적으로 나의 메일 박스에 저장되고, 나는 유저 에이전트를 통해 메일 박스의 내용을 읽고, 답장할 수 있다.

STEP3. 팀장님이 메일을 확인한다

이제 팀장님은 메일 수신함을 확인하고 싶을 때, 언제든지 본인의 유저 에이전트를 통해 메일 서버에 접속해서 메일 박스에 있는 메일을 읽을 수 있다.


메일 전송 과정을 간략하게 요약한다면,

[나의 유저 에이전트] -> [나의 메일 서버] -> SMTP -> [팀장님의 메일 서버] -> [팀장님의 메일 박스]


그런데 만약 팀장님의 메일 서버가 죽어있다면?

내 메일 서버가 팀장님의 메일 서버와 SMTP 프로토콜로 통신을 하다가 '팀장님의 메일 서버 Down'을 파악하고 해당 메일을 임시로 큐에 보관한다. 그리고 30분 단위로 재시도를 한다.

몇번을 재시도해도 메일이 전송되지 않으면 나에게 메일 전송 실패 알림을 보낸다.


STEP 과정을 살펴보면 이 모든 과정을 밑에서 지탱하는 것은 SMTP 프로토콜이다.


SMTP(Simple Mail Transfer Protocol) 프로토콜은 Application Layer 프로토콜로 이름은 Simple 하지만 1982년부터 살아남은 전송 프로토콜이다.(HTTP보다 나이 많음)


위 예시에서 전송 흐름을 다시 짚어보면 아래와 같다.


1. 팀장님께 보낼 메일을 작성

→  leo@sbs.co.kr라는 주소로 메일 작성 완료


2. 노츠(유저 에이전트)가 작성 메일을 내 메일 서버로 보냄

→  메일 서버는 해당 메일을 큐에 넣어놓고 대기


3. SMTP 프로토콜 클라이언트 동작

→  SMTP 프로토콜 클라이언트는 내 메일 서버에 내장되어 있음(모든 메일서버에 내장됨) -> 이제 팀장님의 메일서버(somewhere-mailserver.sbs)에 TCP 연결


4. TCP 연결 후 SMTP 핸드쉐이크

→  "HELO leoMailSever.sbs" -> "250 Hello~"

→  "MAIL FROM", "RCPT TO", "DATA" 등 메시지 전송 절차 수행


5. 팀장님의 메일 서버는 메시지를 수신하고 팀장님의 메일 박스에 저장


SMTP 프로토콜로 직접 메일 서버로 메일을 보내보고 싶어서 Free SMTP Server를 이용해서 Telnet으로 직접 전송을 했다.

telnet smtp.freesmtpservers.com 25 // smtp.freesmtpservers.com 메일 서버로 telnet
Trying 104.237.130.88...
Connected to smtp.freesmtpservers.com.
Escape character is '^]'.
220 tools.wpoven.com Python SMTP 1.4.2

HELO somewhere.sbs.co.kr // telnet 접속 후 SMTP 핸드쉐이크
250 tools.wpoven.com // 250 return 확인

MAIL FROM: <leo@sbs.co.kr> // 메일 발신자 전송
250 OK // 250 return 확인

RCPT TO: <leader@freesmtpservers.com> // 메일 수신자 전송
250 OK // 250 return 확인

DATA // DATA 필드 보내겠다고 신호
354 End data with <CR><LF>.<CR><LF> // 354 return 확인

Subject: Vacation request// 메일 제목
From: leo@sbs.co.kr // 메일 발송자
To: leader@freesmtpservers.com // 메일 수신자

I want to go on vacation, sir. please... // 메일 본문
.
250 OK

QUIT
221 Bye

그런데 MAIL FROM과 DATA의 From이랑 뭐가 다른지, RCPT To와 DATA의 To는 뭐가 다른지, 이해하기 어려웠다.

SMTP 메시지는 RFC5322를 기준으로 하는 Mail Message Format을 기반으로 한다.


해당 포맷에 따르면 메일은 아래처럼 Header + Body 구조로 구성된다.

From: leo@sbs.co.kr
To: leader@freesmtpservers.com
Subject: 휴가좀 보내주세요

팀장님 휴가좀 보내주세요

From, To, Subject는 모두 Header.

Header와 Body는 빈 줄(CRLF) 하나로 구분된다.


중요한점은 SMTP 명령어의 <MAIL FROM>과 DATA의 <From>은 다르다는 점이다.

예를 들어, MAIL FROM은 발신자 주소를 전송 수준에서 지칭하는것이고 From은 메일에 들어가는 메타정보다.


복잡해보이지만 SMTP는 위와 같이 "텍스트" 기반으로 메시지를 주고 받는다.

그러므로 메일 제목과 본문을 직접 터미널에서 입력해도 메일이 보내진다.

실제 본인의 메일 서버나 회사의 메일 서버로 테스트를 하면 확인할 수 있다.


하지만 SMTP는 기본적으로 메시지의 본문을 7비트 ASCII로 제한한다. 그래서 이미지나 PDF 같은 첨부된 바이너리 파일은 그냥 전송할 수 없다.

그러므로 대부분 첨부파일들은 전송전에 base64와 같은 방식으로 인코딩을 한 후, 전송 후 받은 쪽에서 다시 디코딩을 해야한다.


그런데 SMTP는 일반적인 HTTP 프로토콜이랑 다른점이 있다. SMTP는 메일을 '보내는' 프로토콜이다.

정확히는 전송자의 메일 서버가 수신자의 메일서버로 메일을 Push 하는 프로토콜이다.


그럼 반대로 수신자는 어떻게 자신의 박스에서 메일을 가져올까? → 이 부분은 SMTP의 영역이 아니다.

SMTP는 보내는(Push)데까지가 역할이고, 받는(Pull)건 다른 프로토콜의 몫이다.


Push 프로토콜 방식은 데이터를 가진 쪽(발신 서버)이 먼저 움직여서, 수신자의 준비여부와 관계 없이 데이터를 전달하는 방식이다.

나의 메일서버는 팀장님의 메일 서버에 TCP 연결을 시도하고 → "팀장님 메일박스에 내 메일을 저장해라!"라고 말하며 메일을 보내버린다 → 팀장님이 준비가 되었든 아니든 메일은 이미 도착해서 메일 박스에 쌓인다.


Pull 프로토콜 방식은 클라이언트가 서버에게 "수신된 메일이 있나요?"라고 요청을 한 후 데이터를 가지고 온다.

팀장님의 유저 에이전트가 주기적으로 메일 서버에 접속해서 "새로운 메일이 있나요?"라고 물어보고, 새로운 메일이 있다면 메일을 가지고 온다.

이걸 가능하게 해주는 대표적인 프로토콜이 IMAP, POP3, HTTP(웹메일용)이다.


그렇다면 왜 SMTP는 Push 프로토콜인 반면, 클라이언트는 Pull 프로토콜을 사용할까?

그 이유는 클라이언트는 항상 켜져있지 않기 때문이다.


만약 팀장님의 PC가 꺼져 있다면 아무리 Push로 메일을 보내도, 팀장님은 받을수가 없다.

대신 항상 켜져있는 팀장님의 메일 서버가 대신 메일을 받고, 언젠가 PC가 켜지면 그때 Pull로 확인을 한다.


이처럼 메일 한 통이 도착하기까지 여러 프로토콜과 메일 서버, 메일 박스가 존재한다.

이제 메일 시스템은 나의 업무 중 하나가 아니라, 내가 가장 깊게 들여다본 시스템 중 하나가 되었다.