Develop/🌸 Spring

스프링 - 동일 패키지를 두번 이상 ComponentScan 할때 & 조회 대상 빈이 2개 이상일때

인피니 2024. 1. 7. 02:59

조회 대상 빈이 2개 이상일 때  생기는 문제를 정리하게 된 이유?  

 


팀에서 스프링 배치를 통해 특정 기능을 구현하던 중 다른 팀원분이 @ComponentScan을 통해 특정 패키지 하위를 스캔하여 빈 등록을 하도록 구현되어 있었는데, @ComponentScan 이 적힌 클래스보다 상단의 패키지에  main 메서드가 존재하고, 해당클래스 상단에 @SpringBootApplication 이 붙어있었기 때문에 @ComponentScan을 따로 클래스에 적지 않아도 빈이 등록되었을 구조였다.

해당 코드를 보면서, 스프링이 런타임 오류를 발생시키지만 컴포넌트 스캔위치가 중복되는 것은 괜찮은 걸까? (물론 상용에서 사용 중인 코드니 문제는 없겠지만)  궁금해졌다. 

위의 궁금증과 엄청난? 관계가 있는 것은 아니지만, 조회대상 빈이 2개 이상일 때를 어떻게 해결하면 좋을지 예전에 공부했던 내용도 같이 복습하고자 한다.

 

 

동일 경로의 패키지를 두 번 이상 @ComponentScan 하는 경우 


-> 테스트해본 프로젝트의 구조이다

 

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}

해당 main으로부터 Applicaiton 은 실행된다.

 

@Configuration
@ComponentScan(basePackages = "com.example.demo.bean")
public class AConfiguration {


}

동일 패키지에 AConfiguration 클래스가 존재하고  해당 패키지는 컴포넌트 스캔을 할 경로를 

com.example.demo.beam 패키지부터 하위 패키지를 컴포넌트 스캔대상으로 설정했다.

 

@Service
public class BService {

}

BService는 위의 두 클래스가 설정한 것처럼 컴포넌트 스캔의 대상이 된다.

 

@SpringBootTest
class DemoApplicationTests {

	@Autowired ApplicationContext applicationContext;
	
	@Test
	void getBeans() {
		String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
		for (String beanDefinitionName : beanDefinitionNames) {
			Object bean = applicationContext.getBean(beanDefinitionName);
			System.out.println("beanDefinitionName = "+ beanDefinitionName+", bean = "+ bean);
		}
	}

}

테스트 코드이기 때문에 필드주입을 통해 ApplicationContext를 주입받아, 스프링 컨테이너에 주입된 모든 빈을 출력해 보았다.

-> 말짱히 AConfiguration, BService 빈이 등록된 것을 볼 수 있다.

 

위 테스트를 결론으로 스프링에서 @ComponentScan 범위가 겹쳐도 별다른 문제없이 애플리케이션이 잘 돌아가는 것을 확인할 수 있다.

 

조회 대상 빈이 2개 이상 일 때 


문제 상황

public interface DiscountPolicy {
}

 

@Component
public class FixDiscountPolicy implements DiscountPolicy {
}

 

@Component
public class RateDiscountPolicy implements DiscountPolicy {
}

위처럼 1개의 인터페이스와 해당 인터페이스를 구현한 두 개의 클래스가 있다. 

 

@SpringBootTest
class DemoApplicationTests {

	@Autowired DiscountPolicy discountPolicy;

	@Test
	void getBeans() {
		String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
		for (String beanDefinitionName : beanDefinitionNames) {
			Object bean = applicationContext.getBean(beanDefinitionName);
			System.out.println("beanDefinitionName = "+ beanDefinitionName+", bean = "+ bean);
		}
	}

}

@Autowired를 통해 DiscountPolicy 타입의 빈을 주입받고자 하면 NoUniqueBeanDefinitionException 예외가 발생한다. 

 

 해결방법 1) @Autowired 필드명 매칭 

@Autowired는 타입 매칭 -> 조회대상빈이 여러 개일 경우, 파라미터 이름 or 필드이름으로 빈 이름을 추가 매칭한다.

@Autowired DiscountPolicy fixDiscountPolicy;

-> 해당 코드처럼 수정한다면 fixDiscountPolicy를 빈으로 주입받게 된다.

 

해결방법 2) @Quailifier 사용

주의할 것은@Quailifier는 추가 구분자를 붙여주는 것이지 빈 이름을 변경하는 것은 아니다.

@Component
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}

-> 클래스 위에 추가 구분자를 붙여주고 

 

@Autowired @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy;

기존 테스트 코드에서 빈 주입 시 추가 구분자를 붙여주면 된다.

@Quailifier 또한 매개변수 & 필드에서 사용할 수 있다.

만약 @Quailifier("name")에서 name에 해당하는 빈을 못 찾을 경우, main이라는 이름의 스프링 빈을 추가로 찾는다.

하지만, 코드는 명확한 게 좋기 때문에 해당 어노테이션은 @Quailifier를 찾는 용도로만 쓰자 

 

해결방법 3) @Primary 사용

@Primary는 우선순위를 정하는 방법이다. 빈 주입 시 여러 빈이 매칭되면 @Primary 어노테이션이 있는 빈이 우선권을 갖는다.

@Component
@Primary
public class FixDiscountPolicy implements DiscountPolicy {
}

 

 

 

참고

김영한 님의 스프링 핵심 원리 - 기본편