목차
Method Signature 오버라이딩할 경우 중요(메소드 이름, 파라미터 타입들)
Method Type (리턴타입, 파라미터 타입들, 메소드 타입 파라미터 , 익셉션)
Super Type Token
위 메모의 메서드 시그니처와 슈퍼 타입 토큰은 자바의 타입 시스템과 관련된 별도 주제다. 스프링 DI 자체는 객체가 필요로 하는 다른 객체를 외부에서 전달하는 방식으로 이해하면 된다.
class MailService {
private final MailSender sender;
MailService(MailSender sender) {
this.sender = sender;
}
}
MailService가 내부에서 new SmtpMailSender()를 호출하는 대신 생성자 인자로 받으면, 실제 전송 구현과 테스트용 구현을 선택하는 책임을 조립하는 쪽으로 옮길 수 있다. 스프링에서는 빈 등록 후 컨테이너가 생성자 인자를 찾아 연결한다.
flowchart LR 컨테이너[스프링 컨테이너] --> 발신자[MailSender 빈 생성] 발신자 --> 서비스[MailService 생성자에 전달]
생성자 주입은 필요한 의존성을 객체 생성 시점에 명시하고, 필드를 final로 둘 수 있다. 같은 타입의 빈이 여러 개라면 어떤 빈을 주입할지 이름이나 한정자 등으로 명시해야 한다. 순환 의존성이 생기면 단순히 설정을 우회하기보다 두 객체의 책임을 다시 살펴본다.
스프링 없이 먼저 이해하기
MailSender sender = new SmtpMailSender();
MailService service = new MailService(sender);
이 두 줄의 조립을 스프링 컨테이너가 대신 수행한다고 생각하면 된다. 테스트에서는 SmtpMailSender 대신 전송 기록만 남기는 FakeMailSender를 전달할 수 있다. 그러면 메일 서버를 호출하지 않고 MailService의 업무 규칙을 확인할 수 있다.
MailSender fakeSender = new FakeMailSender();
MailService testService = new MailService(fakeSender);
이 예시는 인터페이스와 구현체가 이미 있다는 전제의 조립 코드다. 의존성 주입의 목적은 모든 클래스를 인터페이스로 쪼개는 데 있지 않다. 객체가 자신의 필수 협력자를 명시하고 생성 책임을 밖으로 보내는 데 있다. 선택 가능한 구현이 하나뿐이고 바뀔 이유가 없다면 구체 클래스를 생성자에서 받아도 주입은 성립한다.
메일 전송 사례를 끝까지 연결하기
앞 코드의 MailSender, SmtpMailSender, FakeMailSender는 조립 방향을 설명하는 이름이다. 실제 프로젝트에서는 계약을 명시하고, 서비스가 전송 결과나 예외를 어떻게 처리할지 정해야 한다. 예를 들어 가입 환영 메일은 가입 저장과 동시에 반드시 성공해야 하는지, 실패해도 가입은 유지하고 나중에 재시도할지에 따라 트랜잭션 설계가 달라진다. DI는 구현을 교체할 수 있게 하지만 업무 실패 정책을 대신 정해주지 않는다.
interface MailSender {
void send(String to, String body);
}
final class RecordingMailSender implements MailSender {
private final List<String> recipients = new ArrayList<>();
@Override
public void send(String to, String body) {
recipients.add(to);
}
List<String> recipients() {
return List.copyOf(recipients);
}
}
테스트에서는 RecordingMailSender를 서비스 생성자에 넣고 recipients()로 전송 대상을 검증한다. 이 가짜 구현은 실제 SMTP 서버를 호출하지 않으므로 메일 내용·주소 검증과 서비스의 호출 여부를 분리할 수 있다. 실제 SMTP 설정이 맞는지는 별도의 통합 테스트에서 확인한다.
flowchart LR A["프로덕션 설정"] --> B["SmtpMailSender"] C["단위 테스트"] --> D["RecordingMailSender"] B --> E["MailService 생성자"] D --> E
같은 생성자가 두 환경에서 다른 구현을 받는다. MailService가 구현을 직접 생성하면 이 교체가 어려워진다. Spring에서는 @Component나 @Bean으로 등록된 후보를 타입으로 찾는다. 두 구현을 동시에 등록하면 어떤 빈을 고를지 @Qualifier 또는 @Primary로 명확히 해야 한다.
기존 메모의 “Method Signature”와 “Super Type Token”은 Java 타입 시스템을 학습할 때 별도로 다룰 주제다. DI 문제를 디버깅할 때는 먼저 필요한 타입, 등록된 빈, 후보 선택 규칙, 빈 스코프를 확인한다. 생성자에서 순환 의존성이 드러나면 @Lazy로 덮기 전에 각 객체의 책임을 재검토한다.