Spring Boot의 DI(Dependency Injection)

D A S H B O A R D
D E V E L O P
S E C U R I T Y
 DI란 무엇인가?
 DI의 필요성
 동작 방식 - 컴포넌트 스캔
 동작 방식 - 자바 코드로 등록
 다양한 의존관계 주입 방법
 Lombok 라이브로리
 @Autowired
Reference

 DI란 무엇인가?

DI는 의존성 주입(Dependency Injection)의 약자입니다. 객체 지향 프로그래밍에서 클래스들은 서로 의존 관계를 갖습니다. 하나의 클래스가 다른 클래스의 객체를 사용하는 경우 그 클래스는 의존하고 있는 클래스입니다. DI는 이러한 의존 관계를 느슨하게 만들어주는 방식
스프링에의해 스프링이 관리하고 있는 Bean으로 이용해 의존성을 설정해주기 때문에 Dependency Injection이라고 하고 이는, @Autowired 어노테이션 아래에서 수행된다.(자세한 내용은 하단 참고)
DI를 통해 SOLID에서 문제가 되던, 추상화 객체 및 구현 객체 모두에 의존하는 문제를 해결해준다. → 즉, DI를 통해 SOLID의 DIP와 OCP 원칙을 만족시킬 수 있다.

 DI의 필요성

DI를 사용하면 의존 관계가 느슨해지므로 기능의 변경이나 추가가 편리해집니다. 또한, 코드의 재사용성이 높아지고 유지보수성이 좋아짐
예를 들면, ‘김영한의 스프링 입문 - 코드로 배우는 스프링부트, 웹 MVC, DB접근 기술’에서와 같이 우리가 특정 DB를 선정하지 않았을 때 미리 interface를 통해 추상화여 추후에 DB가 선택되었을 때 경우 등에서 유지보수하기가 쉬워진다.

 동작 방식 - 컴포넌트 스캔

스프링을 실행하면 스프링 컨테이너라는 통이 생긴다. 이 때 @ComponentScan을 설정 정보에 추가해주면@Component 라는 어노테이션이 있으면 스프링이 이 객체를 생성해 컨테이너에 넣어두고 관리한다. 이 때 스프링에 의해 관리되는 객체를 Spring Bean이라고 한다. 그리고 이러한 과정을 컴포넌트 스캔이라고 한다.
@Controller , @Service, @Repository 가 있을 경우에도 스프링 컨테이너에 들어가는데, 이는 3가지 어노테이션 모두에 @Component라는 어노테이션이 내장되어 있기 때문이다.
Component Scan의 위치는 기본적으로 main method가 들어 있는 Class가 포함된 package부터 히위폴더들이다. (다른 위치도 컴포넌트 스캔이 가능하도록 할 수 있긴 함)

1. 동작방식 상세

1. @ComponentScan

@ComponentScan@Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.
이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
빈 이름 기본 전략: MemberServiceImpl 클래스 ⇒ memberServiceImpl
빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 → @Component("memberService2") 이런식으로 이름을 부여하면 된다

2. @Autowired 의존관계 자동 주입

생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당스프링 빈을 찾아서 주입한다.
이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
@Autowired는 의존관계를 자동으로 주입
getBean(MemberRepository.class) 와 동일하다고 이해 (만약 Constructor가 한개라면 생략도 가능!)

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

1. 탐색할 패키지의 시작 위치 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
@ComponentScan( basePackages = "hello.core", }
Java
복사
basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.
basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

2. 권장하는 방법

패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것
예를 들어서 프로젝트가 다음과 같이 구조가 되어 있으면
com.hello com.hello.serivce com.hello.repository
Plain Text
복사
com.hello 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략
이렇게 하면 com.hello 를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다.
프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다
컴포넌트 스캔은 @ComponentScan 이라는 어노테이션을 붙이지 않는 프로젝트가 많은데 어디다가 설정하나요? ⇒ Spring Boot에서는 처음 프로젝트 생성 시 main 메서드에 @SpringBootApplication 어노테이션이 붙어 있는데 안을 들여다 보면 @ComponentScan 어노테이션이 내장되어 있기 때문에 따로 설정을 하지 않아도 무방하다. ⇒ 이것이 관례이다.
@Controller, @Service, @Repository, @Configuration은 정형화되어 있는 패턴이다. ⇒ 해당 어노테이션들에는 내부에 @Component를 포함하고 있기 때문에 컴포넌트 스캔 이외의 부가적인 기능 제공한다.
@Controller : 외부 요청 받기
@Service : Controller를 통해 받은 외부 요청을 비즈니스 로직에 따라 처리
@Repository : 비즈니스 로직에 따라 처리된 데이터를 저장
@Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리

