Backend/Spring

컴포넌트 스캔과 자동 의존관계 주입

olsohee 2023. 12. 29. 15:46

컴포넌트 스캔(@ComponentScan, @Component)과 자동 의존관계 주입(@Autowired)

지금까지 스프링 빈을 등록할 때 @Bean을 포함한 설정 정보 클래스를 작성해주었다. 그러나 등록해야 할 빈이 수십, 수백개가 되면 일일히 등록하기 매우 번거롭다. 따라서 스프링은 자동으로 스프링 빈을 등록해주는 컴포넌트 스캔이라는 기능을 제공한다. 

 

다음과 같이 설정 정보 클래스에 @ComponentScan을 붙여주면 된다. 그러면 기존과 달리 @Bean을 작성하며 일일히 빈을 등록해주던 코드를 작성해주지 않아도 된다.

 @Configuration
 @ComponentScan()
 public class AutoAppConfig {
 }
  • 설정 정보 클래스임을 나타내기 위해 @Configuration을 붙여주었다. (➡️ 스프링 빈이 싱글톤임이 보장된다.)
  • @ComponentScan을 붙여줌으로써 기존과 달리 수동으로 빈 등록 코드를 작성하지 않아도 된다.

그리고 다음과 같이 스프링 빈으로 등록할 클래스에 @Component를 붙여주면 된다.

 @Component
 public class MemberServiceImpl implements MemberService {
 
     private final MemberRepository memberRepository;
     
     @Autowired
     public MemberServiceImpl(MemberRepository memberRepository) {
         this.memberRepository = memberRepository;
     }
}

 

그리고 이때 @Autowired가 등장한다. 기존에 수동으로 빈을 등록할 때는 직접 설정 정보를 작성했고 의존관계도 명시했다. 그러나 자동 빈 등록 시에는 설정 정보 자체를 작성하지 않기 때문에 의존관계를 명시할 수 없다. 따라서 자동 빈 등록 시에는 @Autowired를 붙여서 의존관계를 명시한다. 그러면 스프링은 의존관계를 자동으로 주입해준다. 

컴포넌트 스캔 동작 과정

1. 스프링 빈 등록

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 이름은 클래스 명을 사용하되 맨 앞글자만 소문자로 한다. 예를 들어 클래스명이 MemberServiceImpl이면 빈 이름은 memberServiceImpl이 된다. 그리고 @Component("memberService")와 같이 빈 이름을 지정해 줄 수도 있다.

2. 의존관계 주입

생성자에 @Autowired를 지정하면, 스프링 컨테이너에서 해당 빈을 찾아서 의존관계를 주입해준다. 이때 기본 조회 전략은 타입이 같은 빈을 찾는 것이다. 즉 아래 예시의 경우 getBean(MemberRepository)로 조회하는 것과 동일하다고 할 수 있다.

컴포넌트 스캔의 탐색 대상

컴포넌트 스캔의 대상은 무엇일까? 별도로 대상을 지정하지 않으면, @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 되어, 해당 패키지를 포함하여 하위 패키지를 모두 탐색한다

 

그리고 다음과 같이 탐색 시작 위치를 지정해 줄 수 있다. 

@ComponentScan(basePackages = "hello.core")
  • basePackages 옵션: 해당 패키지가 탐색 시작 위치가 된다. 
  • basePackageClasses: 해당 클래스의 패키지가 탐색 시작 위치가 된다. 

그런데 가급적 프로젝트의 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 권장된다. 스프링부트도 이러한 방식을 사용하고 있는데, 스프링 부트의 대표 시작 정보인 @SpringBootApplication 어노테이션은 다음과 같다. 

@SpringBootApplication
public class BasicApplication {

	public static void main(String[] args) {
		SpringApplication.run(BasicApplication.class, args);
	}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

 

보다시피 스프링 부트의 대표 시작 정보인 @SpringBootApplication는 @Configuration과 @ComponentScan을 포함한다.

