v13.39.0의 구현을 확인한 내용입니다. 기본 전송 방법은 메일 전송을, 프로바이더 준비는 패키지 개발을 참조하세요.
Mailer 이름과 Transport 이름 구분하기
Mail::mailer('receipts')는 mail.mailers.receipts를 읽고 transport에 대응하는 생성 처리를 호출합니다. MailManager::build()가 그 Transport를 Laravel의 Mailer에 전달하며, 일반적인 이름 기반 해석에서는 전역 발신자 등도 설정합니다.
커스텀 생성 처리는 내장 Transport보다 먼저 선택됩니다. smtp나 ses를 덮어쓰지 말고 패키지 고유의 Transport 이름을 사용하세요.
외부 전송 없이 등록 경로 만들기
먼저 배송처 API가 아니라 설정과 해석 경로를 확인합니다.Acme\Postbox 패키지의 src/Transport/MemoryTransport.php에 표준 ArrayTransport를 상속한 클래스를 만듭니다.
ArrayTransport::send()는 Symfony의 SentMessage를 내부 Collection에 저장합니다. messages()로 확인할 수 있고, flush()로 해당 인스턴스의 기록을 지울 수 있습니다. 라벨은 설정이 생성 처리까지 전달되었는지 확인하기 위한 것입니다.
지연 로드되는 Manager에 등록하기
공식 문서에서는boot()에서 Mail::extend()를 호출합니다. 패키지에서는 메일을 사용하지 않는 애플리케이션에서 Manager가 불필요하게 생성되지 않도록 서비스 해석 후 훅을 조합할 수 있습니다.
src/PostboxServiceProvider.php를 만듭니다.
- Laravel의
MailServiceProvider는 지연 프로바이더이며mail.manager를 singleton으로 등록합니다.Mail파사드도 이 키를 해석합니다. callAfterResolving()은 아직 해석되지 않았다면 이후의 해석을 기다리고, 이미 해석된 Manager에는 즉시 확장을 적용합니다. 생성된 Mailer까지 교체하는 처리는 아닙니다.- 확장을 등록하는 자신의 프로바이더까지 단순히
DeferrableProvider로 만드는 것은 피하세요. 훅이 등록되기 전에 독자적인 Transport가 요청되지 않도록 합니다. - 이 예는 자신의
boot()에서 훅을 등록합니다. 다른 프로바이더가 그보다 먼저 독자적인 Mailer를 사용하는 부팅 순서는 보장하지 않습니다. 등록 순서를 설계하고,register()안에서는 메일을 전송하지 마세요.
extend의 인수를 다른 Manager와 혼동하지 않기
MailManager::createSymfonyTransport()는 커스텀 생성 처리를 설정 배열 하나만으로 호출합니다. Cache::extend()처럼 애플리케이션과 설정의 두 인수를 받는 방식이 아닙니다. 반환값은 TransportInterface를 구현한 Transport이며, Laravel의 Mailer나 전송 결과가 아닙니다.
또한 MailManager::extend()는 클로저를 보관할 뿐이며 Manager로의 재바인딩은 하지 않습니다. 예에서는 static 클로저와 명시적인 인수를 사용해 $this의 해석에 의존하지 않도록 했습니다.
외부 API용 클라이언트가 필요하다면 이 생성 처리에서 설정을 적용해 Transport에 전달합니다. 등록 시점에는 전송하지 않고, 같은 Transport 이름을 쓰는 Mailer가 여러 개 있을 때 설정이나 수신처가 섞이지 않도록 설계하세요.
사용하는 애플리케이션에 Mailer 추가하기
config/mail.php의 기존 mailers 배열에 다음 두 항목을 추가합니다. 패키지 쪽에서 사용자의 기본값이나 mailers 전체를 덮어쓰지 않습니다.
MAIL_MAILER에 지정하는 것은 Transport 이름이 아니라 receipts라는 Mailer 이름입니다. 개별로 지정하면 기존 기본값을 바꾸지 않고 사용할 수 있습니다.
raw()는 뷰를 준비하지 않고 동기 전송 경로를 확인하기 위한 예입니다. Mailable을 사용하는 일반적인 전송에서도 Mailer가 본문 등을 생성한 뒤 같은 Transport에 전달합니다.
실제 배송 Transport를 만들 때의 경계
외부 서비스로 배송하는 구현에서는 공식 문서에 따라Symfony\Component\Mailer\Transport\AbstractTransport를 상속하고 doSend(SentMessage $message): void와 __toString(): string을 구현할 수 있습니다. 위의 등록 방법은 그대로 사용할 수 있습니다.
Laravel의
Mailer::sendSymfonyMessage()는 Transport의 send()를 호출합니다. MessageSending 리스너가 전송을 중단했거나 Transport가 null을 반환한 경우, Laravel의 send()도 전송 결과를 반환하지 않습니다. 예외가 없었다는 사실만으로 배송 성공을 판단하지 마세요. SentMessage가 반환되더라도 수신자의 메일함에 도달했다는 보장은 아닙니다.
설정 변경과 Mailer 재해석 구분하기
MailManager는 Mailer와 그 Transport를 Mailer 이름별로 보관합니다. 설정 변경이나 extend() 재등록만으로는 이미 생성된 객체가 갱신되지 않습니다.
$same은 $before와 같은 Mailer입니다. purge('receipts') 이후에 가져온 $after에서는 새 설정으로 Transport가 생성됩니다.
확인한
purge() 구현은 Manager 내부 배열에서 참조를 제거할 뿐입니다. 이전의 $before나 그 Mailer를 이미 주입받은 서비스는 교체되지 않습니다. 외부 연결을 명시적으로 종료하는 처리도 아닙니다.
failover 등의 부모 Mailer도 별도로 보관된다
failover와 roundrobin은 자식 Mailer 이름의 설정을 읽어 Transport를 직접 생성해 부모 Transport에 포함합니다. 자식의 Mail::mailer() 인스턴스를 공유하는 구조가 아닙니다.
따라서 자식의 설정을 변경하고 purge('receipts')를 호출해도, 이미 해석된 부모 Mailer의 내부 Transport는 갱신되지 않습니다. 갱신 대상인 부모 Mailer도 재해석해야 합니다. forgetMailers()로도 이미 사용하는 쪽이 보관하고 있는 부모 Mailer의 참조까지 교체할 수는 없습니다.
운영 환경에서는 설정 캐시 갱신과 장기 실행 워커의 재시작을 일반적인 배포 절차에 포함하세요. 재해석을 모든 프로세스의 일괄 갱신으로 여기지 말고 서비스의 라이프사이클도 확인하세요.
Mail::fake뿐 아니라 실제 생성 경로 테스트하기
Mail::fake()는 Mailable의 전송·큐 등록을 기록하기 위한 것입니다. MailFake::mailer()는 실제 Mailer를 해석하지 않고 Mailer 이름을 기록한 뒤 fake 자신을 반환합니다. raw()도 전송 처리를 하지 않습니다. 독자적인 Transport가 등록되어 있지 않아도 Mailable 전송 어설션이 성공할 수 있습니다.
패키지 테스트 환경에서 다음과 같이 fake를 사용하지 않고 실제 Manager와 Transport를 거치게 합니다. 이 예의 Transport는 외부로 전송하지 않으므로 실제 배송 위험 없이 등록 경로를 확인할 수 있습니다. 보조 프로바이더를 먼저 로드해 Manager가 이미 해석된 경우도 검증합니다.
참조한 1차 자료
- Laravel 공식 문서: 메일의 커스텀 Transport와 테스트(확인 시점)
- Laravel 공식 문서: 패키지 개발
- MailManager: 생성 처리, Mailer 보관, purge와 복합 Transport
- MailServiceProvider: MailManager 바인딩
- ServiceProvider: callAfterResolving
- Mailer: Transport로의 전송과 이벤트
- ArrayTransport: 메시지 기록
- Mail 파사드와 MailFake
- MailFake: Mailer 선택과 Mailable 기록
- Laravel 공식 테스트: Mailer와 ArrayTransport를 이용한 전송 검증