[섹션 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") 으로 적으면, 컴파일시 타입체크가 안된다 ⇒ 에노테이션을 만들어서 문제를 해결하자
✅ @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. 스프링의 빈 생명주기 콜백 지원 방법
- 인터페이스 ( InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestroy 애노테이션 지원
⬇️ 아래에서 알아보자 !
(1) 인터페이스 InitializingBean, DisposableBean
[결과]
➡️ 초기화 메서드가 적절하게 호출되고, 소멸 메서드도 호출된 것을 확인할 수 있다
⚠️ 그러나, 초기화 소멸 인터페이스의 단점이 있다
- 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다
⇒ 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법 들이 있어서 거의 사용하지 않는다.
(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를 사용하자
'스프링' 카테고리의 다른 글
[HTTP] 웹 기본 지식 3 (1) | 2024.11.16 |
---|---|
[HTTP] 웹 기본 지식 2 (0) | 2024.11.12 |
[HTTP] 웹 기본 지식 1 (1) | 2024.11.10 |
[스프링 핵심 3] 스프링 컨테이너와 빈 & 싱글톤 컨테이너 (2) | 2024.10.16 |
[스프링 핵심 2] 스프링 핵심 원리 이해 (2) | 2024.10.15 |