
노션에 정리해놓았다가 드디어 블로그로 옮기는 작업을 하고 있다.
이번에 쓰는 글을 우리 서비스의 핵심기능인 이메일 서버관련 글이다.
먼저 우리 서비스가 모르시는 분이 있을 수도 있는데 이 서비스를 만든 가장 큰 이유 중 하나
즉 페인포인트는 여러 메일이 섞인다는 것이다.
먼저 예시를 보자

위 이미지는 제 받은메일함입니다.
메일 종류를 보면 결제 내역, 뉴스레터, facebook, github 등등 뒤죽박죽 섞여있습니다.
이를 정리하기 위해 메일함을 만들어서 분류하기도 귀찮습니다.
그렇다고 뉴스레터 전용 이메일을 만든다? 뉴스레터 이메일 갔다가 다시 원래 이메일 갔다가 하기가 번거롭다.
여기

현재 서비스를 보면 이렇게 이메일이 도착하면 오늘의 뉴스레터 페이지에서 볼 수 있다.
이 화면을 보여주면 크루들은 이렇게 말한다
"이거 어디서 긁어오는거야?"
"크롤링 해도 괜찮아??"
그럼 이렇게 대답한다.
"이거 이메일 서버를 통해서 직접 이메일 받을 거야!!"
그렇다면 이제부터 이메일서버가 왜 필요했는지? 어떻게 구축했는지에 대해서 알아보자 ㅎㅎ

뉴스레터는 크롤링 같은걸 해오는게 아니다 이것도 하나의 저작권으로 인정되기 때문에 우리가 함부로 가져올 수가 없다.

우리 서비스는 그대로 보여주기 때문에 공정사용에 해당하지 않는다.
따라서 누구나와서 볼 수 있게 하면 불법이 된다.
오직 뉴스레터를 신청한 당사자만 메일을 통해서 볼 수 있다.
그렇기에 우리서비스는 로그인이 필수이고 이메일을 받을 수 있어야 했다.
기존의 이메일은 이렇게 더럽다.
이게 너무 불편했다. 또 가끔은 이렇게 스팸메일함에 들어가있다.

지메일 같은 경우는 아래의 설정을 해줘 스팸으로 오지 않는다

그래서 뉴스레터만 받을 수 있는 앱이 있으면 어떨까 생각이 들었다.
이렇게 하려면 이메일을 받아야했다.
첫 번째 시도: Gmail API로 뉴스레터 긁어오기
이전에 했던 작은 사이드 프로젝트에서는 인프라 구축에 익숙하지 않았고 여러가지 방법도 잘 찾아보지 않았다.
그 당시 조사했던 방법은 딱 두 가지였다. Gmail API를 통해 활용하는 것 이메일 서버를 구축하는 것
이 당시 인프라 구축에 익숙하지 않았고 비용을 최대한 아끼기 위해 돈의 거의 들지 않는 Gmail API로 부턱대고 시작했다.
Google Gmail API를 이용해서 다음과 같은 구조로 만들었다.
1. 유저가 OAuth로 우리 서비스에 Gmail 접근 권한을 준다.
2. 우리가 유저의 Gmail에 들어가서 뉴스레터 메일만 골라 읽고
4. 그 내용을 파싱해서 DB에 저장한다.
처음에는 꽤 그럴듯해 보였다.
사용자는 기존 이메일 주소를 그대로 쓰면 되고 우리는 권한만 받아서 필요한 뉴스레터만 읽으면 되니 별도 메일 서버를 운영하지 않다도 됐다.
실제로도 잘 동작했다.
하지만 엄청난 문제가 있었다.
신규회원가입을 하려면 화면이 아래와 같이 뜬다.

원인은 바로 CASA다
Gmail 본문을 읽으려면 넘어야 하는 벽, CASA
구글은 요즘, Gmail 같은 민감한 사용자 데이터에 접근하는 앱에 대해
보안 검증을 굉장히 강하게 요구하고 있다.
그 중심에 있는 제도가 바로 CASA (Cloud Application Security Assessment)다.
요약하면 이런 제도다.
“너희 앱이 사용자 데이터를 안전하게 다루는지,
제3의 보안 기관이 공식적으로 확인해주는 인증 제도”
특히 Gmail 본문을 읽는 권한 같은 걸 쓰려면,
그냥 “우리가 잘 할게요” 수준으로는 안 되고,
Tier 2 이상의 CASA 인증을 요구한다.
CASA Tier 간단 정리
- Tier 1 – 개발자가 체크리스트를 보고 셀프 점검하는 단계 (무료)
- Tier 2 – 공인 보안 실험실에서 코드/구성 자동 분석 + 검증
- Tier 3 – 인프라, 네트워크, 데이터 저장 방식까지 포함한 풀 보안 감사
문제는 비용이다.
- Tier 1: 무료
- Tier 2: 수백만원
- Tier 3: 수천만원
즉, 작은 팀이나 사이드 프로젝트 입장에서
“Gmail API로 메일 본문을 읽는 서비스”를 만들려면
CASA Tier 2 비용 때문에 사실상 불가능에 가깝다
라는 결론이 나온다.
그 당시에는 자체 CASA Tier 2를 받을 수 있었다.
레퍼런스도 너무 부족해서 어디서부터 시작해야할지 감잡기도 어려웠고 링크드인에 어떤 현업자분이 인증받았다해서 연락을 드렸지만 회신이없었고 우리나름대로 여러가지 시도를 해보았지만 실패했었다.
심지어 지금은 자체 스캔 프로세스는 지원하지않고 무조건 인증기관에 돈을 주고 받게 되어잇다.

