이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › Spring
의존성 주입은 무엇을 대신해 주는가
의존성 주입은 클래스가 일을 하려고 필요로 하는 객체를 스스로 만들지 않고 밖에서 넣어 받는 것입니다. 여기서 OrderService는 결제를 하려고 PayService 객체가 필요하고, 이것을 "OrderService가 PayService에 의존한다"고 합니다. 스프링에서는 OrderService의 생성자 매개변수에 PayService를 적어 두는 것이 전부이고, 클래스 위의 @Component는 스프링이 그 클래스의 객체(빈)를 만들어 보관하라는 표시입니다. 스프링이 OrderService 객체를 만들 때 보관하던 PayService 빈을 그 생성자에 넣어 줍니다. 이 실습은 PayService를 생성자로 받는 대신 new PayService()로 직접 만들거나 아예 받지도 만들지도 않고, PayService 위의 @Component를 지워 가며 같은 main을 실행해, OrderService가 쓰는 PayService가 어디서 왔는지와 PayService 객체가 몇 개 만들어지는지를 봅니다.
실행으로 대조 · 스프링 프레임워크 7.0.9에서 여섯 가지 조합을 실제 컴포넌트 스캔으로 컴파일해 실행하고, 필드의 객체까지 리플렉션으로 재어 대조
@Component
public class PayService {
public String pay() {
return "결제 완료";
}
}
@Component
public class OrderService {
private final PayService payService;
// 주입: 필요한 객체를 생성자의 매개변수로 적어 두면 스프링이 넣어 준다
public OrderService(PayService payService) {
this.payService = payService;
}
public String order() {
return payService.pay();
}
}
public class Main {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext("app");
OrderService orderService = context.getBean(OrderService.class);
System.out.println(orderService.order());
}
}
스프링 프레임워크 7.0.9를 JDK 21.0.12.1에서 실행한 결과입니다. 스프링 부트는 쓰지 않고, 화면의 main이 그대로 쓰는 AnnotationConfigApplicationContext("app")의 컴포넌트 스캔만 썼습니다. 여섯 가지 조합을 모두, 화면의 코드와 같은 소스(패키지 app의 PayService, OrderService, Main)를 javac(-parameters)로 컴파일하고 자식 자바 프로세스로 실행해 확인했습니다. 컴포넌트를 손으로 등록하지 않고 스프링이 클래스를 훑어 찾게 했습니다. 대조용 소스에만 Probe 클래스를 더하고 네 줄을 끼웠습니다. 두 클래스에는 객체가 만들어질 때 그 생성자를 부른 쪽(스프링인지 OrderService의 어느 생성자인지)을 스택에서 읽어 적는 인스턴스 초기화 블록 한 줄씩을, main에는 컨텍스트가 만들어진 뒤와 getBean 뒤에 측정하는 줄을 한 줄씩 더했고, 대조 스크립트는 화면의 코드와 견줄 때 이 줄들을 뺍니다. 대조한 것은 실행 결과(출력, 시작 실패한 예외의 종류와 원인, 멈춘 예외와 그 자리), OrderService가 쓰는 PayService, 만들어진 PayService 객체의 수입니다. "OrderService가 쓰는 PayService"는 짐작이 아니라 실측했습니다. 실행 중에 리플렉션으로 OrderService 객체의 payService 필드를 읽어, 컨텍스트에 있는 PayService 빈과 같은 객체인지(==)와 비어 있는지를 견주었습니다. 객체의 수도 생성자가 불린 횟수를 센 것입니다. 단계 문장이 기대는 사실도 따로 재서 확인했습니다. 컴포넌트 스캔이 찾은 클래스, 객체가 만들어진 순서(생성자 주입에서는 PayService가 먼저, 직접 만들 때는 OrderService가 만들어지는 중에 PayService가 만들어짐), 모든 객체가 컨텍스트를 만드는 줄에서 만들어지고 getBean 줄에서는 새로 만들어지지 않는다는 것, 스프링이 기록한 OrderService의 의존 대상(생성자로 받을 때만 payService), PayService 빈이 없을 때 OrderService가 만들어지지 않고 main이 첫 줄에서 멈춘다는 것, 비어 있는 필드를 쓰다 멈추는 자리가 OrderService의 order() 메서드라는 것, 우리 코드에 new OrderService(...)라는 줄이 없다는 것입니다. 시작에 실패한 조합의 실제 예외는 UnsatisfiedDependencyException이고 원인은 NoSuchBeanDefinitionException(PayService 타입의 빈이 없음)이며, 원문은 Probe 출력에 남겼습니다. 문서가 정한 것은 스프링 프레임워크 문서 Core의 Dependency Injection 절(의존성 주입이 객체가 의존하는 다른 객체를 생성자 인자 등으로만 정하고 컨테이너가 빈을 만들 때 그것을 넣어 주는 과정이라는 것, 생성자 기반 주입이 컨테이너가 각각 의존성을 뜻하는 인자를 가진 생성자를 부르는 것이라는 것), Using @Autowired 절(대상 빈의 생성자가 하나뿐이면 그 생성자에 @Autowired를 붙이지 않아도 된다는 것), Classpath Scanning and Managed Components 절과 @Component, AnnotationConfigApplicationContext의 API 문서(@Component가 붙은 클래스가 자동 탐지의 후보이고, 지정한 패키지를 스캔해 빈 정의를 등록한다는 것)입니다. 같은 실행에서 본 다음 동작은 문서가 정한 것이 아니라 이 버전에서 관찰한 것입니다. 클래스 파일을 훑는 순서에서 OrderService가 PayService보다 먼저 나왔고, 생성자 주입에서는 그 때문에 OrderService를 만들다가 PayService를 먼저 만들었다는 것, new로 직접 만들 때 스프링이 OrderService를 먼저 만들고 PayService 빈을 그다음에 만든다는 것(스캔 순서에 달린 것이어서 클래스 이름이 바뀌면 달라질 수 있음), 비어 있는 필드를 스프링이 오류로 알리지 않는다는 것입니다. 화면의 코드는 package와 import 줄을 줄였습니다. 이 실습은 생성자 주입만 다룹니다. 세터 주입, 필드 주입(@Autowired), 같은 타입의 빈이 여러 개일 때, 순환 의존, 빈의 범위와 생명주기, 인터페이스로 받는 설계는 다루지 않았고, "세부 동작 실습: 빈 생성과 주입"에서 다룹니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.