본문 바로가기
스프링

[스프링 핵심 4] 컴포넌트 스캔 & 의존관계 자동 주입

by 메이02 2024. 11. 2.

[섹션 7] 컴포넌트 스캔

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

지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 등을 통해서 직접 나열해야했다

⇒ 스프링빈이 늘어날수록 일일이 등록하기가 너무 귀찮다 😭

∴  스프링은  설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공함!

+ 스프링은 의존관계도 자동으로 주입하는 ` @Autowired ` 라는 기능도 제공함  

 

 

@ComponentScan 을 설정 정보에 붙여주면 됨 ➡️기존과 다르게 @Bean으로 등록한 클래스가 하나도 없다!
⚠️ 컴포넌트 스캔을 사용하면  @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, 기존 AppConfig 등도 같이 실행되버림 ⇒ excludeFilters  를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외함 

 

 

 

✅ 기존의 파일들에 @Configuration을 붙여서 스프링빈으로 등록해주고, @Autowired를 붙여서 의존관계도 자동으로 주입해준다 

⚠️ 기존 : @Bean 으로 직접 설정 정보를 작성하고, 의존관계주입도 클래스 안에서 해결해야했음

 

[테스트]

 

[동작 과정 그림]

 

 

 

 

2. 탐색 위치와 기본 스캔 대상

 

 

basePackages = "hello.core"  로 컴포넌트 스캔을 탐색하는 위치를 지정할 수 있다

 

 

 

basePackageClasses  로 패키지를 탐색 시작위치로 지정할 수 도 있다

 

 

만약 지정하지 않았다면, @ComponentScan이 붙은 클래스의 패키지가 자동으로 시작위치가 된다
(여기에서는 자동으로 hello.core이 시작위치가 될 것)

 

 

[컴포넌트 스캔 기본 대상]

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

 

3. 필터

✅ includeFilters : 컴포넌트 스캔 대상을 추가로 지정함
✅ excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정함

 

컴포넌트 스캔 대상에서 제외할 애노테이션
컴포넌트 스캔 대상에 추가 할 애노테이션

 

[ FilterType 옵션 ]

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    • ex) ` org.example.SomeAnnotation `
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    • ex) ` org.example.SomeClass `
  • ASPECTJ: AspectJ 패턴 사용
    • ex) ` org.example..*Service+
  • REGEX: 정규 표현식
    • ex) ` org\.example\.Default.* `
  • CUSTOM: ex) ` TypeFilter ` ` 이라는 인터페이스를 구현해서 처리 
    • ex ) org.example.MyTypeFilter
⚠️ 참고: @Component 면 충분하기 때문에, includeFilters를 사용할 일은 거의 없다

4. 중복등록과 충돌

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

 

자동 빈 등록 vs 자동 빈 등록

