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


스프링 빈을 등록할 때 Java 코드의 @Bean이나 XML의 등을 이용해 설정 정보에 직접 등록할 빈을 명시했습니다. 그런데, 이렇게 등록해야할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 파일도 커지는 문제가 발생합니다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공합니다.

 

탐색할 패키지의 시작 위치 지정
모든 Java 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸립니다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있습니다.

@ComponentScan(
            basePackages = "com.demo.core",
            basePackageClasses = AutoConfig.class
)
  • basePackages: 탐색할 패키지의 시작 위치를 지정합니다. (이 패키지를 포함해서 하위 패키지를 모두 탐색)
    • basePackages = {”com.demo.aa”, “com.demo.bb”} <- 여러 시작 위치를 지정할 수도 있습니다.
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정합니다.
    • 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됩니다.


basePackage 관련 권장하는 방법

개인적(김영한님)으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것.
최근 Spring Boot도 이 방법을 기본으로 제공합니다.


컴포넌트 스캔 대상

컴포넌트 스캔은 @Component뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함합니다.

  • @Component: 컴포넌트 스캔에서 사용
  • @Controller: 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비즈니스 로직에서 사용
  • @Repository: 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

 

위 애너테이션들을 보면 코드가 이런 식으로 되어있습니다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service { ... }

그런데 사실 애너테이션에는 상속관계라는 것이 없다. 그래서 위 코드처럼 애너테이션이 특정 애너테이션을 들고 있는 것을 인식할 수 있는 것은 Java 언어가 지원하는 기능이 아니고, Spring이 지원하는 기능입니다.

컴포넌트 스캔의 용도뿐만 아니라 다음 애너테이션이 있으면 Spring은 부가 기능을 수행합니다.

  • @Controller: Spring MVC 컨트롤러로 인식
  • @Repository: Spring 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해줍니다.
  • @Configuration: Spring 설정 정보로 인식하고, Spring 빈이 싱글톤을 유지하도록 추가 처리를 합니다.
  • @Service: 사실 @Service는 특별한 처리를 하지 않습니다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 됩니다.

1.1 필터


아래 두 가지 필터가 존재합니다.

  • includeFilters
  • excludeFilters

MyIncludeComponentMyExcludeComponent는 다음과 같습니다.

@Target(ElementType.TYPE) // 해당 애너테이션이 어디에 붙을건지를 명시합니다. (클래스, 필드, 메서드 등등)
@Retention(RetentionPolicy.RUNTIME) // 해당 애너테이션을 런타임에 메모리에 올리도록 설정?
@Documented
public @interface MyIncludeComponent{}

@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent{}
@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                            type = FilterType.ANNOTATION, // ANNOTATION이 디폴트값.
                            classes = MyIncludeComponent.class)),
        excludeFilters = @ComponentScan.Filter(
                            type = FilterType.ANNOTATION,
                            classes = MyExcludeComponent.class))
)

FilterType 옵션
FilterType 옵션은 5가지가 있습니다.

  • ANNOTATION: 기본값, 애너테이션을 인식해서 동작합니다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작합니다.
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..*Service+
  • REGEX: 정규 표현식
    • ex) org\.example\.Default.*
  • CUSTOM: TypeFilter 라는 인터페이스를 구현해서 처리합니다.
    • ex) org.example.MyTypeFilter

 

1.2 중복 등록과 충돌


컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까요?
다음 두 가지 상황이 있습니다.

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

 

자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이때 이름이 같은 컴포넌트가 있을 경우 스프링은 예외를 던집니다. ConflictingBeanDefinitionException 예외 발생

 

수동 빈 등록 vs 자동 빈 등록
예전에는 수동 빈 등록(@Bean으로 직접 등록하는거)을 우선으로 오버라이딩했는데, 최근에는 예외가 발생하도록 바꼈습니다.

2. 의존관계 자동주입


의존관계 주입은 크게 4가지 방법이 있습니다.

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

  • 이름 그대로 생성자를 통해서 의존 관계를 주입받는 방법입니다.
  • 지금까지 우리가(김영한 강의) 진행했던 방법이 바로 생성자 주입입니다.
  • 특징: 생성자 호출시점에 딱 1번만 호출되는 것이 보장됩니다. ‘불변, 필수’ 의존관계에 사용합니다.

프로그래밍은 제약을 두는 것이 후에 관리 측면에서 용이합니다. setter로 다 열어두면 언제 어디서 수정될지 가늠이 안되기 때문입니다.