우리도 결국 이 문제 때문에, 정식 출시를 하지 못햇다.
프로젝트 기간도 거의 끝나갔고 아쉬움에 남았었다.
(혹시 지금 CASA Tier 2 인증을 받고 싶다면 여기를 참고하면 좋을 것 같다)
같은 실수를 반복하지 않기 위해: 이메일 수신 옵션들 비교
그렇게 우테코 팀프로젝트를 시작하고 뾰족한 대안이 없을 때 뉴스레터 관련 아이템을 제시했고 이전에 수많은 자료조사와 실패한 경을 토대로 팀원들을 설득했고 결국 이 프로젝트를 진행하게되었다.
이 아이템의 핵심은 이메일 도착이다.
찾아본 방법은 크게 네 가지 정도다.
1. AWS SES로 메일로 수신
2. SendGrid / Mailgun inbound API
3. Cloudflare Email Routing
4. 수신 전용 이메일 서버 직접 구축
1) AWS SES(Simple Email Service)로 이메일 받기
가장 먼저 검토한 선택지는 AWS SES로 메일을 받는 방식이다.
news@mydomain.com 같은 주소로 온 메일을 SES가 대신 수신하고, 그 메일을 S3에 저장하거나 Lambda를 실행하거나 SNS로 이벤트를 발행하는 식으로 다른 AWS 리소스로 넘겨주는 구조다.
이미 AWS를 쓰고 있다면, 그림 자체는 꽤 깔끔하다.
- 직접 메일 서버를 운영할 필요가 없다
- Postfix, Dovecot 같은 걸 설치하고 운영/백업/보안 패치를 신경 쓸 필요가 없다.
- AWS가 기본적인 스팸 필터링을 어느 정도 해준다
- 우리가 여러 이메일 관련 설정과 스팸 필터를 처음부터 다 짜는 것보다 부담이 적다.
- Lambda와 바로 붙일 수 있다
- “메일 수신 → Lambda 트리거 → 본문 파싱 후 DB 저장” 같은 파이프라인을 서버 없이 구성할 수 있어서,
- 초기에 빠르게 프로토타입을 만들기에는 꽤 매력적인 옵션이다.
초기 작은 서비스이거나,
“메일 수신량이 많지 않고, AWS 안에서만 빠르게 파이프라인을 구성하고 싶다”면
실제로 도입을 충분히 고려해볼 만한 구조다.
하지만 우리 서비스 기준으로는, 치명적인 단점이 몇 가지 있었다.
첫 번째는 경제성 문제다.
SES는 발송뿐만 아니라 수신도 건당 과금이 붙는다.