➡️ ` ConflictingBeanDefinitionException 예외 발생

 

수동 빈 등록  vs 자동 빈 등록

➡️ 이 경우 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 해버린다.)


[섹션 8] 의존관계 자동 주입

1. 다양한 의존관계 주입 방법

크게 4가지의 의존관계 주입

 

[생성자 주입]

  • 생성자를 통해서 의존관계를 주입받는 방법
  • 지금까지 우리가 진행했던 그 방법이다 !
  • 특징
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장됨
    • 불변, 필수 의존관계에 사용 ➡️ 즉, 값을 바꾸면 안되고, 값이 없어도 안됨 !
⚠️ 중요! 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 

 

[수정자 주입 (setter주입)]

  • 수정자 메서드를 통해서 의존관계를 주입하는 방법
  • 특징
    • 선택, 변경 가능성이 있는 의존관계에서 사용
      • @Autowired(required = false) 라고 하면 선택적으로 할 수 있다 ! (필수값이 아니니까)
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

 

 

⚠️ 참고: @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생
➡️ 주입할 대상이 없어도 동작하게 하려면  @Autowired(required = false) 로 지정하면 된다

 

 

[필드 주입]

  • 필드에 바로 주입하는 방법
  • 특징
    • 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점
    • DI 프레임워크가 없으면 아무것도 할 수 없다
    • 결론 : 사용하지 말자 !
    • 그렇지만, 사용가능한 코드들 
      • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
      • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용
✅ 참고 : 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. 컨테이너를 테스트에 통합한 경우에만 가능하다.

 

[일반 메서드 주입]

  • 일반 메서드를 통해서 주입
  • 특징
    • 한번에 여러 필드를 주입받을 수 있음
    • 일반적으로 잘 사용하지 않음

2. 옵션 처리

[자동 주입 대상을 옵션으로 처리하는 방법]

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

⇒  setNoBean1() 은  @Autowired(required=false) 이므로 호출 자체가 안된다.

 

3.  생성자 주입을 선택해라 !

☑️ 과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입 을 권장한다. 그 이유는 다음과 같다

 

[불변]

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

[누락]

if ) ❓ 만약 수정자 의존관계였다면?

 

⚠️ NPE(Null Point Exception)이 발생하는데, memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문이다.

 

생성자 주입을 사용하면 주입 데이터를 누락했을때, 컴파일 오류가 발생해서 어떤값을 필수로 주입해야하는지 알 수 있다 !

 

 

➕ 생성자 주입을 사용하면 필드에 ` final ` 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다 !

📌정리
☑️ 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징 을 잘 살리는 방법이기도 하다.
☑️ 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
☑️ 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는 게 좋다.

4. 롬복과 최신 트랜드

👩🏻 : 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하고… 필드 주입처럼 좀 편리하게 사용하는 방법은 없을까?

 

❗ 롬복 라이브러리를 사용하자 !

 

@RequiredArgsConstructor을 작성하고, 생성자 코드를 지워준다

(final이 붙은 필드를 모아서 생성자를 자동으로 만들어 주기 때문)

 

5. (문제 발생) 조회 빈이 2개 이상 

✅ @Autowired 는 타입(Type)으로 조회한다. 

≒ ac.getBean(DiscountPolicy.class)

 

⚠️ 타입으로 조회하면, 선택된 빈이 2개 이상일때 문제가 발생한다 !

➡️ NoUniqueBeanDefinitionException 오류가 발생

ex) FixDiscountPolicy , RateDiscountPolicy 둘 다 스프링 빈으로 설정했을 때 문제 발생함

6. (해결방법) @Autowired 필드명, @Qualifier, @Primary

 

(1) @Autowired 필드 명 매칭

  • 먼저 타입 매칭을 시도하고, 타입이 여러개라면 ⇒ 필드명 (or 파라미터명) 이 일치하는 것으로 매칭 시도 

(2) @Qualifier 사용

  • 추가 구분자를 붙여주는 방법 ( 빈이름을 변경하는게 아님!)

☑️ 주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다

📌정리
먼저 @Qualifier끼리 매칭 → 만약 해당되는 @Qualifier 찾지 못하면 같은 이름의 빈을 찾아서 매칭 → 이것도 없으면 NoSuchBeanDefinitionException 예외 발생

 

(3) @Primary

  • @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다

 

📌 @Primary, @Qualifier 활용
: 자주 쓰는 메인 DB 스프링빈과, 가끔 쓰는 서브 DB 스프링 빈이 있을 때, 메인 DB 스프링빈은 @Primary를 사용하고, 서브 DB 스프링빈은 @Qualifier 으로 명시적으로 획득하는 방법 사용

📌 우선 순위
: @Qualifier > @Primary

 

7. 애노테이션 직접 만들기

☑️ 위와 같이 @Qualifier("mainDiscountPolicy") 으로 적으면, 컴파일시 타입체크가 안된다 ⇒ 에노테이션을 만들어서 문제를 해결하자

 

MainDiscountPolicy 파일 생성

 

✅ @Qualifier대신 만들어둔 @MainDiscountPolicy를 사용

8. 조회한 빈이 모두 필요할 때, List , Map

📌상황
의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.
스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.

 

 

 

[로직 분석]

  • DiscountService는 Map으로 모든 rateDiscountPolicy 를 주입받는다 → 이때,  fixDiscountPolicy, rateDiscountPolicy가 주입된다
  • discount() 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy  스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy  스프링 빈을 찾아서 실행한다.

[주입 분석]

  • Map <String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List <DiscountPolicy>  : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

 

 

💡 참고 (스프링 컨테이너를 생성하면서 스프링 빈 등록하기)

new AnnotationConfigApplicationContext(AutoAppConfig.class,DiscountService.class);

(1) new AnnotationConfigApplicationContext() 를 통해 스프링 컨테이너를 생성한다

(2) AutoAppConfig.class,DiscountService.class 를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링빈으로 등록한다

 

9. 자동, 수동의 올바른 실무 운영 기준

👩🏻 : " 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 수동으로 주입해야 할까?  "

 

💡 결론 : 편리한 자동 기능을 기본으로 사용하자 !

  • 스프링 빈을 하나 등록할 때, @Component ` 만 넣어주면 끝나는 일을  @Configuration에 가서 @Bean 을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다
  • 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다
  • 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다 !!

 

