[Spring Core] Spring DI(Dependency Injection)
DI(의존성) 주입 방법
- 생성자 주입
- 수정자 주입(setter)
- 필드 주입
- 일반 메서드 주입
1. 생성자 주입
생성자를 통해서 의존성을 주입 받는 방법이다.
생성자에 @Autowired를 붙이면 스프링 컨테이너에 @Component로 등록된 Bean에서 생성자에 필요한 Bean들을 주입한다.
특징
- 생성자 호출 시점에 딱 1번만 호출된다.
- 불변과 필수 의존 관계에 사용된다.
- 생성자가 1개만 존재할 경우, @Autowired 생략이 가능하다.
- NullPointerException을 방지할 수 있다.
- 주입 받을 필드를 final로 선언 가능하다.
@Component
public class OrderServiceImpl implements OrderService {
private final UserRepository userRepository
private final DiscountInfo discountInfo;
@Autowired // 생성자 주입
public OrderServiceImpl(UserRepository userRepository, DiscountInfo discountInfo) {
this.userRepository = userRepository;
this.discountInfo = discontInfo;
}
}
2. 수정자 주입
setter 메서드의 매개변수로 의존 객체를 주입하는 방법이다.
set필드명(주입객체) 형식의 메서드로 의존성을 주입한다.
특징
- 선택과 변경 가능성이 있는 의존 관계에 사용된다.
- 수정자가 복수 일 때, @AutoWired가 필수적으로 있어야 한다.
- 생성자가 1개 일 때 @AutoWired가 없어도 되는 이유는, 스프링이 해당 클래스 객체를 생성하여 Bean에 넣을 때, 생성자를 부를 수 밖에 없다. Bean을 등록하면서 의존 관계 주입도 같이 발생하게 되는 것이다.
@Component
public class OrderServiceImpl implements OrderService {
private UserRepository userRepository;
private DiscountInfo discountInfo;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setDiscountInfo(DiscountInfo discountInfo) {
this.discountInfo = discountInfo;
}
}
3. 필드 주입
필드에 @Autowired를 붙여서 바로 주입하는 방법이다.
실제 코드와 상관 없는 특정 테스트를 하고 싶을 때 사용할 수 있다.
특징
- 외부에서 변경이 불가능하여 테스트하기 힘들다는 단점이 있다.
- DI 프레임워크가 없다면 아무것도 할 수 없다.
- setter가 필요하여 수정자 주입이 더 편리하다.
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserRepository userRepository;
@Autowired
private DiscountInfo discountInfo;
}
4. 일반 메서드 주입
일반 메서드를 사용해 의존성을 주입하는 방법이다.
특징
- 한번에 여러 필드를 주입 받을 수 있다.
- 일반적으로 사용되지 않는다.
의존성 주입 옵션
스프링 컨테이너에 의존성 주입할 Bean이 존재하지 않는 경우가 있다. 이러한 경우엔 @Autowired만 사용한다면 에러가 발생한다.
의존성 주입할 Bean이 존재하지 않더라도, 기본 로직으로 동작하거나 다른 값으로 처리하고 싶은 경우에 옵션 처리를 하면 된다.
- @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않는다.
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
public class AutowiredApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required = false)
public void setNoBean1(User noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable User noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<User> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
/**
noBean2 = null
noBean3 = Optional.empty
**/
생성자 주입을 사용해야 하는 이유?
과거에는 수정자, 필드 주입을 많이 사용했지만, 최근에는 대부분 생성자 주입 사용을 권장한다.
생성자 주입의 이점
불변
- 수정자 주입의 경우엔, 메서드 변경이 가능하기 때문에 적합하지 않다.
- 생성자 주입은 객체를 생성할 때, 최초 1번만 호출되기 때문에 불변하게 설계할 수 있다.
누락
- 의존관계 주입 누락 시, NPE(Null Point Exception)이 발생하는데 생성자 주입은 누락 시 컴파일 오류를 발생시킨다.
final 키워드 사용 가능
- 생성자는 객체 생성 시 최초 1회만 호출되므로, 상수를 지정할 수 있다.
- 나머지 주입 방식은 생성자 이후에 호출되는 형태이므로 final 키워드를 사용할 수 없다.
순환 참조
- 순환 참조(참조했던 객체를 다시 참조)를 방지할 수 있다.
- 생성자를 통한 참조간순환이 발생한다면, BeanCurrentlyInCreationException 에러가 발생한다.
- 나머지 주입 방식은 Bean이 생성된 후에 참조를 하기 때문에 에러 없이 구동된다. (실제 코드가 호출되기 전까지 문제를 알 수 없음)
Component Scan
@ComponentScan은 설정 정보 없이 자동으로 Bean을 등록하는 기능이다.
지금까지는 @Configuration이 지정된 클래스에 @Bean이 지정된 메서드를 호출하여 스프링 컨테이너에 Bean을 추가했지만, 설정 정보가 커지고 누락되는 등 다양한 문제가 발생할 수 있다.
@ComponentScan은 @Component가 붙은 모든 클래스를 Bean으로 등록해준다.
(@Configuration에 @Component 애너테이션도 포함되어 있으므로, 설정 정보 클래스도 스프링 컨테이너에 Bean으로 추가된다.)
@Configuration
@ComponentScan
public class AutoAppConfig {
}
또한, @Autowired 기능도 제공한다.
basePackages
탐색할 패키지의 시작 위치를 지정하고, 해당 패키지부터 하위 패키지 모두 탐색한다.
@ComponentScan(basePackages = "탐색 시작할 패키지 위치") 형식으로 사용한다.
매개변수를 지정하지 않으면, @ComponentScan 애너테이션이 지정된 클래스의 패키지가 시작 위치가 된다.
스프링부트를 사용한다면 @SpringBootApplication을 프로젝트 시작 루트에 위치하는 것을 추천한다.
(@SpringBootApplication에 @ComponentScan이 들어있다.)
Component Scan 기본 대상
- @Component : 컴포넌트 스캔에서 사용된다.
- @Controller & @RestController : 스프링 MVC 및 REST 전용 컨트롤러에서 사용된다.
- @Service : 스프링 비즈니스 로직에서 사용된다.
- 특별한 처리를 하지 않지만, 개발자들이 핵심 비즈니스 로직이 있다는 비즈니스 계층 인식에 도움이 된다.
- @Repositrory : 스프링 데이터 접근 계층에서 사용된다.
- 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
- @Configuration : 스프링 설정 정보에서 사용된다.
- 스프링 설정 정보로 인식하고, 스프링 Bean이 싱글톤을 유지하도록 추가 처리를 한다.
Filter
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
- includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
Filter Type option
- ANNOTATION : 기본값, 애너테이션을 인식해서 동작한다.
- ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다.
- ASPECTJ : AspectJ 패턴을 사용한다.
- REGEX : 정규 표현식을 나타낸다.
- CUSTOM : TypeFilter라는 인터페이스를 구현해서 처리한다.
내용 요약
스프링 컨테이너
- ApplicationContext가 스프링 컨테이너이다.
- 스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용한다.
- Bean은 스프링 컨테이너에서 관리하는 객체이다.
- @Bean이 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- BeanDefinition은 Bean 설정 메타 정보이다.
- 스프링 컨테이너는 BeanDefinition 이라는 추상화를 통해 스프링 Bean을 생성한다.
컴포넌트 스캔
이전에는 직접 @Bean을 통해 설정 정보를 작성하고 의존 관계도 직접 명시했지만, 컴포넌트 스캔은 Bean을 자동으로 찾아 생성해준다.
@Component 애너테이션이 붙은 클래스를 찾아 Bean으로 등록해 준다.
스프링 컨테이너 생성 순서
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class)
- AppConfig.class를 파라미터로 ac를 생성하게 되면 내부적으로는 AppConfig.class 정보를 바탕으로 BeanDefinition 인터페이스의 구현체 중 하나인 객체를 만든다.
- AnnotationConfigApplicationContext에 AppConfig.class를 넘겨줬을 때 BeanDefinition의 구현체인 AnnotatedGenericBeanDefinition을 만들게 된다.
- 해당 객체에서 Bean 메타정보를 가지고 스프링 컨테이너에서 Bean을 생성하게 된다.
애너테이션
@Configuration : 클래스 레벨에서 선언. bean 메타 설정 정보를 담고있는 bean. 이 정보를 토대로 bean을 만들어낸다.
@Bean : 메서드 레벨에서 선언. 메서드는 리턴해주는 객체가 있으므로, 해당 메서드가 반환하는 객체를 bean으로 등록
@Component : 클래스 레벨에서 선언. 해당 클래스를 bean으로 등록
@ComponentScan : @Component이 붙은 bean의 위치부터 하위 패키지 위치까지 bean을 자동 등록
@Autowired : @Component & @ComponentScan만 사용했을 때, 클래스에 어떤 의존 객체를 주입할지 명시해주지 않기 때문에 의존성 주입이 필요한 생성자 부분에 @Autowired를 통해 의존 관계 주입이 필요하다.