보수적으로 계산을 해보았다.
평균 1명당 4개의 뉴스레터를 구독하고 그 뉴스레터는 매일 도착한다고 해보자
그럼 회원수가 500명일 때 하루에 2000개의 메일을 수신해야한다.
여기에 청크 비용도 계산을 해야한다.
도착하는 이메일은 뉴스레터고 거의 대부분이 HTML이다
HTML 평균 크기는 11.5KB로 계산했다. (8,000 ~ 15,000자 -> 평균 11,500자)
메일 크기 11.5KB 기준 청크 비용 계산
✔ 청크 단위 = 256KB
→ 11.5KB / 256KB = 0.04492 청크
✔ 청크 비용
0.04492 × 0.00009 USD = 0.000004042 USD / 건
✔ 메시지 비용
0.0001 USD / 건
최종 1건당 비용 : 0.0001 + 0.000004042 = 0.000104042 USD / 건
최종 비용 예상
| 회원 수 | 하루 수신량 | 하루 비용(USD) | 월 비용(USD) |
| 500명 | 2,000건 | 0.208 USD | 6.24 USD |
| 1,000명 | 4,000건 | 0.416 USD | 12.48 USD |
| 2,500명 | 10,000건 | 1.040 USD | 31.20 USD |
| 5,000명 | 20,000건 | 2.080 USD | 62.40 USD |
회원 수가 증가할 수록 비용이 선형 증가하게된다.
중요한건 여기에 스팸처리 비용은 계산하지 않았다.
스팸 메일도 똑같이 비용이 든다.
뉴스레터 서비스 특성상,
- 사용자 수가 늘어나고
- 구독하는 뉴스레터 종류가 많아질수록
- 하루에 들어오는 메일 수도 기하급수적으로 증가한다.
초기에야 “얼마 안 나오네?” 싶다가도,
규모가 커졌을 때 고정 비용이 아닌 ‘트래픽 기반 비용’으로 계속 빠져나간다는 점이 큰 부담이었다.
두 번째는 커스터마이징의 제약이다.
SES를 쓰는 순간, 메일 수신 이후의 파이프라인이 AWS 리소스 중심으로 고정되는 느낌이 있다.
- Maildir 같은 전통적인 디렉터리 구조를 그대로 쓰기도 어렵고
- 원본 .eml 파일을 어떻게 보관할지, 장기 보관 정책을 어떻게 가져갈지 등에서
SES 설계에 맞춰서 생각해야 한다. - 인프라를 완전히 AWS 밖으로 빼거나,
자체 메일 서버로 구조를 전환하고 싶어지는 시점이 왔을 때 마이그레이션 비용도 고려해야 한다.
우리 서비스처럼
- 장기적으로 메일을 아주 많이 받게 될 가능성이 높고
- 원본 메일을 그대로 쌓아두고,
- 파이프라인과 저장 구조를 우리가 원하는 대로 세밀하게 설계하고 싶다
라는 요구사항을 생각했을 때,
초기에는 편하지만, 규모가 커질수록 비용과 제약이 서서히 발목을 잡을 수 있는 선택지 라는 결론을 내렸다.
그래서 실제 도입 후보에서 완전히 제외한 것은 아니지만,
프로토타입/소규모 서비스에는 좋지만,
장기적으로 우리가 가고 싶은 아키텍처와는 방향이 다르다고 판단했다.
2) SendGrid/Mailgun 수신 API
한 마디로 하면 “메일을 직접 ‘받아’ API로 전달해주는 서비스”
SES 다음으로 눈이 간 건 “메일 수신을 SaaS에 맡기고, 우리는 HTTP로만 받자”는 접근이었다.
메일 프로토콜(MX/SMTP) 세계를 직접 다루지 않아도 되고, 우리 서버는 웹훅 요청 하나만 처리하면 되니까 초반 생산성이 압도적으로 좋아 보였다.
동작 방식
- 발신자 → (MX) → SendGrid/Mailgun 수신 → 메일 파싱 → 우리 Webhook 엔드포인트로 POST → 저장/파이프라인 실행
- SendGrid는 Inbound Email Parse Webhook 형태로 제공한다.
또 하나 마음에 들었던 포인트는, Webhook이 실패해도 재시도 메커니즘이 있다는 점이었다. 예를 들어 SendGrid는 우리 엔드포인트가 5xx로 응답하면 큐잉 후 재시도하고, 2xx를 받으면 처리를 끝낸다(최대 3일 재시도 후 드랍)
우리 기준에서 걸렸던 지점들
1) 비용이 고정 + 제한 형태로 다가온다
SES가 “쓴 만큼”에 가깝다면, 이쪽은 월 플랜(포함량 + 초과 과금) 성격이 강했다.
우리 서비스 가정(1인당 4개/일, 30일 기준)으로 수신량만 단순 환산하면 대략 아래 정도다.
| 회원 수 | 월 수신량(건) | Mailgun 추정 월 비용(USD) | SendGrid 추정 월 비용(USD) |
| 500 | 60,000 | 약 48 (Foundation 50k + 초과 10k) | 약 33.25 (Essentials 50k + 초과 10k) |
| 1,000 | 120,000 | 약 112 (Scale 100k + 초과 20k) | 약 52.95 (Essentials 100k + 초과 20k) |
| 2,500 | 300,000 | 약 310 (Scale 100k + 초과 200k) | 약 214.95 (Essentials 100k + 초과 200k) |
| 5,000 | 600,000 | 약 640 (Scale 100k + 초과 500k) | 약 484.95 (Essentials 100k + 초과 500k) |
Mailgun은 무료 구간이 작고(하루에 100통), 유료는 월 구독(Basic이 월 15$)이 붙고, SendGrid도 월 구독(월 19.95$)이 시작점이라 장기적으로 수신량이 늘어나는 서비스에선 부담이 될 수 있다.
핵심은 수신량 증가가 곧 비용 증가로 직결되고, 스팸/오발송처럼 “원치 않는 메일”도 같은 방식으로 비용에 들어온다는 점이었다.
2) 원문(.eml) 보관이 ‘공짜’가 아니다
Maildir처럼 “메일이 오면 원문이 파일로 남는” 흐름이 아니라, Webhook으로 받은 결과를 기반으로
원문(raw MIME)을 그대로 저장할지 어디에,
얼마나 오래 보관할지(S3/DB/파일)
장애/버그 시 재처리(리플레이) 를 어떻게 할지 를 별도로 설계해야 한다.
즉 “수신”은 SaaS가 해주지만, “아카이빙”은 결국 우리가 책임져야 했다.
초기 구현 속도는 최고였다. 하지만 우리 서비스는 이메일 수신이 핵심 도메인이고, 1~2달 하고 끝낼 프로젝트도 아니다. 그래서 월 플랜 기반 비용 구조 + 원문 보관 설계 부담까지 합쳐 보면, 장기적으로 우리가 원하는 아키텍처 방향과는 거리가 있다고 판단했다.
3) 직접 이메일 서버 운영하기
마지막 선택지는, 우리가 직접 이메일 수신 전용 서버를 운영하는 방식이었다.
즉, SendGrid/Mailgun 같은 외부 Inbound 서비스에 의존하지 않고 SMTP(Postfix)로 직접 수신하고, Maildir에 원문을 저장한 뒤 내부 파이프라인(파서/저장/후처리)로 넘기는 구조다.
결론부터 말하면 우리는 이 방식을 선택했다.
왜 직접 운영을 택했을까?
1) 비용이 “고정비”로 떨어진다
Inbound SaaS는 보통 월 구독(플랜) 형태가 시작점이다. 예를 들어 SendGrid는 Essentials가 월 $19.95~부터 시작하고, Mailgun도 무료 구간 이후는 유료 플랜을 전제로 한다.
반면 직접 수신 서버는 ‘메일 건수’가 아니라 ‘서버/디스크’ 중심으로 비용이 잡힌다.
서울 리전 기준으로 아주 러프하게 잡으면(온디맨드)
- EC2 t4g.micro: 시간당 약 $0.0104 → 월 약 $7.59
- EBS gp3 10GB: 대략 $0.08/GB-month 기준이면 월 $0.80
합치면 월 $8~9 수준에서 시작할 수 있고, 무엇보다 메일이 늘어도 “건당 과금”이 붙지 않아서 규모가 커질수록 예측이 쉬워진다.
여기에 Saving plans을 적용하면 40%는 더 싸진다.
또한 데이터 전송은 인바운드는 과금이 없고(원칙), 아웃바운드는 월 100GB까지 무료 구간이 있다.
뉴스레터 수신/파싱 구조에서는 보통 인바운드가 중심이라 네트워크 비용이 폭발하는 패턴이 상대적으로 덜했다.
2) 설계를 마음대로 가져갈 수 있다
이 방식의 핵심은 “제약이 없다”였다.
- Maildir 구조 그대로 저장(원문이 파일로 쌓임)
- 파싱 로직/재처리 전략(실패 메일 재시도, 파서 교체 등)
- 첨부파일 처리, 전처리/후처리
- 보관/삭제 정책(예: N일 보관, 특정 발행처 영구 보관)
- 트래킹/감사 로그(수신 시각, 처리 시각, 원문 해시 등)
메일이 핵심 도메인인 서비스에서, 이 “완전한 통제권”은 장기적으로 큰 자산이 된다.
3) 원본 이메일(.eml)을 그대로 확보할 수 있다
Inbound API는 “파싱된 결과”를 주는 대신, 원문을 장기 보관하려면 결국 별도 저장 설계가 필요하다.
반면 자체 수신은 원문(.eml) 확보가 기본값이라, 나중에 파서가 바뀌어도 “원문 기준으로 재처리”가 가능하고, 문제 상황에서 디버깅도 훨씬 단단해진다.
4) 대량 수신/버스트에 강하다
Postfix는 원래 큐 기반으로 트래픽을 흡수하도록 설계되어 있다.
뉴스레터처럼 특정 시간대에 몰아치는 패턴에서도, 큐/워커/파서 처리량을 조절하면서 안정적으로 버틸 수 있다는 점이 매력적이었다.
물론, 공짜는 아니다
직접 운영을 선택하는 순간 아래는 “우리 책임”이 된다.
- 스팸/어뷰징 대응(차단, 레이트리밋, 로그 감시)
- 보안 패치/권한 관리/접근 통제
- 디스크/백업/모니터링(메일이 쌓이는 속도, 큐 적체, 파서 지연)
다만 우리는 이메일이 서비스의 중심이고, 1~2달짜리 실험이 아니라 지속 운영이 전제라서, 이 운영 부담을 감수하더라도 비용 예측 가능성 + 설계 자유도 + 원문 확보 쪽이 장기적으로 더 맞다고 판단했다.
3) 수신 전용 이메일 서버 구현
전체적인 필요한 구조는 아래와 같다.
1. 발송 서버에서 메일 전송 시작
뉴스레터 발행 서비스의 메일 서버는 수신자 목록에 있는 user@bombom.news 주소를 보고, 해당 주소로 메일 전송을 시작
2. DNS에서 메일 담당 서버 조회(MX 레코드)
발송 서버는 먼저 DNS에 bombom.news 도메인의 메일을 어느 서버가 맡고 있는지 조회한다.
이때 DNS는 bombom.news 도메인에 설정된 MX 레코드를 보고,
예를 들어 mail.bombom.news 같은 메일 서버 주소를 알려준다.
이어서 발송 서버는 mail.bombom.news의 실제 ip 주소도 DNS로부터 조회한다.
3. 발송 서버 -> 우리 메일 서버 (SMTP 접속)
발송 서버는 조회한 IP로 SMTP(메일 전송 프로토콜) 연결을 맺고,
보내는 사람, 받는 사람, 제목, 본문 등 메일 데이터를 순차적으로 전송한다.
우리 메일 서버는 이 내용을 차례대로 수시하고, 메일을 저장할 준비를 한다.
4. 서버 내부의 Maildir에 메일을 파일로 저장
메일 서버는 수신이 끝난 이메일을 Maildir 형식의 디렉터리에 파일로 저장한다.
일반적으로 한 통의 메일은 Maildir/new 디렉터리 아래 파일 하나로 떨어지며,
이 파일 안에는 헤더(보낸 사람, 받는 사람, 제목, 날짜)와 본문 내용이 모두 포함된다.
여기까직가 이메일이 발송 서버에서 출발해, 우리 서버의 Maildir까지 도착하는 과정이다.
자 그럼 이제 우리가 구현한다고 했으니까
일단 이메일을 받는 것 까지 구현해보자
MX 레코드, 25번 포트, SMTP 서버
메일을 어디로, 어떤방식으로 받는 것을 정해한다.
1. DNS에서 메일 담당 서버를 정하는 것(MX 레코드)
2. 그 서버가 실제로 메일을 받을 준비하는 것(25번포트 + SMTP 서버)
1. MX 레코드
먼저, user@bombom.news로 메일이 도착하려면 DNS에게 bombom.news로 오는 메일은 어느서버가 담당하나요? 라는 질문을 했을 때, 우리 메일 서버를 가리키도록 MX 레코드를 설정해야 한다.
예를 들어
bombom.news의 MX레코드를 mail.bombom.news로 지정하고 mail.bombom.news에는 실제 메일 서버 IP주소를 연결한다.
이렇게 해두면, 외부 발송 서버는 항상
bombom.news -> MX 조회 -> mail.bombom.news -> IP 조회
라는 순서로 우리 서버를 찾아오게 된다.
그래서 우리가 아는 유명한 메일서버인 naver.com를 보면 아래와 같이 되어있다.

