Spring/공부

[Spring Core] 컴포넌트 스캔과 의존성 주입

오잎 클로버 2023. 1. 14. 09:00
728x90

🔍 컴포넌트 스캔이 필요한 이유

스프링은 무수히 많은 컴포넌트와 빈들을 가지게 되며, 프로젝트의 규모가 커져갈수록 더 많은 컴포넌트들과 빈들을
정의해주어야 하는 번거로움과 함께 실수를 하게 될 수 있다.

그래서 스프링 빈은 설정 정보가 없더라도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공함으로써 
이러한 문제를 해결하고자 마련되어 있다.

 

🎯 컴포넌트 스캔 작동 원리

@ComponentScan@Component 어노테이션이 있는 클래스들을 스프링 빈으로 등록하는 과정을 거치게 된다.

이때, 스프링 빈은 기본적으로 맨 앞글자만 소문자를 사용한 이름으로 지정된다.

@Component("nothing")
public class Something {
    ...
}

위처럼 하는 경우 스프링 빈 이름은 기본적으로는 something으로 등록되지만, 이 경우에는 따로 지정되었기에
  nothing으로 지정된다.

 

📦 컴포넌트 탐색위치 및 기본 탐색 대상

컴포넌트 스캔은 모든 자바 클래스들을 전부 탐색하면서 스프링 빈으로 등록하려면, 너무 많은 시간들이 사용된다.

(외부 라이브러리 등 말 그대로 모든 클래스들을 한다면 말이다.)

따라서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@ComponentScan(
    basePackages = "me.iqpizza6349"
)

basePackages 를 통해 시작 위치를 지정할 수 있다. 이때, 해당 패키지를 시작으로 하위 패키지까지 모두 포함하여

  탐색하게 된다.

추가로1) basePackages = {"me.iqpizza6349.core", "me.iqpizza6349.service"} 처럼 여러 시작위치 역시 지정할 수 있다.

추가로2) basePackagesClasses 를 통해서 특정 클래스의 패키지를 시작 위치로 지정하는 것 역시 가능하다.

추가로3) 만일 따로 지정되지 않은 경우, @ComponentScan 클래스가 위치한 패키지를 시작 위치로 지정한다.

권장하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스를 프로젝트 최상단에 두는 것이다.
스프링 부트 역시 이 방법을 채택하여 기본으로 제공한다.
더보기

참고로 스프링 부트를 사용하면, @SpringBootApplication 어노테이션이 프로젝트의 시작 루트 위치에 두는 것이
관례이며, Spring Initializr 에서 생성하는 경우 역시 이 관례를 따른다.
(그리고 @SpringBootApplication 어노테이션 안에 @ComponentScan 어노테이션이 포함되어 있다.)

 

💉 의존성 주입

의존성 주입(DI)은 크게 4가지 방법이 있다.

  • 생성자(Constructor) 주입
  • 수정자(Setter) 주입
  • 필드(Field) 주입
  • 일반 메소드 주입 (Setter method 제외 / 보편화되지 않은 방법)

 

🎉 생성자 주입

생성자를 통해 의존성을 주입받는 방법이다.
보편적인 방법 중 하나이며, 불변성을 보존할 수 있다는 큰 강점이 있다.
  • 스프링에서는 싱글톤 패턴으로 관리가 되니, 생성자가 단 한 번만 호출된다는 점을 생각해 보면,
      가장 적절한 방법이라 할 수 있다.
  • 또한, 반드시 값을 초기화하도록 유도한다.
  • private final 을 보통 사용한다.
  • 순수 자바의 특징을 잘 사용할 수 있으며, 프레임워크에 의존하지 않게 의존성 주입을 받을 수 있다.
스프링 빈인 경우, 생성자가 단 하나뿐이라면, @Autowired 를 생략하더라도 자동으로 주입된다.

 

🔨 수정자 주입

수정 메소드에 @Autowired 를 추가하여, 스프링에서 의존성을 주입받는 방법이다.
선택 혹은 변경 가능성이 있는 경우에는 조금 유연성 있게 의존성 주입받을 수 있다는 점이 있다.
  • 자바빈 프로퍼티 규약의 수정자 메소드 방식을 사용하여 주입을 받는다.
  • 선택적 의존성 주입이 가능하다는 점에서 유연하다면 유연하나, 만일 깜빡하는 경우 문제가 발생할 수도 있다.
  • 반드시 @Autowired 를 각 수정자 메소드에 요구한다.
  • 스프링 컨테이너는 크게 2가지 라이프 사이클(Life-Cycle)이 있다.
    • 스프링 빈 등록
    • 의존성 자동 주입
      • 빈 등록 이후, @Autowired 가 포함된 메소드 혹은 필드에 주입해 준다. 

 

