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

Seyeong·2022년 10월 24일
0

스프링

목록 보기
3/7

@ComponentScan과 @Autowired

@ComponentScan 어노테이션이 붙으면 해당 클래스는 컴포넌트 스캔의 대상이 된다. @Configuration도 마찬가지로 내부에 ComponentScan 어노테이션을 포함하였기 때문에 컴포넌트 스캔의 대상이 되었다.

이러한 컴포넌트 스캔이 붙은 클래스는 @Component 어노테이션이 붙은 객체들을 찾아 스프링 빈으로 등록하게 된다.

만약 @Component가 붙은 클래스들이 객체를 주입받으려고 한다면 생성자 부분에 @Autowired 어노테이션을 붙여 의존 자동 주입을 설정해주어야 한다.

@ComponentScan

  • @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 스프링 빈의 이름은 클래스 명의 앞글자만 소문자인 형태이다.

@Autowired 의존 자동 주입

  • 만약 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 해당 객체 빈을 자동으로 찾아서 주입한다.
  • 기본 빈 조회 전략으로는 같은 타입이 있는지 탐색한다. (이는 인터페이스를 상속받은 자식타입도 가능하므로 구현체가 발견되면 이를 주입함) 만약, 같은 타입이 여러개 존재한다면 그건 아래에서 더 자세히 설명한다.

@ComponentScan

@ComponentScan 어노테이션에는 많은 옵션이 있다.

탐색 위치 지정과 기본 스캔 대상

  • basePackages : 탐색을 시작할 패키지를 지정함. 이 패키지를 포함한 하위 패키지를 탐색한다. (여러 시작위치 지정이 가능하다)
  • basePackageClasses : 지정한 클래스가 포함된 패키지를 탐색 시작 위치로 지정한다.
  • default : 만약 아무것도 지정하지 않으면 @ComponentScan 어노테이션이 붙은 클래스가 포함된 패키지부터 탐색이 시작된다.(★)

권장하는 방법 : 따로 패키지 위치를 지정하지 않고, 설정 클래스의 위치를 프로젝트 최상단에 위치하는 것이다. 최근 스프링 부트도 이러한 default 방법을 기본으로 제공한다.

예를 들어)

com.hello
com.hello.service
com.hello.repository
와 같은 패키지들이 존재할 때

com.hello 위치에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 어노테이션을 붙인 뒤 basePackages를 생략하는 방식이다.

이는 메인 설정 정보가 프로젝트를 대표하는 정보이기 때문에 최상단에 위치하는 것이 적절하다.

또한 스프링 부트를 사용하면 스프링 부트의 시작 정보인 @SpringBootApplication 어노테이션이 붙은 클래스가 프로젝트의 최상단에 위치하게 되며, 여기엔 @ComponentScan 어노테이션이 포함된다!
-> 즉, @ComponentScan 자체를 쓸 필요가 없어지기까지도 한다. 그냥 @Component로 스프링 빈 등록을 요구만 해도 자동으로 등록되겠지만 원리를 알아야 한다.

@ComponentScan 기본 대상

@Component, @Controller, @Service, @Repository, @Configuration과 같은 어노테이션들은 기본적으로 내부에 @Component 어노테이션을 포함하고 있기 때문에 자동으로 컴포넌트 스캔의 대상이 된다.

※ 참고 : 원래 어노테이션간의 상속 관계란 없다. 위처럼 하나의 어노테이션이 다른 어노테이션을 들고 있는 것을 지원해주는 기능은 자바 언어가 지원하는 것이 아니라, 스프링이 지원하는 기능이다.

추가적으로 아래의 어노테이션들은 다음과 같은 추가 기능이 존재한다.

@Controller : 스프링 MVC 컨트롤러로 인식
@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층 예외를 스프링 예외로 변환시켜 준다.
@Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 처리해준다.
@Service : 사실 @Service는 특별한 처리를 하지 않는다. 단순히 개발자들이 핵심 비즈니스 로직이 이곳에 위치할 것이라고 인식하는데 도움이 된다.

중복 등록과 빈 충돌

1. 자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 만약 이름이 같다면 스프링은 오류를 발생시킨다
-> ConflictingBeanDefinitionException 발생

2. 수동 빈 등록 vs 자동 빈 등록 (★)

수동 빈 등록이 자동 빈 등록을 오버라이딩 해버린다 (덮어쓴다.)
물론 이러한 상황을 개발자가 의도적으로 기대한다면 좋겠지만, 현실에서는 대부분 여러 설정이 꼬이면서 발생하게 된다. 따라서 이러한 버그는 잡기가 매우 힘들기 때문에 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생하도록 기본 값을 바꾸었다.

오해하면 안되는 것이 스프링 프레임워크의 기본 값은 Override이지만,
스프링 부트의 경우가 오류를 발생시키도록 변경한 것이다.

의존 관계 자동 주입

자동 주입의 4가지 방식

1. 생성자 주입

  • 생성자 호출 시점에 1번만 주입
  • 불변, 필수 의존관계에 사용
  • 생성자가 단 1개일 때는 @Autowired 생략해도 자동으로 적용