3. 필터

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

1. 컴포넌트 스캔 대상에 추가할 애노테이션

package hello.core.scan.filter; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyIncludeComponent { }
Java
복사

3. 컴포넌트 스캔 대상에 추가할 클래스

@MyIncludeComponent 적용
package hello.core.scan.filter; @MyIncludeComponent public class BeanA { }
Java
복사

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

package hello.core.scan.filter; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyExcludeComponent { }
Java
복사

4. 컴포넌트 스캔 대상에서 제외할 클래스

@MyExcludeComponent 적용
package hello.core.scan.filter; @MyExcludeComponent public class BeanB { }
Java
복사

5. TestCode

@Test void filterScan() { ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class); BeanA beanA = ac.getBean("beanA", BeanA.class); assertThat(beanA).isNotNull(); assertThrows( NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class) ); } @Configuration @ComponentScan( // type = FilterType.ANNOTATION, 은 기본값 includeFilters = @Filter(classes = MyIncludeComponent.class), excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) ) static class ComponentFilterAppConfig { }
Java
복사
@Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다. ⇒ 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장
@ComponentScan( // type = FilterType.ANNOTATION, 은 기본값 includeFilters = @Filter(classes = MyIncludeComponent.class), excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) )
Java
복사
includeFiltersMyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
excludeFiltersMyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.
FilterType 옵션
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

4. 중복 등록과 충돌

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

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다. ConflictingBeanDefinitionException 예외 발생

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

이 경우 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 해버린다.)
수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러(@SpringBootApplication 가 붙어 있는 main 메서드를 실행해야 볼 수 있음)
에러 메시지 : Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true ⇒ application.properties 파일에서 spring.main.allow-bean-definition-overriding=true 을 넣어주면 Overriding이 가능하긴 하다.

5. 예시

Spring Bean을 이용하지 않는 코드

@Controller public class MemberController { private final MemberService memberService = new MemberService(); }
Java
복사
MemberService를 여러 Controller에서 사용한다 가정했을 때 계속해서 새로운 객체를 생성해주는 것은 좋지 못한 코드이다.
그렇기에 Spring Bean을 이용하여 Spring이 관리해주는 하나의 객체(Bean)을 이용하여 사용해주는 것이 유지보수 등 다방면으로 좋음

Spring Bean을 이용하는 코드

@Controller public class MemberController { private final MemberService memberService; @Autowired public MemberController(MemberService memberService) { this.membverService = memberService; } }
Java
복사
Spring Container에 등록할 경우 딱 하나의 Bean만 생성되는데 이때 여러 부가적인 강점을 가질 수 있다.
@Autowired 라는 태그가 있다면 Spring이 Spring Container에 있는 memberService라는 Bean을 가져다가 매칭 시켜준다.
하지만 아직 MemberService라는 객체를 Spring Container에 넣어주지 않았기 때문에 위의 코드만으로는 MemberService를 찾을 수 없다는 오류를 내뱉는다.
그렇기 때문에, MemberService 객체로가 @Service 라는 어노테이션을 추가해준다.

MemberService

memberRepository에 대한 의존성이 또 존재하기 때문에 memberRepository에서도 어노테이션을 추가해주어야 함
@Service public class MemberService { private final MemberRepository memberRepository; @Autowired public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } }
Java
복사

MemoryMemberRepository

Repository이기 때문에 @Repository 어노테이션을 추가
@Repository public class MemoryMemberRepository implements MemberRepository{}
Java
복사

 동작 방식 - 자바 코드로 등록