이렇게 여러개인이유는 네이버는 대형 메일 서비스 회사이기에 하루에 아마 수억 통 이상을 받을 것이다.
그럼 수신을 담당하는 서버가 하나로 버티기 힘들기도 하고 장애를 대비해야하기에 이렇게 여러개로 분산해놓는 거다.
자 그럼 이렇게 하면 메일을 받을 수 있을까??
아니다
MX 레코드만 있어서는 메일을 받을 수 없다.
표지판만 있고 정작 건물 문이 잠겨 있거나 안에 사람이 없는 것과 비슷하다.
그래서 다음 단계가 필요하다.
1. 메일 서버에서 25번 포트를 연다.
- 이메일 전송에 사용되는 기본 포트가 25번이기 때문에 외부 발송 서버가 이 포트로 접속할 수 있어야 한다.
2. MTA 서버를 설정한다.
- 25번 포트에서 대기하면서 들어오는 메일을 받아서 Maildir 같은 저장소에 기록하는 역할을 한다.
- 발송 서버가 제목,본문,수신자 정보를 보내면 이를 정상적으로 수신하고 파일로 저장한다.
앞선 글에서 SMTP는
메일 서버들이 서로 대화하기 위해 지켜야 하는 통신 규약이라고 설명했습니다.
메일을 보낼 때 어떤 순서로 명령을 주고받고, 성공이나 실패를 어떻게 응답하는지에 대한 약속이 바로 SMTP입니다.
그렇다면 자연스럽게 이런 질문이 생깁니다.
“그럼 이 SMTP 규약을 실제로 구현해서 메일을 주고받는 주체는 누구일까?”
SMTP(Simple Mail Transfer Protocol)는 이메일을 주고받기 위한 표준 통신 규약(프로토콜)이다.
사람이 메신저에서 대화를 주고받듯이, 메일 서버끼리는 SMTP라는 약속된 형식에 따라 대화를 주고받는다.
이때 보낸 사람 주소, 받는 사람 주소, 제목, 본문 내용, 그 외 헤더 정보들이 오간다.
그 역할을 맡는 것이 MTA(Mail Transfer Agent) 이다..
MTA는 SMTP 규약에 따라
외부 메일 서버와 통신하면서 메일을 받고, 전달하고, 필요하다면 큐에 쌓아 재시도하는 역할을 한다..
SMTP가 “규칙”이라면, MTA는 그 규칙을 현실에서 실행하는 프로그램이라고 볼 수 있다..
여기까지 이해했다면, 다음으로 궁금해지는 건 이 질문일 것이다.
“MTA에는 어떤 종류들이 있고, 각각 어떤 특징이 있을까?”
대표적인 MTA 종류들
1️⃣ Postfix
가장 널리 사용되는 MTA 중 하나이다.
- 보안, 성능, 설정 난이도의 균형이 좋음
- 구조가 비교적 단순하고 안정적
- Maildir, 스팸 필터, 외부 필터 연동이 쉬움
- 많은 리눅스 배포판에서 기본 또는 권장 MTA로 사용
요즘 새로 메일 서버를 구축한다면
가장 먼저 고려되는 선택지라고 봐도 무방하다.
2️⃣ Sendmail
한때 표준처럼 사용되던 전통적인 MTA이다.
- 아주 오래된 역사
- 강력하지만 설정이 복잡함
- 설정 파일 문법이 난해한 편
현재는 신규 구축보다는
기존 레거시 시스템 유지에서 주로 만나는 경우가 많다.
3️⃣ Exim
특히 유럽 쪽에서 많이 사용되는 MTA이다.
- 유연한 설정
- 복잡한 메일 정책을 표현하기 좋음
- 설정 자유도가 높은 만큼 진입 장벽도 있음
“메일 흐름을 세밀하게 제어해야 하는 환경”에서 선택되는 경우가 많다.
여기서 우리가 고려해야할 점들은 아래와 같다.
(1) 운영 난이도와 장애 대응 가능성
- 설정이 복잡하면 장애 때 “원인 추적”이 어려워진다.
- 작은 팀/개인 운영일수록 디버깅 가능한 복잡도가 중요하다.
- 로그가 명확한지, 흔한 문제의 해결책이 잘 정리돼 있는지도 크다.
(2) 보안 기본값과 설계 철학
- 외부에서 직접 연결을 받는 컴포넌트라 공격 표면이 큼
- 프로세스/권한 분리, 기본 설정의 안전함, 업데이트 흐름이 중요하다.
(3) 메일 큐(queue)의 신뢰성
현실에서는 메일이 항상 한 번에 성공하지 않는다.
- 상대 서버 장애/지연
- DNS 문제
- 일시적 네트워크 오류
이때 MTA가 큐잉/재시도/백오프를 얼마나 안정적으로 해주느냐가 핵심이다.
큐잉(Queuing): 실패해도 메일을 안전하게 보관하기
전송이 실패하면 MTA는 메일을 바로 버리지 않고 큐(Queue) 에 넣어둔다.
큐는 보통 디스크에 저장되기 때문에, 잠깐 장애가 있어도 메일이 사라지지 않는다.
즉 큐잉은 “전송 실패를 곧바로 ‘유실’로 만들지 않고, 다시 시도할 수 있도록 메일을 안전하게 보관하는 장치”이다.
백오프(Backoff): 재시도 간격을 점점 늘려 폭주를 막기
일시 실패가 났을 때, 바로바로 계속 재시도하면 문제가 생긴다.
상대 서버가 다운인데 계속 두드리면 상대도 더 힘들어지고 우리 서버도 큐 처리 때문에 리소스가 소모되고 특히 뉴스레터처럼 한꺼번에 많이 보내는 상황에서는 재시도가 겹쳐 폭주하기 쉽다.
그래서 MTA는 보통 재시도 간격을 점점 늘린다.
처음엔 몇 분 뒤 재시도 → 또 실패하면 더 늦게 → 계속 실패하면 간격을 더 벌리는 식이다.
이게 백오프이다.
“빠르게 회복을 노리되, 장애가 길어지면 시스템을 보호하는 전략”이다.
(4) 스팸/어뷰징/검증 확장성
운영하다 보면 “메일이 온다”가 끝이 아니라,
- 스팸 유입
- 봇/어뷰징
- 도메인 신뢰 판단(SPF/DKIM/DMARC)
같은 이슈가 반드시 따라온다.
MTA가 이런 확장 지점을 붙이기 쉬운지도 봐야 한다.
(5) 저장 방식과 파이프라인 궁합
Bombom처럼 “수신 → 저장 → 파싱” 구조라면 특히 중요하다.
- Maildir로 떨구기 쉬운가?
- 파일 기반 파이프라인으로 넘기기 쉬운가?
- 장애 시 “어디까지 처리됐는지” 추적이 쉬운가?
(6) 레퍼런스(문서/커뮤니티/사례)
이건 너무 실무적이라서 오히려 진짜 중요하다.
시간 단축을 엄청 해준다.
- 검색했을 때 바로 나오는가?
- 배포판 패키지/운영 사례가 충분한가?
- “처음 운영”의 시행착오 비용을 줄여주는가?
5) Bombom은 왜 Postfix를 선택했을까?
Bombom의 목표는 단순히 “메일을 받는다”가 아니라,
- 도메인으로 직접 메일 수신
- 수신 메일을 Maildir에 저장
- 이후 내부 로직에서 파싱/가공해 서비스화
이 흐름을 안정적으로 만드는 것이다.
그래서 MTA 선택 기준도 자연스럽게 이렇게 잡혔다.
- 운영 난이도가 과도하지 않을 것
- 외부 포트(25) 공개를 감당할 수 있을 만큼 안전한 설계일 것
- 큐/재시도 같은 “전달 신뢰성”이 검증되어 있을 것
- 앞으로 스팸/필터/검증을 붙일 때 확장 지점이 있을 것
- Maildir 기반 파이프라인과 궁합이 좋을 것
- 문제 생겼을 때 레퍼런스가 풍부할 것
이 기준으로 보면 Postfix는 “특정 하나가 압도적이라서”라기보다,
운영 현실에서 중요한 요소들의 균형이 가장 좋았던 선택지에 가까웠다.
즉, Bombom의 Postfix 선택은“많이 쓰니까”가 아니라
“우리가 감당해야 할 운영 책임(보안/큐/확장/디버깅)을 고려했을 때, 가장 합리적인 균형점이라서”이다.
| 기준 | Postfix | Exim | Sendmail |
| 운영 난이도(초기 세팅/유지보수) | 낮음~중간 (문서/예제 많음) | 중간~높음 (유연하지만 복잡성↑) | 높음 (레거시/설정 난해) |
| 장애 대응(로그/트러블슈팅 레퍼런스) | 매우 좋음 (사례 풍부) | 좋음 (다만 설정 복잡 시 난이도↑) | 보통~낮음 (레거시) |
| 보안 기본값/설계 철학 | 좋음 (현대적 권한 분리 구조) | 좋음 (설정에 따라 좌우) | 보통 (역사 오래됨) |
| 메일 큐/재시도 신뢰성(운영 안정성) | 매우 좋음 | 매우 좋음 | 좋음 |
| 확장성(필터/검증/정책 연동) | 좋음 (milter, policy, content_filter 등) | 매우 좋음 (정책 표현 강력) | 좋음(난이도↑) |
| Maildir/파이프라인 궁합 | 좋음 (구성 흔함) | 좋음 | 보통 |
| 생태계/도입 사례(배포판 기본/가이드) | 매우 많음 | 많음 | 레거시 많음 |
| 작은 팀이 처음 직접 운영 적합도 | 높음 | 조건부(운영 숙련도 있으면 강력) | 낮음 |
실제 적용해보기
일단 Postfix에 잘 도달할 수 있도록 미리 설정해줘야할게 있다.
발신 서버: “example.com 메일 어디로 보내지?” → MX 조회 → mail.example.com
발신 서버: “mail.example.com의 IP 뭐지?” → A 조회 → x.x.x.x
발신 서버: 그 IP의 25번 포트로 SMTP 접속해서 메일 전달
1) MX 레코드 설정
누군가가 user@example.com 으로 메일을 보내면, 보낸 쪽 메일 서버는 DNS에서 example.com의 MX 레코드를 조회하고, 그 결과로 나온 메일 서버 호스팅명(mail.example.com)으로 SMTP 연결을 시도한다.