  • @Configuration: 해당 클래스가 설정 정보이다.
  • @ComponentScan: 스프링 부트를 통해 프로그램을 실행하면, 프로젝트의 전체 패키지가 컴포넌트 스캔의 대상이 된다. 

컴포넌트 스캔의 기본 탐색 대상

@Component 뿐만 아니라 다음 어노테이션들은 @Component를 포함하고 있기 때문에 컴포넌트 스캔의 대상이 된다. 

  • @Controller: 스프링 MVC 컨트롤러로 인식된다.
  • @Service: 스프링 비즈니스 로직에서 사용한다. 별다른 처리를 하지는 않으나, 개발자들이 핵심 비즈니스 로직이 이곳에 위치한다는 것을 인식하는데 도움이 된다.
  • @Repository: 스프링 데이터 접근 계층으로 인식되고, 데이터 계층의 예외를 스프링 예외로 변환해둔다.
  • @Configuration: 스프링 설정 정보로 인식되고, 스프링 빈이 싱글톤임이 보장된다.

의존관계 주입 방법

앞서 자동 빈 등록 시에는 @Autowired 어노테이션을 통해 의존관계를 명시하여 의존관계가 주입된다고 했다. 그리고 @Autowired를 통한 의존관계 주입은 스프링 컨테이너에 의해 관리되는 스프링 빈이어야 정상 동작한다. 의존관계 주입 방법은 다음과 같다.

생성자 주입

@Component
public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
    
	@Autowired // 생성자가 1개만 있으므로 생략 가능
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}
  • 생성자를 통해서 의존관계를 주입하는 방법이다. 
  • 생성자가 호출될 때 "딱 1번"만 의존관계가 주입된다. 따라서 의존관계가 불변일 때 사용한다.
  • 생성자가 호출될 때 "반드시" 의존관계가 주입된다. 따라서 반드시 의존관계가 주입되어야 할 때(필수) 사용한다.
  • 기본으로 스프링 컨테이너에 스프링 빈이 생성되고, 그 다음에 의존관계가 주입된다. 그러나 생성자 주입은 예외로 스프링 빈의 생성과 의존관계 주입이 동시에 일어난다. 빈 객체를 생성할 때 생성자가 호출되고, 이때 의존관계 주입이 일어나기 때문이다.
  • 생성자가 1개만 있으면 @Autowired를 생략할 수 있다.

수정자 주입(setter 주입)

@Component
public class OrderServiceImpl implements OrderService {

	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;

	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}
  • 수정자 메소드를 통해 의존관계를 주입하는 방법이다.
  • 스프링 컨테이너에 해당 객체가 등록되어 있지 않을 때, 즉 선택적으로 의존관계를 주입할 때 사용한다. 위 코드를 예로 들면, MemberRepository가 스프링 컨테이너에 등록되어 있지 않을 수도 있을 경우에 사용하는 것이다. 그리고 이때는 @Autowired(required = false) 옵션을 주어야 한다. (@Autowired는 기본적으로 주입할 대상이 없으면 오류가 발생한다.)
  • 수정자 메소드를 사용하기 때문에 중간에 의존관계를 변경하고 싶으면, 외부에서 수정자 메소드를 호출하여 다른 객체와의 의존관계를 주입해주면 된다. 따라서 의존관계가 변경될 가능성이 있을 때 사용한다. 

필드 주입

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}
  • 필드에 바로 주입하는 방법이다.
  • 코드가 간결하지만, 외부에서 의존관계를 변경할 수 없다.
  • 스프링 없이 순수 자바 코드로 테스트해야 하는 경우가 있는데, 필드 주입을 사용하면 스프링 없이 테스트가 불가능하다. (스프링 컨테이너가 없기 때문에 빈을 찾고 주입하는 과정이 일어나지 않기 때문이다. 그러면 외부에서 의존관계를 직접 주입하면 되지 않을까? 그러나 필드 주입은 외부에서 의존관계를 주입할 수 없다.) 
  • 만약 스프링 없이 테스트를 하려고 하면 의존관계가 주입되지 않았기 때문에 NullPointerException이 발생할 것이다.
  • 따라서 필드 주입은 되도록 사용하지 말고, @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 사용하자. 