기본적인 원리는 컴포넌트 스캔과 같다. 하지만 @Component 어노케이션을 찾는 컴포넌트 스캔과 달리 자바 코드로 등록할 경우 @Configuration@Bean 이라는 어노테이션을 가지고 직접 등록하게 된다.
Controller, Service, Repository와 같이 정형화되어 있는 패턴 같은 경우는 컴포넌트 스캔을 이용하고 정형화되지 않거나 상황에 따라 구성 클래스를 변경해야 하면 자바 코드를 이용하여 스프링 빈을 등록하는 것이 좋다.

SpringConfig.class

해당 Config 파일을 main method가 잇는 package 위치와 동일하게 생성해준다.
@Configuration public class SpringConfig { @Bean public MemberService memverService(){ return new MemberService(memberRepository()); } @Bean public MemberRepository memberRepository(){ return new MemoryMemberRepository(); } }
Java
복사
이렇게 되면 스프링 컨테이너에 스프링 Bean을 등록하는 과정은 끝이 나게된다. 이 후 @Autowired를 통해 DI가 수행되도록만 하면된다.(링크 참고, 컴포넌트 스캔과 같음)
이렇게 DI를 진행하는 SpringConfig와 같은 것을 DI 컨테이너 혹은 IoC 컨테이너라고 부른다. → 어셈블러, 오브젝트 팩토리 등으로 불리기도 함

 다양한 의존관계 주입 방법

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

생성자 주입

→ 의존관계를 변경할 일이 거의 없기 때문에 생성자 주입을 권장한다.
@Controller public class MemberController { private final MemberService memberService; @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; } }
Java
복사
lombok 라이브러리 사용으로 생성자 주입을 아주 간단히 구현할 수 있다. ⇒  Lombok 라이브러리 참고
“불변(private), 필수(final)” 의존관계에 사용
“불변”
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.)
수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.
누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
“필수(final)”
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 오류 : java: variable discountPolicy might not have been initialized

필드 주입

→ 처음 외에 변경해줄 방법이 전혀 없다. 그렇기에 비권장
@Controller public class MemberController { @Autowired private MemberService memberService; }
Java
복사
코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점
DI 프레임워크가 없으면 아무것도 할 수 없다.
사용하지 말자! → 애플리케이션의 실제 코드와 관계 없는 테스트 코드 → 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

Setter 주입

→ public 으로 열려있기 때문에 남들이 이 컨트롤러의 memberService를 바꿔치기 할 수도 있다. 그렇기에 비권장
@Controller public class MemberController { private final MemberService memberService; @Autowired public void setMemberService(MemberService memberService){ this.memberService = memberService; } }
Java
복사
“선택, 변경” 가능성이 있는 의존관계에 사용
자바 빈 프로퍼티 규약으로 setxxx, getxxx라는 매서드를 통해 값을 읽거나 변경해야 함
getter/setter 예시 코드
class Data { private int age; public void setAge(int age) { this.age = age; } public int getAge() { return age; } }
Java
복사

 Lombok 라이브러리

1. Build.gradle - 환경변수 추가

plugins { id 'java' id 'org.springframework.boot' version '3.0.5' id 'io.spring.dependency-management' version '1.1.0' } group = 'hello' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' //lombok 설정 추가 시작 configurations { compileOnly { extendsFrom annotationProcessor } } //lombok 설정 추가 끝 repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' //lombok 라이브러리 추가 시작 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' //lombok 라이브러리 추가 끝 testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
Java
복사

2. IntelliJ 설정

preference(Cmd + ,) → Plugin → Lombok 설치 확인
preference(Cmd + ,) → Annotation Processors → Enable 체크

3. @RequiredArgsConstructor 어노테이션 추가

@RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
@Component public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } }
Java
복사
@Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; }
Java
복사

 @AutoWired

@AutoWired 옵션 처리 ⇒ 주입할 스프링 빈이 등록되지 않았어도 동작해야 할 때가 있다.

@Autowired(required = false) public void setNoBean1(Member noBean1) { System.out.println("noBean1 = " + noBean1); } @Autowired public void setNoBean2(@Nullable Member noBean2) { System.out.println("noBean2 = " + noBean2); } @Autowired public void setNoBean3(Optional <Member> noBean3) { System.out.println("noBean3 = " + noBean3); }
Java
복사
@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

결과

Member는 스프링 빈으로 등록해 주지 않았기 때문에 setNoBean1()@Autowired(required=false)는 동작을 하지 않는다.