2) A 레코드 설정
MX가 mail.example.com을 알려줬다면, 이제 실제로 접속하려면 IP가 필요하다.
그때 DNS에서mail.example.com의 A레코드를 조회해서 서버 IP를 알아낸 다음 그 IP의 25번 포트(SMTP)로 접속한다.

3) 보안 그룹/방화벽 25번 포트 인바운드 허용
만약 발신도 한다면 안정장치를 추가로 2가지를 해주면 좋다.
- PTR(Reverse DNS) 설정
- 오픈 릴레이(Open Relay) 차단 (reject_unauth_destination)
하지만 우리는 수신전용이기 때문에 이 부분은 넘어간다.
이제 Postfix를 설치하면된다.
Amazon Linux 2023
sudo dnf install -y postfix
sudo systemctl enable --now postfix
2) 기본 설정 적용
Postfix 핵심 설정 파일
- `/etc/postfix/main.cf`
# 1) 서버 식별
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
# 2) 바인딩
inet_interfaces = all
inet_protocols = all
# 3) 로컬 수신 도메인 (최소)
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
# 4) 오픈 릴레이 방지(필수)
smtpd_recipient_restrictions =
permit_mynetworks,
reject_unauth_destination
# 5) 메일 저장 형식(간단하게 Maildir 권장)
home_mailbox = Maildir/
3) 메일 저장 위치 만들기 Maildir
home_mailbox = Maildir/를 쓰면, 유저 홈 디렉토리에 Maildir로 떨어져.
예: newsletter라는 시스템 유저로 수신 받고 싶으면
sudo adduser newsletter
sudo -u newsletter mkdir -p /home/newsletter/Maildir
4) 서비스 재시작(적용 완료)
sudo systemctl restart postfix
sudo systemctl status postfix --no-pager
Postfix 설치와 DNS(MX/A), 25번 포트 개방까지 끝나면 다음 고민이 생긴다.
“메일은 이제 서버로 들어오는데… 이걸 우리 애플리케이션이 어떻게 읽어서 파싱하지?”
메일 수신 파이프라인을 만들 때 핵심은 단순히 “읽기”가 아니라, 장애가 나도 유실 없이 처리되고, 재처리/운영이 쉬우며, 추후 확장에도 부담이 적은 구조를 고르는 것이다.
우리는 크게 두 가지 방식을 비교했다.
1) pipe 방식(Push): 메일이 오면 Postfix가 즉시 프로그램에 넘긴다
이 방식은 Postfix가 메일을 받은 뒤, 그 내용을 바로 애플리케이션(혹은 스크립트) 에 전달해 즉시 처리한다.
- Postfix 수신
- 특정 주소/도메인 규칙에 따라 “로컬 배달” 단계에서
- 메일 본문을 프로그램의 stdin으로 전달(pipe)
- 프로그램이 즉시 파싱/저장하고 종료 코드로 성공/실패를 Postfix에 알림
즉 “메일이 도착한 순간 처리”가 가능해져서 체감상 실시간에 가깝다.
장점은 확실하다: 거의 실시간
- 폴더에 쌓아두고 나중에 처리하는 단계가 줄어든다.
- “도착 → 파싱 → 저장”이 곧바로 이어져 반응성이 좋아진다.
그런데 운영 난이도가 급격히 올라간다
pipe의 핵심 문제는, 메일 수신과 애플리케이션 처리가 강하게 결합된다는 점이다.
애플리케이션이 아래 상황에 들어가면, “수신” 자체가 불안정해질 수 있다.
- 파싱 로직이 느려져서 처리 시간이 길어짐
- 순간 트래픽으로 처리 요청이 몰림(버스트)
- 애플리케이션이 재시작/배포/장애로 응답 못함
이때 Postfix는 “전달 실패”로 인식하고 큐에 쌓거나 재시도하게 되는데, 이 동작을 안정적으로 굴리려면 생각보다 고려할 것이 많다.
- 성공/실패 기준(exit code) 설계
- 프로그램 실행 시간 제한(timeout)
- 밀릴 때의 backpressure(폭주 제어)
- 재시도 중 중복 처리 방지(멱등성)
- 장애 시 어떤 메일이 어디까지 처리됐는지 추적
정리하면, pipe는 “실시간”을 얻는 대신 운영에서 맞아야 할 변수가 확 늘어난다.
2) Maildir 폴링 방식(Pull): 폴더에 쌓아두고, 앱이 가져간다
이 방식은 Postfix가 받은 메일을 서버 파일로 저장하고, 애플리케이션이 주기적으로 확인해서 처리한다.
- Postfix 수신 → Maildir/new/에 메일 파일 저장
- 애플리케이션(스케줄러/워커)이 new/ 폴더를 주기적으로 확인
- 발견한 메일을 읽어서 파싱 → DB 저장/가공
- 처리 완료 후 cur/ 또는 별도 archive/ 폴더로 이동 (실패 시 failed/ 이동)
즉, Postfix는 “수신 및 저장” 역할에 집중하고, 애플리케이션은 “처리”만 담당한다. 수신과 처리가 분리되는 구조다.
왜 이 방식이 안정적일까?
이 방식의 가장 큰 장점은 애플리케이션 장애가 메일 수신을 망치지 않는다는 점이다.
- 애플리케이션이 잠깐 죽어도 Postfix는 계속 메일을 받아서 디스크에 쌓아둔다.
- 장애 복구 후 애플리케이션이 다시 켜지면, 밀려 있던 메일 파일을 순서대로 처리하면 된다.
- 운영 관점에서 “유실”보다 “지연”이 감당 가능한 문제로 바뀐다.
뉴스레터 수집/파싱처럼 “조금 늦게 처리되어도 되지만, 빠뜨리면 안 되는” 작업에는 이 점이 특히 크다.
단점은?
- 폴링 주기만큼 지연이 생긴다. (예: 2초~30초)
- 폴더 스캔 비용이 있다.
하지만 뉴스레터 수신량이 폭발적으로 크지 않은 초기 단계에서는, 이 비용은 보통 미미하다. 폴링 주기를 적절히 잡고(예: 2~5초), 처리 중인 메일을 별도 폴더로 옮겨 중복 처리만 막아주면 안정적으로 돌아간다.
운영하면서 추가 고려할 점
Pull 방식은 “폴더를 읽는다”로 끝나지 않고, 운영을 위해 보통 아래 3가지를 함께 챙긴다.
- 실패 폴더 + 재처리 전략
파싱 실패/DB 장애 등으로 처리에 실패하면 failed/로 옮겨두고, 나중에 재처리 배치로 다시 돌린다. - 중복 방지(멱등성)
메일은 재전송/중복 수신이 생각보다 자주 있다. Message-ID 같은 값을 DB에 저장해 unique 처리하면 안전하다.
우리는 왜 Pull(Maildir 폴링)로 결정했나
우리의 목표는 “메일이 도착하자마자 즉시 처리”가 아니라, 유실 없이 안정적으로 수집하고 파싱하는 것이었다. 뉴스레터 파이프라인에서는 특히 다음이 중요했다.
- 애플리케이션이 잠깐 실패해도 메일 수신이 멈추지 않을 것
- 실패한 메일을 따로 모아 재처리할 수 있을 것
- 운영이 단순해서 디버깅이 쉬울 것(원문 eml 파일 기반 확인 가능)
이 기준으로 보면, Maildir 폴링 방식은 수신과 처리를 분리해 리스크를 줄여주고, 장애가 나도 “지연”으로 흡수할 수 있는 구조였다. 반면 pipe 방식은 실시간이라는 장점은 크지만, 애플리케이션 상태가 곧바로 수신 안정성에 영향을 줄 수 있고, 재시도/폭주 제어/중복 방지 같은 운영 설계를 초기에부터 탄탄히 가져가야 했다.
그래서 우리는 초기 단계에서는 안정성과 단순성이 가장 큰 가치라고 판단했고, 최종적으로 Maildir 폴링(Pull) 을 선택했다.
'서비스 운영 일지 > 봄봄' 카테고리의 다른 글
| 봄봄 코드 리뷰 문화 개선 (4) | 2025.12.23 |
|---|---|
| 봄봄 AWS 비용 다이어트 이야기 (3) | 2025.12.09 |
| 봄봄에서 서드파티 라이브러리를 대하는 방법 (2) | 2025.11.22 |
| 봄봄 서비스에 맞게 검색 성능 개선기 (4) | 2025.11.17 |
| 저장이 왜 안되는 거지? (0) | 2025.11.15 |