2. setter 주입

  • setter라 불리는 메서드를 통해서 의존관계를 주입
  • 선택, 변경 가능성이 있는 의존관계에서 사용

3. 필드 주입

  • 필드에 바로 의존관계를 주입
  • 편리하지만 외부에서 변경이 불가능하기 때문에 테스트하기 힘들다는 치명적인 단점이 존재
  • DI 프레임워크 없이는 사용이 불가능 (단위 테스트 시 부적절)
  • 애플리케이션 코드와 관계가 없는 테스트 코드는 필드 주입 사용 가능

4. 일반 메서드 주입

  • 일반 메서드를 통해 주입
  • 한번에 여러 필드 주입 가능
  • 잘 사용하지 않는 방법

의존주입 @Autowired의 옵션

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 메서드 자체가 호출이 안됨
  • @Nullable : 자동 주입할 대상이 없으면 Null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

의존주입 시 2개 이상 빈 매칭 해결 방안

1. @Autowired 필드 명 매칭

@Autowired는 처음에 타입 매핑을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
ex)

@Autowired
private DiscountPolicy rateDiscountPolicy;

위처럼 의존주입을 받아오면 만약 DIscountPolicy의 구현체 두 개가 모두 @Component 어노테이션이 붙어서 둘 다 스프링 빈으로 등록되어도, 필드 이름으로 빈 매칭을 한다.

2. @Qualifier -> @Qualifier 끼리 매칭 -> 빈 이름 매칭

@Qualifier("mainDiscountPolicy") 식으로 빈 이름에 식별자를 지정해준다. 단, 빈의 이름을 변경시키는게 아니라 @Qualifier 식별자만 지정해준 것이다.
만약 @Qualifier를 통해 의존주입 시 같은 식별자를 가진 빈이 없다면, @Qualifier 지정자에 적힌 이름을 가진 빈을 가져온다. 그래도 빈이 없다면 NoSuchBeanDefinitionException 이 발생한다.

추가로 @Qualifier는 파라미터로 문자열을 넘겨주기 때문에 컴파일 단계에서 에러 체크가 안된다. 따라서 이 경우 개발자가 별도로 어노테이션을 제작하여 적용시켜주는게 좋다.

3. @Primary 사용 (★)

자주 사용하지만 한계가 있는 우선 순위 방식.
동일한 이름을 가진 빈이 여러개 선택될 경우 @Primary가 붙은 어노테이션이 그냥 우선순위를 가지고 주입된다.

@Qualifier와 @Primary의 우선권

@Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 따라서 @Qualifier가 우선권이 더 높다.

Map과 List를 이용해 조회한 빈 모두 찾기

'스프링 빈으로 등록된' 클래스 내부에서 @Autowired를 이용하여 Map과 List에 특정 빈을 생성자 주입 받으면, 제네릭<T> 에 지정한 타입과 일치하는 모든 타입들을 컬렉션에 담아서 가져오게 된다.

가령 Map<String, DiscountPolicy>를 생성자 파라미터로 받아오게 되면 구현체들인 Fix, Rate 두 스프링 빈이 map에 담기게 되는 것이다.

이를 위해서는 당연히 의존 대상이 스프링 빈으로 등록되어야 하고, 객체 자신또한 스프링 빈으로 등록되어 있어야 스프링 컨테이너가 이를 인식하고 의존관계를 맺어줄 것이다.

자동, 수동 빈 등록의 올바른 실무 운영 기준

어떤 경우에 의존자동 주입을 사용하고 어떤 경우에 수동으로 빈 등록을 해야 할까?

결론부터 말하면 스프링은 갈수록 자동주입을 선호하는 추세이다.
@Controller, @Service, @Repository처럼 계층에 맞추어 자동으로 스캔할 수 있도록 지원한다.

그럼 언제 수동으로 빈을 등록해야 할까?

애플리케이션은 크게 두 가지 종류로 나눌 수 있다.

  • 업무 로직 빈 : 컨트롤러, 서비스, 리포지토리 등이 모두 업무 로직이며 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 사용한다. 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

업무 로직의 경우 계층별로 유사한 패턴이 존재해서 문제 발생 시 원인을 파악하기가 비교적 쉽기 때문에 자동 주입을 권장한다.

기술 지원 로직의 경우 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 광범위하게 영향을 미친다. 또한 기술 지원 로직은 문제 발생 시 비교적 문제가 명확하게 드러나지 않기 때문에 이러한 로직들은 가급적 수동 빈 등록을 통해서 명확하게 나타낼 필요가 있다.

추가적으로 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보면 좋다.

예를 들어

@Configuration
class DiscountPolicyConfig {
	
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
    	return new FixDiscountPolicy();
    }
    
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
    	return new RateDiscountPolicy();
    }
    
}

이같이 DiscountPolicy를 다형성을 활용하여 설계하였다면 이에 대한 설정 정보를 한 눈에 보이도록 해주면 더 나은 가독성을 기대할 수도 있다.

물론 위처럼 빈 등록 자체를 Config에 담으면 빈 충돌이 일어날 수 있기 때문에 이에 대한 방안은 @Primary나 @Qualifier로 해결할 수 있을 것이다.

0개의 댓글