생성자 주입에서 중요한 점은 !
생성자가 1개일 때는 @Autowired를 생략할 수 있습니다.
만약 생성자가 2개 이상이면 @Autowired 를 명시해줘야합니다.

Spring Framework는 실행될때 크게 두 가지 작업을 합니다.

  • 스프링 빈을 컨테이너에 등록
  • 의존관계를 주입

 

필드 주입
필드에 @Autowired 를 붙여 바로 주입하는 방법입니다.
특징은 다음과 같습니다.

  • 코드가 간결해서 많은 개발자들을 유혹하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있습니다.
  • DI 프레임워크가 없으면 아무것도 할 수 없습니다.
  • 결론, 사용하지 말자!
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드에 사용하거나,
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하자!

옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있습니다.
그런데 @Autowired만 사용하면 required 옵션 기본값이 true 로 되어있어서 자동 주입 대상이 없으면 오류가 발생합니다.

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같습니다.

  • @Autowired(required=false): 자동 주입할 대상이 없으면 setter 메서드 자체가 호출 안됩니다.
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력됩니다.
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력됩니다.

생성자 주입을 선택해라!
과거에는 setter 주입과 필드 주입을 많이 사용했지만, 최근에는 Spring을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장합니다.
그 이유는 다음과 같습니다.

“불변”

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없습니다.
    오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됩니다.(불변해야 한다.)
  • setter 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 합니다.
    이러면 누군가 실수 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아닙니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. (따라서 불변하게 설계 가능)

2.1 자동 주입하기 위해 조회되는 빈이 2개 이상일 때


@Autowired는 타입으로 빈을 조회해서 가져옵니다.
만약 아래와 같이 FixDiscountPolicy와 RateDiscountPolicy 두 개를 컴포넌트로 등록하고,

@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }

@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }

아래와 같이 DiscountPolicy 타입에 @Autowired를 하여 자동 주입을 하면 어떻게 될까요?

// 아래와 같이 있으면 ac.getBean(DiscountPolicy.class) 로 빈을 조회해서 가져옵니다.
@Autowired
private DiscountPolicy discountPolicy;

DiscountPolicy 타입으로 조회되는 빈이 2개이기 때문에 Spring은 어떤 빈을 주입해야할지 알지 못합니다.
이때 스프링은 NoUniqueBeanDefinitionException 예외를 던집니다.

위와 같은 상황일 때 해결 방법은 세 가지가 있습니다.

  • @Autowired 필드 명 매칭
  • @Qualifier@Qualifier 끼리 매칭 → 빈 이름 매칭
  • @Primary 사용

 

@Autowired는 타입 매칭을 시도하고, 이때 빈이 여러 개 있으면 필드명, 파라미터명으로 한번 더 조회합니다.
여기서 파라미터명이란 생성자 주입을 사용했을 때 파라미터명을 의미합니다.

@Qualifier는 추가 구분자를 붙여주는 방법입니다.
주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아닙니다!

@Component
@Qualifier("rate")
public class RateDiscountPolicy { ... }

위와 같이 컴포넌트에 @Qualifier를 통해 추가적인 정보를 제공하고,

@Autowired
public OrderServiceImpl(@Qualifier("rate") DiscountPolicy discountPolicy) { ... }

위와 같이 파라미터 앞에 주입할 빈에 대한 추가적인 정보를 제공합니다.

@Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 빈이 여러 개 매칭되면 @Primary 가 우선권을 가진다.
(자주 사용됨)

@Component
@Primary
public class RateDiscountPolicy { ... }

애너테이션 직접 만들기
@Qualifier(”mainDiscountPolicy”) 이렇게 문자를 적으면 컴파일시 타입 체크가 안됩니다.
이때 다음과 같이 애너테이션을 만들어서 문제를 해결할 수 있습니다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy { ... }
@Component
@MainDiscountPolicy
public class RateDiscountPolicy { ... }
@Autowired
public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) {..}

 

수동 빈 등록은 언제 사용하면 좋을까요?
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있습니다.

  • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직입니다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경됩니다.
  • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용됩니다.
    DB 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들입니다.
  • 업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느 정도 유사한 패턴이 있습니다.
    이런 경우 자동 빈 등록 기능(@Controller, @Service, @Repository, @Component)을 적극 사용하는 것이 좋습니다. 이러면 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽습니다.
  • 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미칩니다.
    그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많습니다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋습니다.