서론
스프링 프레임워크의 주요 개념을 이해하기 위해 상향식 접근법으로 공부를 하였다. 먼저 제어의 역전부터 시작하여 스프링 컨테이너 까지 이해하는것을 목표로 공부를 하고 기록하였다.
제어의 역전
일반적으로 소프트웨어 개발 시 제어 흐름은 개발자가 직접 관리한다. 하지만 제어의 흐름이 개발자에게 있는 것이 아니라 프레임워크나 외부 환경에 의해 관리될 때, 이를 제어의 역전이라고 부른다.
예를 들어, 아래 소스 코드에서 `OrderServiceImpl`은 `OrderService` 인터페이스를 구현하고 있으며, 클래스 내부에서 직접 `MemberRepository`와 `DiscountPolicy` 객체를 생성하여 의존관계를 설정하고 있는것을 확인할 수 있다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
❌ 정책 변경에 따른 문제 발생
만약 메모리에 멤버를 저장하는 방식이 아닌 데이터베이스에 멤버를 저장하는 방식으로 정책을 변경해야 한다고 가정해보자. 이렇게 정책이 바뀌게 된다면 `OrderServiceImpl` 내부에서 직접 생성된 객체들을 수정 해야한다.
예를 들어, `MemberRepository`를 `DatabaseMemberRepository` 로 변경하려면 `OrderServiceImpl`에서 의존하고 있는 객체를 다음과 같이 직접 수정해야한다. 따라서 현재 코드는 개방 폐쇠 원칙과, 의존 관계 역전 법칙을 따르지 않고 있다고 말할 수 있다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new DatabaseMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 주문 생성 메서드 구현
public Order createOrder(...) {
//로직...
}
}
이런 방식은 구체적인 클래스에 의존하고 있기 때문에, 새로운 정책이나 새로운 요구사항이 생길때마다 코드를 수정해야 한다.
✔ DI 컨테이너 생성
위와 같은 문제를 해결하기 위해 다음과 같은 방법을 사용하였다. 먼저 `Appconfig` 라는 클래스를 생성하여 객체의 생성과 관리를 담당하는 책임을 갖도록 하였고 메서드를 통해 객체를 생성해 반환할 수 있도록 코드를 작성하였다.
public class AppConfig {
// MemberService를 생성해 반환
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// OrderService를 생성해 반환
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// MemberRepository 객체 생성
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// DiscountPolicy 객체 생성
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
이제 다시 `OrderServiceImpl` 클래스로 돌아와 다음과 같이 수정해주었다. 이전과 달리 클래스 내부에서 직접 객체를 생성하는것이 아닌 생성자를 통하여 클래스 외부에서 구현 객체를 주입받도록 하였다. 이제 새로운 기능이나 정책이 추가되어도 클래스 외부에서 객체를 주입받으므로 `OrderServiceImpl`의 코드를 변경하지 않아도 된다.
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;
}
// 주문 생성 메서드 구현
public Order createOrder(...) {
//로직...
}
}
이제 메인 코드이다. `Appconfig` 클래스를 생성하여 필요한 객체를 생성하고, 그 객체들을 통해 회원 가입 및 주문을 처리할 수 있도록 코드를 작성하였다.
public class Main {
public static void main(String[] args) {
// AppConfig를 통해 객체 생성 및 의존성 주입
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
// 회원 가입
Member member = new Member(1L, "john@example.com");
memberService.join(member);
// 주문 생성
Order order = orderService.createOrder(1L, "itemA", 10000);
System.out.println("주문 정보: " + order);
}
}
🌞 의존성 주입(Dependency Injection)
이처럼 `AppConfig` 클래스를 통해 `OrderServiceImpl`의 제어권이 외부로 넘어간 것을 확인할 수 있다. 여기서 `OrderServiceImpl`은 더 이상 자신이 직접 객체를 생성하지 않고, 외부에서 필요한 객체들을 주입받는다 실제 어떤 구현 객체가 들어올지는 `OrderServiceImpl` 클래스 입장에서는 알 수 없다.
이처럼 클래스 내부에서 직접 객체를 생성하거나 의존관계를 설정하지 않고, 외부에서 필요한 객체를 주입받는 방식을 의존성 주입이라고 한다. 이를 통해 OCP (개방 폐쇠 원칙) 과 DIP (의존 관계 역전 법칙) 을 따를 수 있게 되었다. 즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않아도 되며, 유연성과 확장성이 향상되었다.
🌞 제어의 역전(Inversion Of Control)
`Appconfig` 클래스와 같이, 객체의 생성과 의존성 주입을 외부에서 관리하는 클래스를 Ioc 컨테이너 또는 DI 컨테이너라고 이야기한다. 이 컨테이너는 객체의 생명 주기와 의존성 관리를 담당하며, 이를 통해 제어의역전을 일으킬 수 있다. 이제 스프링 프레임워크로 넘어가 `Appconfig`와 같은 역할을 스프링 컨테이너가가 담당하게 된다.!!
스프링 컨테이너
스프링 컨테이너란 스프링 프레임워크에서 객체의 생성과 관리, 의존성 주입을 담당하는 핵심 구성요소이다. 스프링 컨테이너는 애플리케이션의 모든 객체를 관리하며, 객체들 간의 의존성을 설정해주는 역할을 한다.
🌱 스프링 빈(Spring Bean)
스프링 컨테이너는 애플리케이션에서 사용되는 모든 객체를 관리한다고 이야기를 하였다, 이때 스프링 컨테이너에 의해 생성되고 관리되는 객체들을 스프링 빈이라고 부른다.
🧱 스프링 컨테이너의 종류
스프링 컨테이너의 종류로는 가장 먼저 `BeanFactory` 가 존재한다. BeanFactory는 스프링 컨테이너의 최상위 인터페이스로 스프링에서 빈을 생성하고 의존성 주입을 담당하는 역할을 한다. 다음으로 이 `BeanFactory` 를 구현한 `ApplicationContext`이다. BeanFactory를 확장하여 애플리케이션 전반에 걸친 빈 관리를 담당하는 역할을 한다. 빈 생성 및 의존성 주입 외에도 이벤트 처리, 국제화 지원, 메시지 리소스 핸들링 등 부가 기능 지원기능들을 지원한다.
👍 ApplicationContext
`BeanFactory` 도 스프링 컨테이너지만 직접적으로 사용할 일은 별로 없다고 한다. 따라서 스프링 프레임워크에서 가장 많이 사용되는 ApplicationContext에 대해서 알아보려고 한다. `ApplicationContext` 도 마찬가지로 인터페이스이다. 따라서 여러 구현체가 존재하며, 이를 통해 다양한 방식으로 스프링 컨테이너를 생성할 수 있다.
- AnnotationConfigApplicationContext
- 자바 기반 설정을 사용하는 컨테이너.
- ClassPathXmlApplicationContext
- XML 파일 기반 설정을 사용하는 컨테이너.
- FileSystemXmlApplicationContext
- XML 설정 파일이 클래스패스가 아닌 파일 시스템 경로에 있을 때 사용한다.
이와 같이 다양한 구현체가 존재할 수 있는 이유는 빈을 등록하는 방식을 BeanDefinition으로 추상화하여 빈을 생성하기 때문이다. XML 설정을 사용하든, 자바 설정을 사용하든, 결국 BeanDefinition이 생성된다. 이를 통해 다양한 설정 방식에도 불구하고, 일관된 방식으로 스프링 빈이 관리될 수 있다.
이제 스프링 컨테이너를 도입하여 기존 코드를 수정해보겠다. `Appconfig` 클래스에 `@Configuration` 어노테이션과 `@Bean` 어노테이션을 추가해주었다. 스프링은 이 어노테이션이 붙은 클래스를 설정 정보 클래스로 사용하고 `@Bean` 어노테이션이 붙은 메서드를 호출하여 반환된 객체를 스프링 빈으로 등록한다.
@Bean 어노테이션이 붙은 각 메서드의 이름은 해당 스프링 빈의 이름이 된다.
@Bean 어노테이션이 붙은 각 메서드의 반환된 객체는 스프링 빈의 객체가 된다.
@Configuration
public class AppConfig {
// MemberService를 생성해 반환
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// OrderService를 생성해 반환
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// MemberRepository 객체 생성
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// DiscountPolicy 객체 생성
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
다음은 메인 코드이다. `new AnnotationConfigApplicationContext()`를 사용하여 자바 기반 스프링 컨테이너 객체를 생성했했고. 그 후, 설정 정보 클래스인 `AppConfig`를 파라미터로 전달하여 스프링 컨테이너가 이를 바탕으로 빈(Bean)을 생성하고 관리하도록 하였다.
public class Main {
public static void main(String[] args) {
// 스프링 컨테이너 생성 (자바 기반 설정)
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = context.getBean(MemberService.class);
OrderService orderService = context.getBean(OrderService.class);
// 회원 가입
Member member = new Member(1L, "john@example.com");
memberService.join(member);
// 주문 생성
Order order = orderService.createOrder(1L, "itemA", 10000);
System.out.println("주문 정보: " + order);
}
}
💨 스프링 컨테이너 생성과정
1. 스프링 컨테이너 생성
- ApplicationContext 인터페이스의 구현체를 사용하여 스프링 컨테이너를 생성한다.
- 설정 정보(Java, XML 등)를 기반으로 빈(Bean)을 생성하고 관리하는 컨테이너를 구성한다.
2. 스프링 빈 등록
- 스프링 컨테이너는 파라미터로 들어온 설정 클래스 정보를 활용하여 스프링 빈을 등록한다.
- @Bean 어노테이션이 붙은 메서드를 호출하여 반환된 객체를 스프링 빈으로 등록한다.
3. 의존 관계 설정 - 준비
- 준비 단계에서는 의존성 주입이 필요한 필드를 모두 찾아낸다.
4. 의존 관계 설정 - 완료
- 스프링 컨테이너는 의존성 주입(DI)을 통해 빈 간의 관계를 설정한다.
스프링 컨테이너에 등록된 스프링 빈 조회하기
1. 타입으로 조회하기
먼저, `AppConfig` 클래스에 `@Configuration` 어노테이션을 선언하여 이 클래스가 스프링의 설정 클래스임을 명시하였다다. 그런 다음, 자바 기반의 스프링 컨테이너를 생성하고, `AppConfig` 클래스를 통해 자바 설정 정보를 스프링 컨테이너에 전달한다. 이후, `ac.getBean()` 메서드를 사용하여` MemberRepositor`y 타입의 빈을 조회하였다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberRepository bean = ac.getBean(MemberRepository.class);
이 코드를 통해 현재 MemberRepository 타입으로 스프링 컨테이너에 등록된 빈을 조회하였다. 이는 부모 타입으로 빈을 조회한 것이라고 할 수 있다. 그런데, 스프링 컨테이너에 동일한 타입의 빈이 여러 개 있을 경우 어떻게 될까? `AppConfig` 클래스에 다음과 같이 빈을 추가해 보았다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository_1() {
return new H2MemberRepository();
}
}
이제 다시 조회하게 된다면, `NoUniqueBeanDefinitionException` 예외가 발생하는 것을 확인할 수 있다. 이는 스프링 컨테이너에서 빈을 조회할 때 같은 타입의 빈이 두 개 이상 존재하면, 스프링이 어떤 빈을 반환해야 할지 결정하지 못해 발생하는 예외이다. 즉, 타입으로 조회할 때는 빈이 중복되면 안 된다는 점을 이해할 수 있다.
이러한 문제를 해결하기위해 타입으로 빈을 조회할때 구체적인 타입을 명시하여 스프링 빈을 조회할 수 있다. 하지만 이 방법은 구체적인 타입에 의존하기 때문에 DIP 원칙을 따르지 않는다고 볼 수 있다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Object bean = ac.getBean(MemoryMemberRepository.class);
System.out.println(bean.getClass());
이렇게 스프링 컨테이너는 부모 타입으로 조회할 때, 자식 타입의 스프링 빈이 모두 반환될 수 있다는 사실을 알게 되었다. 그렇다면 Object 타입으로 조회하면 모든 스프링 빈이 반환될 것이라고 예상해볼 수 있을 것이다.
2. 이름으로 조회하기
타입으로 조회할 때 스프링 컨테이너에 동일한 타입의 빈이 여러 개 있을 경우 예외가 발생하는 것을 확인했다. 이 문제는 이름으로 조회함으로써 해결할 수 있다. 스프링 컨테이너는 빈을 생성할 때 기본적으로 메서드의 이름을 빈 이름으로 등록한다. 다음 예제에서 memberRepository와 memberRepository_1이라는 이름으로 스프링 빈이 등록하였다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository_1() {
return new H2MemberRepository();
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Object bean = ac.getBean("memberRepository");
System.out.println(bean.getClass());
이전에 타입으로 조회할 때와는 달리, 이름으로 조회할 경우 예외가 발생하지 않는다. 타입으로 조회할 때는 동일한 타입의 빈이 두 개 이상 있어서 스프링이 어떤 빈을 반환해야 할지 결정하지 못해 `NoUniqueBeanDefinitionException`이 발생하는것이다. 하지만 이름으로 조회하면 스프링 컨테이너에 등록된 이름이 고유하므로, 명확히 해당 빈을 선택할 수 있어 예외가 발생하지 않는다. 다만, 등록되지 않은 이름을 조회하면 `NoSuchBeanDefinitionException` 예외가 발생한다.
또한, 부모 타입으로 빈을 조회할 때 구체적인 이름을 명시하면 스프링 컨테이너에 동일한 타입의 빈이 여러 개 있더라도 문제가 발생하지 않는다. 이 경우, 빈의 이름과 타입을 함께 지정하여 스프링이 정확히 어떤 빈을 반환해야 하는지 명시할 수 있다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Object bean = ac.getBean("memberRepository",MemberRepository.class);
System.out.println(bean.getClass());
++ 부모타입을 지정하고스프링 빈의 이름을 틀리게 작성하였을 시에도 `NoSuchBeanDefinitionException` 예외가 발생하였다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Object bean = ac.getBean("memberReposito321ry",MemberRepository.class);
System.out.println(bean.getClass());
정리하자면 스프링 컨테이너에 등록된 스프링 빈을 조회할때 이름과 타입으로 빈을 조회할 수 있다. 부모타입으로 조회시 자식 타입을 모두 조회하고 부모 타입의 빈이 여러개 등록되어있을 경우 예외가 발생하는것을 확인할 수 있다.
이름으로 조회할 때는, 스프링 컨테이너에 등록된 빈 이름이 고유하므로 동일한 타입의 빈이 여러 개 있더라도 정확히 하나의 빈을 조회할 수 있어 예외가 발생하지 않다. 그러나 등록되지 않은 이름으로 조회할 경우, NoSuchBeanDefinitionException 예외가 발생한다.
3. 모든 빈 조회
string[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
}
4. 어플리케이션 빈 조회
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
System.out.println(ac.getBean(beanDefinitionName));
}
}
스프링 컨테이너 자동 생성
이전 까지는 스프링 컨테이너를 직접적으로 생성하고 스프링 빈도 수동으로 등록하였다, 일반적으로 스프링 프레임워크를 사용할 때 스프링 컨테이너를 직접 생성하지 않고, 스프링 부트가 애플리케이션을 시작할 때 자동으로 스프링 컨테이너를 생성해준다. 따라서 개발자가 직접 `AppConfig` 클래스와 같이 컨테이너를 직접 생성하고 스프링 빈을 등록할 수고를 덜어준다.
@SpringBootApplication
스프링 부트를 사용할 때 main/java/.../ 패키지에서 xxxApllication.java 클래스가 생성되는것을 확인할 수 있고, 이 클래스 내부에 `@SpringBootApplication` 어노테이션을 붙어있는것을 확인할 수 있다.
`@SpringBootApplication` 어노테이션은 스프링 부트의 핵심 어노테이션으로, 이 어노테이션을 선언하면 스프링 부트가 자동으로 스프링 컨테이너를 생성하고 애플리케이션을 실행할 수 있도록 해준다. 이 어노테이션을 따라 클래스에 접근하면 내부적으로 `@Configuration`, `@EnableAutoConfiguration`, `@ComponentScan` 어노테이션을 포함하고 있는것을 확인할 수 있다. 따라서 설정 파일을 자동으로 읽고, 빈을 자동으로 등록하며, 필요한 설정을 자동으로 수행한다.
이 코드를 실행하면, 스프링 부트가 자동으로 `ApplicationContext`를 생성하고 관리하게 된다. 개발자는 이제 `SpringApplication.run()` 메서드를 통해 스프링 부트 애플리케이션을 시작할 수 있으며, 그 뒤로는 스프링이 알아서 컨테이너를 설정하고 빈을 관리하게 된다.
@SpringBootApplication
public class MvcApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@ComponentScan
`@SpringBootApplication` 을 통해 스프링 부트가 애플리케이션을 실행할 때 `ApplicationContext` 즉 스프링 컨테이너를 자동으로 생성해주는것을 알았다. 그러면 스프링 빈은 어떻게 등록할까?
스프링은 `@ComponentScan` 메서드를 사용하여 특정 패키지 내에서 `@Component`, `@Service`, `@Repository`, `@Controller` 와 같은 어노테이션을 스캔하여 빈을 자동으로 등록한다. 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자는 소문자를 사용하며 개발자가 직접 이름을 지정해 줄 수 있다. @Component, @Controller, @Service, @Repository 에도 내부에 @Component가 달려있다.
@Repository
public class MemoryMemberRepository implements MemberRepository{
}
@Repository
public class H2MemberRepository implements MemberRepository{
}
그렇다면 컴포넌스 스캔은 모든 패키지를 검색하며 @Component 어노테이션이 붙은 클래스를 검색하는 것일까?
- 개발자가 따로 지정하지 않은 경우 @ComponentScan 이 붙은 설정 정보 클래스부터 하위로 검색한다.
- ex) AutoAppConfig 클래스의 위치가 package com.hello.core 이므로 core 부터 하위로 검색하게 된다.
- 컴포넌트 스캔을 임의로 필요한 위치부터 검색할 수 있도록 변경할 수 있다.
- basePackages : 탐색할 패키지의 시작 위치를 지정한다. 패키지를 포함하여 하위로 검색한다.
- basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다
- 필터 기능을 사용하면 컴포넌트 스캔 대상을 추가하거나 제외할 수 도 있다.
- includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig{
}
컴포넌트 스캔에서 같은 빈 이름을 등록하면?
- 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데 이름이 같을 경우 스프링은 오류 발생한다.
- ConflictingBeanDefinitionException 예외 발생
@AutoWird
기존 `AppConfig` 클래스에서는 `@Bean`을 통해 클래스를 등록하면서 의존관계도 명시해주었다. 하지만 컴포넌트 스캔을 사용하면서 명시적으로 의존관계를 주입할 방법이 없다. 스프링에서는 `@AutoWird` 어노테이션을 사용하여 의존관계를 주입하여준다.
의존관계 주입은 크게 4가지 방법이 있다.
생성자 주입
- 객체가 생성될 때 해당 클래스의 생성자를 호출하여 의존 관계를 주입하는 방식이다.
- 생성자를 통해 의존관계가 주입되므로 딱 1번만 호출되는것을 보장한다.
- 불변, 필수 의존관계에 사용된다.
수정자 주입
- 객체를 먼저 생성후 스프링 컨테이너가 해당 객체의 Setter 메서드를 호출하여 의존관계를 주입하는 방식이다.
- 선택, 변경 가능성이 있는 의존관계에서 사용된다.
- Setter 메서드를 통해 의존관계를 변경 할 수 있기 때문.
메서드 주입
- 객체 생성후 스프링 컨테이너가 @Autowired 붙은 메서드를 실행하여 의존관계를 주입하는 방식이다.
- 한번에 여러 필드를 주입받을 수 있다는 장점이 있다.
필드 주입
- 객체 생성후 해당 객체의 필드에 직접 주입하는 방식이다.
- 코드가 간결하게보일 수 있지만 외부에서 변경할 방법이 없어 테스트가 힘들다는 단점이있다.
- DI 프레임 워크가 없으면 아무것도 할 수 없다.
옵션처리
@Autowired 만 사용하면 required 의 값이 ture 이기 때문에 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입할 대상을 옵션으로 처리하는 방법
- @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않는다.
- @Nullable : 자동 주입할 대상이 없으면 null 값
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty
생성자 주입 권장
- 대부분 의존관계는 한 번 주입하면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.
- 대부분 의존관계는 애플리케이션 종료 시점까지 변하면 안된다 → 불변
- 수정자 주입을 사용하면 Setter 메서드를 public으로 열어두어야한다 → 실수 할 수 있다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 실행되므로 이후 호출 X → 불변하다.
- 프레임워크에 의존하지 않고 순수한 자바 언어 특징을 잘살리는 방법이다.
의존관계 주입 - 조회 빈이 2개 이상 일때
- 의존관계 주입시 타입으로 빈을 조회하기 때문에 하위 타입이 2개 이상 조회되면 예외가 발생한다.
- 의존관계 주입시 타입을 하위타입으로 지정하여 해결할 수 있지만 DIP 위배하고 유연성이 떨어진다.
해결방법
- @Autowired 필드 명 매칭
- @Qualifier 끼리 매칭
- @Primary 사용
@Autowird
- @Autowired 는 타입 매칭을 시도하고 여러 빈이 있을 경우 필드 명, 파라미터 명으로 빈 이름을 추가 매칭한다.
필드 명을 빈 이름으로 변경
private final DiscountPolicy rateDiscountPolicy;
파라미터 이름을 빈이름으로 변경
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Qualifier
- 추가 구분자를 붙여주는 방법이다.
- @Qualifier 의 설정된 이름을 못찾을 경우 Qualifier 의 해당하는 이름의 스프링 빈을 추가로 찾는다.
- @Qualifier(“testRepository”) -> testRepository 스프링 빈 검색
- Qualifier 는 Qualifier를 찾는용도만 사용하는것이 명확하고 좋다.
빈 등록시에 @Qualifier 어노테이이션 사용.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Qualifier 사용
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary
- 우선 순위를 정하는 방법이다.
- @Autuwired 시 여러 빈이 매칭되면 @Primary 어노테이션이 붙은 빈이 우선권을 가진다.
rateDiscountPolicy 가 우선권을 가지는 소스코드
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
우선 순위
스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선순위가 높다.
@Primary < @Qualifier
'TIL' 카테고리의 다른 글
[TIL - 2024-09-12] 스프링 테스트, 단위 테스트, 통합 테스트 (0) | 2024.09.12 |
---|---|
[TIL - 2024-09-11] 싱글톤 패턴, 스프링 빈 생명주기 콜백, 스프링 빈 스코프, DL, Proxy (0) | 2024.09.11 |
[TIL - 2024-09-10] 좋은 객체 지향 설계의 5가지 원칙 SOLID (1) | 2024.09.10 |
[TIL - 2024-09-09] REST, RESTful API, 실습 (3) | 2024.09.09 |
[TIL - 2024-09-07] 디자인 패턴,스프링 MVC 패턴 (2) | 2024.09.07 |