채널톡 앱스토어팀이 이메일로만 들어오는 외부 서비스 고객 문의를 채널톡 유저챗으로 연결한 Mail Relay Extension 구조를 다룬다. 연동 앱마다 메일 수신·발송 권한을 갖게 하면 권한이 앱 수만큼 퍼지고 relay 주소 소유권 검증 책임도 흩어진다. 그래서 App Store가 메일 권한과 라우팅 경계를 소유하고, 연동 앱은 provider별 도메인 해석만 맡도록 책임을 갈랐다.
핵심 포인트- 마켓플레이스·쇼핑몰처럼 채팅 API 없이 이메일 알림으로만 문의가 오는 외부 서비스가 있어, 상담자가 채널톡과 외부 도구를 오가며 누락·중복 응대가 생기던 문제에서 출발했다.
- 수신 경로는 메일 인프라 → App Store → queue → worker → 연동 앱 handler, 발신 경로는 연동 앱 → App Store native function → 메일 발송 인프라로 고정했다.
- App Store는 메일 내용을 해석하지 않는다. 출처 검증과 라우팅 키 판별, 권한 검증만 하고 의미 해석은 전부 앱 몫이다.
- 앱은 메일 원문 저장소 권한도, 메일 발송 권한도 직접 갖지 않는다. App Store native function이 권한·소유권·메시지 ID를 대조한 뒤 broker 한다.
- 앱 전용 relay 도메인으로 온 메일은 기존 이메일 medium 수신 경로에 닿기 전에 분리해, 기존 이메일 문의 흐름을 건드리지 않았다.
상세 정리- 문제 정의: 고객 문의는 웹 채팅이든 카카오 상담이든 유저챗 한 곳에 모여야 담당자 지정·이전 맥락 확인·응답 이력이 성립하는데, 이메일 포워딩으로만 오는 문의는 판매자 개인 메일함이나 외부 관리자 페이지에 갇혔다.
- 첫 후보 기각: 연동 앱이 각자 메일 수신·발송 권한을 갖는 안은 앱마다 권한이 복제되고 relay 주소 소유권 검증 책임까지 흩어져 버려 채택하지 않았다.
- 수신 경로 분리: 앱 전용 메일 도메인(shop-123@{app-relay-domain} 형태)으로 온 메일은 raw MIME을 저장하고 App Store로 이벤트를 발행한 뒤 그 자리에서 멈춰, 일반 이메일 medium 경로까지 흘러가지 않게 했다.
- 라우팅 키: worker가 수신 주소의 local-part를 라우팅 키로 읽어 Mail Relay Extension을 등록한 앱 중 일치하는 앱의 inbound handler를 호출한다. 여기까지가 App Store 책임의 끝이다.
- 원문 접근 방식: function input에는 raw MIME 전체가 아니라 원문을 찾을 참조 정보만 담는다. 앱이 본문이 필요하면 native function을 호출하고, App Store가 호출 앱의 권한·수신자 소유권·메시지 ID·원문 위치가 서로 맞는지 검증한 뒤에야 저장소에서 읽어 돌려준다.
- 앱의 책임 범위: relay 주소에서 shop id나 reply token 파싱, 앱 설정의 Mail Relay 사용 여부 확인, 메일 header·본문 파싱, 중복 수신 판단, 새 UserChat 생성 또는 기존 thread 이어붙이기.
- UserChat 생성을 App Store가 하지 않는 이유: 주소 포맷·원문 구조·중복 판단 키·답장 스레딩 방식이 서비스마다 달라 전부 앱별 도메인 지식이기 때문이다.
- 답장 경로: 상담원이 유저챗에서 답장하면 앱이 From·To·Reply-To·Message-ID·In-Reply-To·References 등 스레드 유지용 header로 raw MIME을 조립하되, 발송은 native function에 맡긴다. App Store가 sender의 relay domain 소속 여부, Extension 등록 여부, token scope와 멱등 키 유효성을 검증한 뒤 보낸다.
- SDK 계약: Mail Relay를 SDK의 extension 타입으로 등록해 앱 개발자는 정해진 entrypoint와 input 타입만 구현하고, App Store는 registry로 라우팅 가능한 앱만 호출한다. 인프라 권한과 검증은 전부 App Store 내부에 숨긴다.
- 설계가 바뀐 지점 1: 초기에는 기존 중계 계층이 SNS webhook을 받아 App Store로 넘기는 안도 검토했으나, registry·앱 권한·native function 계약을 이미 App Store가 갖고 있어 외부 webhook을 App Store가 직접 받는 쪽으로 정리하고 중계 계층을 뺐다.
- 설계가 바뀐 지점 2: HTTPS subscription은 endpoint 소유자가 confirmation을 마쳐야 실제 이벤트가 오는데, 이를 확인하지 않고 메일 수신 rule부터 열면 메일은 저장돼도 이벤트가 안 온다. 운영 순서를 subscription confirmation → queue·권한 확인 → 메일 수신 rule 적용으로 바꿨다.
- 설계가 바뀐 지점 3: SNS에서 곧장 앱 handler를 부르면 단순해 보이지만 앱 function 호출을 외부 webhook 요청 시간 안에 끝낼 수 없었다. public endpoint는 검증 후 queue 적재만 하고 worker가 handler를 호출하도록 나눠, 파싱 실패·앱 서버 일시 실패·중복 이벤트에 재시도와 DLQ 재처리 경로를 확보했다.
- prod에서 드러난 것: exp 성공 후에도 원문 저장소 권한, 메일 발송 권한, 발송 계정의 production access 상태가 각각 별개 체크포인트로 순차 실패했다. 이 과정에서 로그에 라우팅 키·멱등 키·provider message id를 남기는 일의 중요성을 다시 확인했다.
- 결론적 정리: Mail Relay의 어려움은 메일 파싱 코드가 아니라 메일을 잃지 않고, 중복 처리하지 않고, 실패 시 재시도할 수 있는 운영 경계를 어디에 그을지였다.
왜 읽나플러그인·앱 플랫폼에서 외부 인프라 권한을 공통 계층에 가두고 도메인 로직만 확장 지점으로 여는 경계 설계를 고민하는 백엔드·플랫폼 엔지니어에게 참고가 된다.