👩🏻 : " 그럼 수동 빈 등록은 언제 사용하면 좋을까? " 

 

💡 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다

  • 애플리케이션은 업무 로직과 기술 지원 로직으로 나눌 수 있다
    • 업무로직 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등
    • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다
  • 업무 로직은 숫자도 매우 많고,  유사한 패턴이 있다 ⇒ 자동 기능을 적극 사용하는 것이 좋다
  • 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 어디가 문제인지 파악하기 어려움 ⇒ 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다

[섹션 9] 빈 생명주기 콜백

1. 빈 생명주기 콜백 시작

(1) 가상의 외부네트워크 연결 객체 만들기

(2) 테스트 코드

[결과]

☑️ 객체 생성하는 단계에는 url이 없고, 객체를 다 만든 후에 setUrl을 통해서 뒤늦게 url을 넣었기 때문

 

 

✅ 스프링빈은 "객체 생성 → 의존관계 주입" 이라는 라이프사이클을 가진다

⚠️ 초기화 작업은 의존관계 주입이 모두 완료된 후 호출되야하는데, 개발자가 의존관계 완료 시점을 어떻게 알수 있을까?

 

✅ 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다.

✅ 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

 

📌정리 : 스프링 빈의 이벤트 라이프 스타일
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백소멸전 콜백 → 스프링 종료

 

  • 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백 : 빈이 소멸되기 직전에 호출
⚠️ 참고 : 객체의 생성과 초기화를 꼭 분리하자!
생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우 에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다.

2. 스프링의 빈 생명주기 콜백 지원 방법

  1. 인터페이스 ( InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. @PostConstruct, @PreDestroy 애노테이션 지원

⬇️ 아래에서 알아보자 !

 

(1) 인터페이스 InitializingBean, DisposableBean

implements 받기
메서드 추가

[결과]

➡️ 초기화 메서드가 적절하게 호출되고, 소멸 메서드도 호출된 것을 확인할 수 있다

 

⚠️ 그러나, 초기화 소멸 인터페이스의 단점이 있다

  • 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다

인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법 들이 있어서 거의 사용하지 않는다.

 

(2)  빈 등록 초기화, 소멸 메서드 지정

(1) 설정정보에 초기화 소멸 메서드 지정

 

(2)  초기화, 소멸 메서드 만들기

 

 

📌 특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다

✅  종료 메서드 추론

  • @Bean의 destroyMethod` 속성에는 아주 특별한 기능이 있다.
  • 라이브러리는 대부분 ` close` , ` shutdown` 이라는 이름의 종료 메서드를 사용한다.
  • @Bean의 ` destroyMethod` 는 기본값이 ` (inferred)` (추론)으로 등록되어 있다.
  • 이 추론 기능은 ` close` , ` shutdown` 라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다.
  • 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능을 사용하기 싫으면 ` destroyMethod=""` 처럼 빈 공백을 지정하면 된다.

 

(3)  애노테이션 @PostConstruct, @PreDestroy

 

 

📌 @PostConstruct, @PreDestroy 애노테이션 특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 ` javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 ` 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

💡결론

✅ @PostConstruct, @PreDestroy 애노테이션을 사용하자 

➕ 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod  , destroyMethod를 사용하자