Spring Boot/Core

[Spring Core] Spring DI(Dependency Injection)

WY J 2022. 8. 12. 15:28

DI(의존성) 주입 방법

  1. 생성자 주입
  2. 수정자 주입(setter)
  3. 필드 주입
  4. 일반 메서드 주입

 

 

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)
  1. AppConfig.class를 파라미터로 ac를 생성하게 되면 내부적으로는 AppConfig.class 정보를 바탕으로 BeanDefinition 인터페이스의 구현체 중 하나인 객체를 만든다.
  2. AnnotationConfigApplicationContext에 AppConfig.class를 넘겨줬을 때 BeanDefinition의 구현체인 AnnotatedGenericBeanDefinition을 만들게 된다.
  3. 해당 객체에서 Bean 메타정보를 가지고 스프링 컨테이너에서 Bean을 생성하게 된다.

 

 

애너테이션

@Configuration : 클래스 레벨에서 선언. bean 메타 설정 정보를 담고있는 bean. 이 정보를 토대로 bean을 만들어낸다.

@Bean : 메서드 레벨에서 선언. 메서드는 리턴해주는 객체가 있으므로, 해당 메서드가 반환하는 객체를 bean으로 등록

@Component : 클래스 레벨에서 선언. 해당 클래스를 bean으로 등록

@ComponentScan : @Component이 붙은 bean의 위치부터 하위 패키지 위치까지 bean을 자동 등록

@Autowired : @Component & @ComponentScan만 사용했을 때, 클래스에 어떤 의존 객체를 주입할지 명시해주지 않기 때문에 의존성 주입이 필요한 생성자 부분에 @Autowired를 통해 의존 관계 주입이 필요하다.