조회 빈이 2개 이상일 경우

@Autowired 는 타입(Type)으로 조회 ⇒ ac.getBean(DiscountPolicy.class); 와 유사

1. 동일한 타입으로 스프링 빈 선언

@Component public class FixDiscountPolicy implements DiscountPolicy {}
Java
복사
@Component public class FixDiscountPolicy implements DiscountPolicy {}
Java
복사

2. @AutoWired로 의존성 주입

@Autowired private DiscountPolicy discountPolicy
Java
복사
NoUniqueBeanDefinitionException 오류 발생

3. @Autowired 필드 명, @Qualifier, @Primary

@Autowired 필드 명 매칭 ⇒ discountPolicyrateDiscountPolicy로 변경
@Autowired private DiscountPolicy discountPolicy
Java
복사
@Autowired private DiscountPolicy rateDiscountPolicy
Java
복사
@Qualifier @Qualifier끼리 매칭 빈 이름 매칭 ⇒ @Qualifier("mainDiscountPolicy") ⇒ 만약 등록한 이름의 Bean이 없다면 NoSuchBeanDefinitionException 오류 발생
@Component @Qualifier("mainDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy {}
Java
복사
@Component @Qualifier("fixDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy {}
Java
복사
주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.
@Autowired public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; }
Java
복사
수정자 주입시 && 수동 빈 등록 시
만약 @Qualifier 를 사용할 경우 Stirng은 컴파일 시 체크가 안되는 문제점이 존재하기 때문에 휴먼 에러가 발생하기 쉽다. 그렇기 때문에 따라 어노테이션을 만들어서 사용을 많이 한다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Qualifier("mainDiscountPolicy") public @interface MainDiscountPolicy { }
Java
복사
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented
Java
복사
⇒ 위 코드는 @Qualifier 에 존재하는 어노테이션들을 복붙!!
사용법 ⇒ @Qualifier("mainDiscountPolicy") 대신에 만들어 둔 어노테이션을 넣어주면 된다.
@Component @MainDiscountPolicy public class RateDiscountPolicy implements DiscountPolicy {}
Java
복사
@Autowired public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; }
Java
복사
@Primary 사용 ⇒ 여러 빈이 매칭되면 @Primary 가 우선권을 가진다. ⇒ RateDiscountPolicy가 우선권을 가지도록 설정
@Component @Primary public class RateDiscountPolicy implements DiscountPolicy {} @Component public class FixDiscountPolicy implements DiscountPolicy {}
Java
복사
@Autowired public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; }
Java
복사
만약 @Primary@Qualifier가 함께 사용되었다면? ⇒ @Qualifier 가 우선순위가 더 높다. ⇒ 하지만 @Primary가 더 간편하고 헷갈리지 않기 때문에, 웬만하면 @Primary를 사용하고 추가적인 설정이 필요할 때 @Qualifier를 사용하자

조회 빈이 2개 이상일 경우이지만 조회한 빈이 모두 필요할 때, List, Map

할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.
public class AllBeanTest { @Test void findAllBean() { ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class); DiscountService discountService = ac.getBean(DiscountService.class); Member member = new Member(1L, "userA", Grade.VIP); int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy"); assertThat(discountService).isInstanceOf(DiscountService.class); assertThat(discountPrice).isEqualTo(1000); } static class DiscountService { private final Map<String, DiscountPolicy> policyMap; private final List<DiscountPolicy> policies; public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) { this.policyMap = policyMap; this.policies = policies; System.out.println("policyMap = " + policyMap); System.out.println("policies = " + policies); } public int discount(Member member, int price, String discountCode) { DiscountPolicy discountPolicy = policyMap.get(discountCode); System.out.println("discountCode = " + discountCode); System.out.println("discountPolicy = " + discountPolicy); return discountPolicy.discount(member, price); } } }
Java
복사

로직 분석

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

주입 분석

Map<String, DiscountPolicy> : Map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

참고

Spring Bean을 등록 시 웬만하면 대부분을 싱글톤으로써 하나의 객체로만 존재하도록 한다. (아주 특수한 케이스에서 싱글톤을 사용하지 않는 경우가 존재하기는 함)