본 글은 인프런 김영한님의 스프링 완전 정복 로드맵을 기반으로 정리했습니다.
public class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public Long processOrder() {
// 주문 처리 로직
}
}
이전 OrderService는 자신이 필요한 객체를 직접 생성하는 대신 생성자를 통해 의존성을 주입 받았다.
의존주입을 사용하도록 변경한 OrderService는 어떤 종류의 DiscountPolicy 구체 클래스가 들어오는지 모른다. 더 이상 자신이 직접 의존 객체를 생성하고 실행하지 않고, 제어의 흐름을 다른 곳으로 넘겨준 것이다. 이를 제어의 역전이라고 한다. OrderService는 추상화에 의존하기 때문에 구현객체의 변경에 유연해진다. 그렇다면 의존객체는 어디서 생성되어서 주입되는것일까?
스프링의 DI(Dependency Injection) 컨테이너가 의존객체 생성과 주입의 책임을 가지게 된다. DI 컨테이너는 앞서 말한 제어의 역전이 일어나기 때문에 IoC(Inversion of Control) 컨테이너 혹은, 객체를 생성 및 조립한다는 의미에서 어셈블러 또는 오브젝트 팩토리로 불리기도 한다.
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderService(new discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
빈 이름 | 빈 객체 |
---|---|
orderService | OrderService@x01 |
discountPolicy | RateDiscountPolicy@x02 |
@Configuration
애노테이션은 DI 컨테이너의 설정정보를 담고있는 클래스라는 것을 의미한다. @Bean
애노테이션이 붙은 메서드가 반환하는 객체들은 DI 컨테이너가 관리하게 된다. DI 컨테이너가 관리하는 객체를 빈이라고 한다. @Configuration
애노테이션이 붙은 설정클래스도 빈으로 등록된다.
빈의 이름은 메서드 이름을 사용한다. @Bean(name="myBeanName")
처럼 이름을 지정해줄 수는 있지만 특별한 이유가 없다면 기본값을 사용하도록 하자.
DI 컨테이너는 기본적으로 빈 객체를 싱글톤 객체로 관리한다. 싱글톤 객체는 요청시마다 객체를 새로 생성하는 오버헤드가 없기 때문에 성능상의 이점이 있다. 이게 가능한 이유는 스프링이 @Configuration
애노테이션이 붙은 설정 클래스를 직접 사용하지 않고 내부적으로 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 설정 클래스를 상속한 새로운 설정 클래스를 사용하기 때문이다.
실제론 더 복잡하겠지만 대략 아래의 느낌일 것이다.
public class AppConfigExtensionByCGLIB extends AppConfig {
private Map<String, Object> beans = ...;
@Override
public OrderService orderService() {
if (!beans.containsKey("orderService")) {
beans.put("orderService", super.orderService());
}
return (OrderService) beans.get("orderService");
}
...
}
public class Main {
public static void main(String[] args) {
ApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
}
}
컨테이너에서 빈 객체를 조회하는 코드다. ApplicationContext
를 스프링의 DI 컨테이너라고 생각하면 된다.
ApplicationContext
는 인터페이스다. AnnotationConfigApplicationContext
는 @Configuration
애노테이션이 붙은 설정 클래스를 매개변수로 받아 컨테이너를 생성하는 ApplicationContext
의 구현체다. 생성자의 매개변수가 가변인자이기 때문에 여러개의 클래스 정보를 받아서 컨테이너를 생성할 수 있다.
설정클래스는 @Import
애노테이션을 붙여서 다른 설정 클래스를 포함할 수 있다. 이 외에도 GenericXmlApplicationContext
구현체를 이용하면 xml 파일을 이용해서 컨테이너를 생성할 수 있다. xml 파일은 자바 클래스가 아니기 때문에 컴파일 없이도 빈을 변경할 수 있다는 장점이 있지만 최근에는 스프링 부트를 사용하면서 대부분 애노테이션 기반으로 빈을 생성하기 때문에 잘 사용되지는 않는다.
@Configuration
public class AppConfig {
private DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Bean
public OrderService orderService() {
return new OrderService(discountPolicy);
}
}
꼭 모든 객체를 빈으로 등록해야 하는 것은 아니다. 다음처럼 일반 객체를 생성해서 주입할 수 있다. OrderService에 주입한 DiscountPolicy 객체는 DI 컨테이너의 관리를 받지 않는 일반 객체다.
DI 컨테이너는 싱글톤 관리, 의존관계 자동 주입, 라이프사이클 관리와 같은 다양한 기능을 제공한다. 이런 기능이 필요하지 않다면 꼭 빈으로 등록할 필요는 없다. 그러나, 의존관계 자동 주입과 싱글톤 관리는 프로젝트 전반에 걸쳐 사용되기 때문에 특별한 이유가 없으면 빈으로 등록해서 DI 컨테이너가 제공하는 이점을 활용하도록 하자.