5) RateDiscountPolicy 를 의존관계로 주입하려는 클래스 OrderServiceImpl코드를 열어보자.
DiscountPolicy 에 @Qualifier 를 지정하되, 방금 만든 @MainDiscountPolicy를 쓰자.
6) 로그를 보면 정상적으로 스프링 빈이 등록되고 의존관계도 주입됬음을 확인할 수 있다.
Creating shared instance of singleton bean 'orderServiceImpl'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'
OrderServiceImpl 싱글톤 빈이 생성됬고,
생성자를 통해 memoryMemberRepository와 rateDiscountPolicy 빈이 의존관계 주입이 됬다.
다음 강의에서는조회한 빈이 모두 필요할 때 List, Map 으로 처리하는 방법을 배운다.
1. 조회할 빈이 2개 이상일 때의 문제를 어떻게 처리해야 할지 배운다. 2. @Autowired 필드 명을 매칭, @Qualifier, @Primary 사용법을 배운다.
'의존관계 자동 주입' 목차
1. 다양한 의존관계 주입 방법
2. 옵션 처리
3. 생성자 주입을 선택해라!
4. 롬복과 최신 트랜드
5. 조회 빈이 2개 이상 - 문제 (이번 포스팅)
6. @Autowired 필드 명, @Qualifier, @Primary
7. 애노테이션 직접 만들기
8. 조회한 빈이 모두 필요할 때, List, Map
9. 자동, 수동의 올바른 실무 운영 기준
5. 조회할 빈이 2개 이상일 때의 문제 해결하기
@Autowired 는 타입으로 빈을 조회한다.
1) 만약 타입이 같은 빈이 2개가 조회되면 문제가 생길까?
인터페이스 DiscountPolicy의 구현체 2개가 있다. RateDiscountPolicy와 FixDiscountPolicy 는 타입이 같다.
RateDiscountPolicy 만 컴포넌트 스캔으로 스프링 빈 등록했었는데. FixDiscountPolicy도 스프링 빈 등록하려고 한다.
스프링 빈 등록을 위해 컴포넌트 스캔을 하자.
2) orderServiceImple 에서 의존성 예외 발생했는데, DiscountPolicy 싱글 빈이 매칭되기를 기대했는데, 2개가 발견됬기 때문이다.
beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'orderServiceImpl' Unsatisfied dependency
nested exception is NoUniqueBeanDefinitionException:
No qualifying bean of type 'DiscountPolicy' available:
expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
@Autowired 로 의존관계를 주입하려고 DiscountPolicy 타입의 스프링 빈을 뒤져봤더니
1개 매칭을 기대했는데, 2개의 스프링 빈이 RateDiscountPolicy와 FixDiscountPolicy 발견되서 에러가 났다.
3) 하위 타입인 fixDiscountPolicy 나 rateDiscountPolicy 를 적어야 할까? 아니다. 하위타입에 의존하면 DIP를 위반한다.
다음 시간에 해결책을 배워보자.
6. @Autowired 필드 명, @Qualifier, @Primary
타입이 같은 빈이 여러개 조회되면, 3가지 방법으로 해결할 수 있다.
1) @Autowired 필드 명을 매칭한다
@Autowired 의 스프링 빈 조회 순서-> 1. 타입 매칭, 2. 필드 명 또는 파라미터 명을 매칭
@Autowired 는 가장 먼저 스프링 빈의 타입을 매칭해보고. 빈이 2개 이상 조회되면 필드 명을 확인해서 매칭되는 하나를 조회한다.
이전 코드
@Autowired 필드 명을 매칭시키도록 변경한 코드
2) @Qualifier 추가 구분자를 붙여주기
추가적인 방법을 제공하는 것이지 빈 이름을 변경하는건 아니다.
예를 들어, RateDiscountPolicy 에 @Qualifier("mainDiscountPolicy")를 지정하고,
FixDiscountPolicy 에는 @Qualifier("fixDiscountPolicy")를 지정한다.
생성자 주입을 할 때 @Qualifier를 써주면 된다.
만약 해당 @Qualifier를 못찾으면 스프링은 그 이름으로 등록된 빈을 찾아본다.
@Qualifier의 스프링 빈 조회 순서-> 1. @Qualifier 끼리 매칭, 2. 빈 이름 매칭
@Qualifier 는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다. 헷갈리게 하는 요소를 만들지 말자.
3) @Primary 우선순위를 지정하기 [ 편하고 자주 사용한다 ]
rate 할인정책을 항상 먼저 적용하고 싶다면, RateDiscountPolicy에 @Primary 를 붙인다.
생성자에는 DiscountPolicy 타입 그대로 둔다.
[ 참고 ] @Primary 는 언제 사용할까?
예를 들어, 메인 DB connection 을 주로 사용하고, 백업용 DB connection 이 있는 상황에서,
메인 DB connection 에 @Primary 를 걸어두자고 팀내에서 룰을 지정할 수 있다.
* 로그에서 의존관계 주입을 확인할 수 있다. memoryMemberRepository 이름의 빈이 생성자를 통해 자동주입됨.
Creating shared instance of singleton bean 'orderServiceImpl'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'
6) 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자.
@ComponentScan은 @Component가 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다.
스프링 빈의 기본 이름은 클래스명을 쓰는데 앞글자만 소문자로 바꿔서 쓴다. 직접 빈 이름을 지정할 수 도 있다.
MemberServiceImpl - > memberServiceImpl
생성자에 @Autowired 를 붙이면, 스프링 컨테이너가 자동으로 해당 빈을 찾아서 주입한다.
기본 조회 전략은 타입이 같은 빈을 찾는 것이다.
아래 코드를 보면 MemberRepository 빈을 의존관계로 주입해야 하는 상황이다.
MemberRepository와 타입이 같은 것을 찾는다.
memoryMemberRepository 가 MemberRepository의 구현체니까 타입이 맞다.
그래서 memoryMemberRepository를 꺼내서 주입한다.
getBean(MemberRepository.class)와 동일하다고 이해하면 된다.
2. 탐색 위치와 기본 스캔 대상
컴포넌트 스캔을 시작하는 위치와 대상에 대해 알아보자.
1) 탐색할 패키지의 시작 위치 지정 : 탐색 위치를 지정 할 수 있다!
AutoAppConfig.class 파일을 열어보자. 어떤 패키지부터 탐색할 지 basePackages 를 지정할 수 있다.
모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문이다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
현재 프로젝트 네비게이션은 아래와 같이 여러개의 패키지로 구성되어 있다.
member 패키지를 basePackages 로 지정하고 컴포넌트 스캔하면 어떻게 될까?
basePackages = member 패키지를 포함해서 하위 패키지를 모두 탐색한다.
그래서 memberServiceImpl, memoryMemberRepository 만 스프링 빈으로 등록됬다.
orderServiceImpl같은 다른 패키지의 빈은 제외됬다.
2) 탐색 위치를 지정하지 않으면 어떻게 될까?
@ComponentScan이 붙은 설정 정보 클래스의 위치 부터 하위 패키지를 모두 탐색한다.
hello.corebasic 패키지 하위의 모든 자바 파일에 @Component가 붙었는지 검사한다.
3) 권장하는 방법 : @ComponentScan이 붙은 설정 정보 클래스의 위치를 최상단에 두자 !
최근 스프링부트도 이 방법을 기본으로 제공한다.
예를 들어, 패키지가 아래와 같은 구조라면, hello.corebasic 여기가 프로젝트 시작 루트가 된다. (지금 AutoAppConfig.class위치)
프로젝트 시작 루트에 @ComponentScan 를 붙인 메인 설정 정보 클래스를 두고 basePackage 지정은 생략한다.
이렇게 하면 hello.corebasic 를 포함한 하위는 모두 컴포넌트 스캔의 대상이 된다.
[ 참고 ] 스프링 부트를 사용하는 경우
스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다.
스프링 부트 프로젝트를 만들면 main() 메서드가 있는 프로젝트명으로 된 클래스가 자동 생성되어 있다.
이 클래스에 이미 @SpringBootApplication 가 붙어있다.
@SpringBootApplication 애노테이션이 뭔지 싶어서 ctrl 을 누르고 들어가보면, @ComponentScan 이 붙어있음을 확인할 수 있다.
그래서 스프링 부트를 실행하면 hello.corebasic 위치 부터 하위까지 다 스프링 빈이 등록된다.
스프링 부트를 쓰면 @ComponentScan 을 따로 달아줄 필요가 없다.
4) 컴포넌트 스캔의 기본 대상
@ComponentScan뿐만 아니라 아래 내용도 컴포넌트 스캔 대상에 포함된다.
* @Component
* @Controller
* @Service
* @Repository
* @Configuration
스프링 MVC를 다뤄봤다면 몇개는 익숙한 애노테이션일 것이다. 해당 클래스의 소스코드를 열어보면 @Component를 포함하고 있음을 확인할 수 있다.
[ 참고 ] 애노테이션에는 상속관계 라는 것이 없다.
애노테이션이 특정 애노테이션을 인식할 수 있는 것인 자바 언어가 지원하는게 아니라, 스프링이 지원하는 기능임을 인지하고 넘어가자.
컴포넌트 스캔의 대상이 되면서도 아래의 애노테이션이 있으면 스프링은 부가 기능을 수행한다.
예를 들어, @Controller 라면, MVC 컨트롤러로 인식한다. @Repository라면 데이터 계층의 예외를 스프링 예외로 변환해준다.
1. 스프링 빈 조회 방법을 배운다. 2. BeanFactory와 ApplicationContext 의 차이를 배운다.
'스프링 컨테이너와 스프링 빈'목차
1. 스프링 컨테이너 생성
2. 컨테이너에 등록된 모든 빈 조회
3. 스프링 빈 조회 - 기본
4. 스프링 빈 조회 - 동일한 타입이 둘 이상 (이번 포스팅)
5. [ 중요 ] 스프링 빈 조회 - 상속 관계
6. BeanFactory와 ApplicationContext
7. 다양한 설정 형식 지원 - 자바 코드, XML
8. 스프링 빈 설정 메타 정보 - BeanDefinition
4. 스프링 빈 조회 - 동일한 타입이 둘 이상
1) 타입으로 조회 시, 같은 타입이 2개 이상 있으면 중복 오류가 발생한다.
임시로 MemberRepository 타입 빈이 2개 있는 SameBeanConfig.class 설정 파일을 만들었다.
여기서 MemberRepository.class 타입 빈을 가져오면?
아래와 같은 NoUniqueBeanDefinitionException 이 발생한다. 설명이 굉장히 친절하다.
싱글 매칭 빈을 기대했는데, 2개 였다는 내용.
org.springframework.beans.factory.NoUniqueBeanDefinitionException
: No qualifying bean of type 'hello.corebasic.member.MemberRepository' available
: expected single matching bean but found 2: memberRepository1,memberRepository2
이렇게 특정 에러가 발생하는지 확인하려면 assertThrow와 Exception 클래스명을 써서 확인한다.
2) 타입으로 조회 시, 같은 타입이 2개 이상 있으면 빈 이름을 지정하면 된다!
빈 이름은 중복 불가다. 빈 이름을 안겹치게 지어서 조회하면 된다.
3) 특정 타입을 모두 조회하기
getBeansOfType()메서드로 Map<이름, 타입> 특정 타입의 모든 빈을 모두 조회할 수 있다.
나중에 @Autowired같이 자동으로 주입되는 기능을 배울 때 이런식으로 적용이 된다.
ac.getBeansOfType(MemberRepository.class);
[ 중요 ] 5. 스프링 빈 조회 - 상속 관계
1) 부모 타입으로 빈을 조회하면, 자식 타입도 함께 조회한다.
스프링 빈 대 원칙 하나는 "스프링 빈 하나 조회하면? 그 빈의 자식을 싹다 조회한다. "
그래서 모든 자바 객체의 최고 부모인 Object 타입. Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
테스트 코드로 확인하자. TestConfig 설정파일을 하나 만들자. DiscountPolicy 이름으로 생성될 빈이 2개다! 반환되는 생성자만 다르다.
2) 부모 타입을 조회하면, 자식까지 다 딸려서 조회되니까 자식이 둘 이상인 경우 오류가 발생한다.
이럴 땐 이름을 지어서 구분하면 된다.
3) 부모 타입을 조회 시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다.
rateDiscountPolicy 특정 빈 이름으로 조회하자.
4) 특정 하위 타입으로 조회할 수 있다.
구체적인 타입으로 딱 지정하면 조회할 수 있지만 안 좋은 방법이다.
5) 부모 타입으로 모두 조회해보자.
할인 정책 인터페이스를 구현한 구현체 클래스 2개가 조회 된다.
rateDiscountPolicy, fixDiscountPolicy 자식 타입 스프링 빈 2개가 출력됨을 확인할 수 있다.
[ 참고 ] 실무에서 테스트 코드를 작성할 때는 이렇게 출력물을 만들면 안된다
지금은 공부용이라서 system.out.print를 하지만, 실무에서 테스트 코드를 작성할 때는 이렇게 출력물을 만들면 안된다.
테스트 통과/실패 여부만 판단하도록 작성하는게 좋다.
6) 최고 부모 타입인 Object 로 조회해보자.
모든 자바 객체의 부모는 Object 라서. 모든 스프링 빈이 조회된다.
7)부모 타입으로 조회하면, 자식 타입도 함께 조회한다
실무에서는 ApplicationContext를 통해 getBean() 빈을 조회할 일이 거의 없다. 스프링 컨테이너가 자동으로 의존관계를 주입해주기 때문이다.
굳이 이렇게 배운 이유는, 빈 조회는 스프링의 기본 기능이기도 하고 아주 가끔 순수한 자바 애플리케이션에서 스프링 컨테이너를 생성해서 사용할 일이 있기 때문이다. 그리고 부모 타입으로 조회 시, 자식 타입 빈까지 조회됨을 알고 있어야 자동 의존관계 주입을 배울 때 잘 사용할 수 있다.
6. BeanFactory 와 ApplicationContext
BeanFactory 와 ApplicationContext에 대해서 알아보자. 아래 계층 구조를 보자.
최상위에 있는 인터페이스 BeanFactory 가 있다.
최상위에 있는 인터페이스 BeanFactory를 상속받은 인터페이스 ApplicationContext 가 있다.
ApplicationContext 는 BeanFactory에 부가 기능을 더한 것이다.
BeanFactory
BeanFactory 는 스프링 컨테이너의 최상위 인터페이스다.
스프링 빈을 관리하고 조회하는 역할을 담당한다. 지금까지 빈 조회에 사용한 getBean() 을 BeanFactory이 제공한 것이다.
ApplicationContext
BeanFactory의 모든 기능을 상속 받아서 제공한다.
BeanFactory와 ApplicationContext의 차이는 뭘까?
애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고 수 많은 부가기능이 필요하다.
ApplicationContext는 아래와 같이 여러 인터페이스를 상속받아서 국제화, 이벤트, 리소스 조회 등의 기능을 제공한다.
우리는 BeanFactory를 직접 사용할 일은 거의 없고, 빈 관리와 부가기능이 포함된 ApplicationContext를 사용한다.
AppConfig에 '구성 정보'를 의미하는 @Configuration 애노테이션을 붙인다.
DI 하는 메서드에 @Bean애노테이션을 붙인다.
2) ApplicationContext에 Bean을 등록
테스트를 위해 작성했던 MemberApp를 열어보자.
기존에는 AppConfig 에서 직접 필요한 객체를 꺼냈었다. 이제 스프링을 쓰자.
스프링은 ApplicationContext 로 시작한다. 이것이 객체(Bean)을 관리해주는 스프링 컨테이너다.
AppConfig.class 를 파라미터로 넘겨서 ApplicationContext를 생성하자.
AppConfig에 있는 설정 정보를 가지고 Bean을 스프링 컨테이너에 넣고 관리해준다.
객체(Bean)의 이름은 각각의 메서드 이름으로 붙여진다. ex) memberService 빈, memberRepository 빈
memberService 객체가 필요할 때 AppConfig에서 꺼내지 않고, 스프링 ApplicationContext에서 꺼낸다.
스프링 컨테이너에 빈으로 등록된 인스턴스가 로그에 뜬다!
3) OrderApp도 스프링으로 바꿔보자.
AppConfig.class 를 파라미터로 넘겨서 ApplicationContext를 생성하자.
ApplicationContext 에서 memberService와 orderService를 꺼낸다.
4) 스프링 컨테이너 (== DI 컨테이너)
ApplicationContext를 스프링 컨테이너라 한다.
기존에는 개발자가 AppConfig 에 객체를 생성하고 직접 DI를 구현했다.
이제 스프링 컨테이너가 @Configuration 애노테이션이 붙은 AppConfig를 설정(구성)정보로 사용한다. 여기서 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 모두 등록한다. 이렇게 스프링 컨테이너에 등록된 메서드를 스프링 빈이라고 한다.
스프링 빈은 @Bean애노테이션이 붙은 메서드 이름을 스프링 빈의 이름으로 사용한다.
스프링 빈은 applicationContext.getBean() 메서드로 찾을 수 있다.
코드가 더 복잡해진 것 같은데. 스프링 컨테이너를 사용하면 어떤 장점이 있을까 ? 다음 시간에 장점을 배우자.