일반 메소드 주입

@Component
public class OrderServiceImpl implements OrderService {

	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;

	@Autowired
	public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}
  • 일반 메소드를 통해서 주입하는 방법이다.
  • 일반적으로 잘 사용하지 않는 방법이다.

@Autowired의 옵션 

@Autowired를 통해 의존관계를 주입할 때 해당 객체가 스프링 빈으로 등록되어 있지 않으면 오류가 발생한다(@Autowired(required = true)로 설정되어 있기 때문에). 그런데 주입 대상이 없어도 계속 동작해야 할 때가 있다. 따라서 이 경우에는 다음과 같은 옵션을 사용하면 된다.

  • @Autowired(required = false): 주입할 대상이 없으면 수정자 메소드 자체가 호출되지 않는다.
  • @Nullable: 주입할 대상이 없으면 null이 입력된다.
  • Optional<>: 주입할 대상이 없으면 Optional.empty가 입력된다.
// Member는 스프링 빈이 아니다.

@Autowired(required = false)
public void setNoBean1(Member member) {
	System.out.println("setNoBean1 = " + member); // 메소드 자체가 호출되지 않는다.
}
 

@Autowired
public void setNoBean2(@Nullable Member member) {
	System.out.println("setNoBean2 = " + member); // 출력: setNoBean2 = null
}

@Autowired
public void setNoBean3(Optional<Member> member) {
	System.out.println("setNoBean3 = " + member); // 출력: setNoBean3 = Optional.empty
}

 

@Nullable이나 Optional<>는 스프링 전반에 걸쳐서 지원된다. 따라서 예를 들어 생성자 주입에서 특정 필드를 대상으로 @Nullable이나 Optional<>를 사용할 수 있다.

생성자 주입을 사용하자

지금까지 다양한 의존관계 주입 방법을 알아봤다. 그러나 생성자 주입을 사용하는 것이 가장 권장된다. 그 이유는 다음과 같다.

  • 대부분의 의존관계는 불변해야 한다.
    • 생성자 주입을 사용하면 딱 1번 의존관계가 주입되고, 그 이후로 변경될 수 없다.
  • 의존관계의 누락을 막을 수 있다.
    • 스프링 없이 순수 자바 코드로 테스트 코드를 작성할 때 수정자 주입을 사용한다고 가정하자.
      • 스프링을 통해 실행하는 경우에는 @Autowired가 동작하며 수정자 주입 메소드가 실행될 때, 주입 대상이 없으면 오류가 발생한다.
      • 그러나 스프링 없이 실행하는 경우에는 @Autowired가 동작하며 수정자 주입 메소드가 실행될 때, 스프링 컨테이너가 없어서 주입할 대상이 없음에도 불구하고 오류가 발생하지는 않는다. 그러나 의존관계가 주입되지 않았기 때문에 NullPointerException이 발생한다.
    • 수정자 메소드와 달리 생성자 주입을 사용하면서 의존관계 필드 값을 final로 선언하며 의존관계가 누락되는 것을 막을 수 있다.
    • 생성자 주입이 아닌 나머지 방법은 생성자 호출 이후에 의존관계 주입 코드가 실행되기 때문에 의존관계 필드를 final로 선언할 수 없다.

참고로 롬복의 @RequiredArgsConstructor를 사용하면 final 필드를 인자로 갖는 생성자를 자동으로 생성해주어 더욱 깔끔하게 코드를 작성할 수 있다.

  • @RequiredArgsConstructor ➡️ 생성자 생략 가능 ➡️ 생성자가 1개이므로 @Autowired 생략 가능

Reference

  • 인프런, 스프링 핵심 원리 - 기본편, 김영한