🧫 필드 주입

필드에 의존성 주입을 받는 방식이다.
필드 주입은 많은 개발자들에게 지양받는 방식의 의존성 주입방식이다.
코드가 타 방법에 비하면 굉장히 짧은 편이지만, 외부에서 변경이 불가하며, 테스트하기 어렵다는 점이
  가장 큰 단점이다.
  • DI 프레임워크 없이는 pure-java로 구현하기에는 무리가 있다.
    • 물론 불가능한 것은 아니다. Reflection을 통해 처리하면 가능하긴 하다.
  • 굳이 사용한다면, 단위 테스트에 반드시 꼭 필요할 때에만 사용하길 바란다.

 

🤡 빈 타입이 중복인 경우

인터페이스를 구현한 구현체가 2개 이상인 경우, 의존성 주입 시, 문제가 발생할 수도 있다.

보통 스프링에서의 자동 의존성 주입 시, 타입을 통해 조회를 한다.

그렇기에 보통 인터페이스의 구현체가 2개 이상인 경우에 발생하는 경우가 많다.이런 경우, NoUniqueBeanDefinitionException 예외가 높은 확률로 발생하게 된다.

 

하위 타입으로 지정해서 주입받으면 해결되는 거 아닌가?

라고 생각하기 십상이다. 하지만 이 경우, DIP를 위배하며, 유연상 따윈 사라져 버린다.

빈 타입이 중복인 경우 해결할 수 있는 방법은 크게 3가지 정도 있다.

  • 스프링 빈을 수동으로 등록한다.
  • 따로 이름을 지정한다.
  • 우선순위를 통해 지정한다.

 

🧪 스프링 빈을 수동으로 주입한다.

개인적인 생각이지만, 웬만하면 하지 않았음 하는 방법이다.

물론, 스프링 의존성 주입을 통한다면 수동 등록이 우선시되어 수동 빈이 덮어쓰기 되는 방식으로 작동되어

  해결되긴 한다.

하지만, 만일 버전 별로 다른 의존성을 우선적으로 등록해야 한다면, 굉장히 귀찮을 것이다.

또한, 하나의 클래스에서 각각 다른 구현체들을 주입받는 다면, 문제가 생길 수도 있다.

 

🎈 스프링 빈의 이름을 따로 지정한다.

이 방법은 다시 @AutoWired@Qualifer 를 사용하는 방법, 2가지 방법으로 나뉜다.

@AutoWired 를 사용하는 방식

  • 필드명 혹은 파라미터명을 바꿔 빈 이름을 추가 매칭하도록 하여, 이런 문제를 해결할 수 있다.
  • 물론 가독성이 떨어진다는 것은 덤이다.

@Qualifer 를 사용하는 방식

  • 추가 구분자를 넣는 방식이다. 빈 이름을 직접으로 바꾸는 @Autowired 와는 다르다.
  • 물론, 가독성이 떨어질 수 있지만, 필드명 혹은 파라미터명이 아예 달라지게 되는 @Autowired 방식에 
      비하면 상당히 괜찮은 편이며, 추가 구분자를 잘 작성할 경우 오히려 가독성이 올라가게 된다.
  • 단점이 있다면 NoSuchBeanDefinitionException 예외가 발생할 수 있다는 건 언제나 감안해야 한다.
    • 사실 단점이라고 할 것이 없는 것이, 원래 빈이 정의되지 않아도 발생하는 예외이다. 

 

💼 우선순위를 통해 지정하는 방식

깔끔하게 작성하도록 유도하는 방식이다. 물론 @Qualifer 빈보다 선택권 우선순위가 낮다.

@Primary 를 통해 컴포넌트를 정의할 때 지정할 수 있으며, 한 번 지정해 두면,

  사용되는 클래스에서 의존성 주입받을 때, 일일이 작성할 필요가 없다.

 

💡 팁

하나의 클래스에서 같은 타입의 여러 구현체를 사용해야 한다면, @Primary@Qualifer 둘 다 사용하는 것을 추천한다.

기본적으로 스프링은 자동보다는 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선순위가 높다.

따라서 @Primary@Qualifer를 같이 쓰게 된다면, @Qualifer가 우선적으로 지정되기 된다.

 

 

이상입니다.