스프링에서 스프링 빈(Bean) 조회는 스프링 컨테이너(Spring Container)에 등록된 객체들을 찾아보는 방법이다. 빈 확인은 주로 테스트(Test) 상황에서 많이 활용한다. 빈 등록 방법은 크게 자동 빈 등록(Automatic Bean Registration)과 수동 빈 등록(Manual Bean Registration)으로 나뉘며, 등록 방법에 따라서 조회 방법 또한 달라진다.
🎯 주요 사용 목적
스프링 빈 조회는 다음과 같은 목적으로 사용된다.
- 등록된 빈 확인 : 원하는 빈이 제대로 등록(Registration)되었는지 확인하고, 어떤 빈들이 등록되어 있는지 전체적으로 파악(Overall Grasp)한다.
- 문제 해결 : 의존성 주입(Dependency Injection)이 제대로 되었는지 확인하고, 빈 등록 관련 문제 발생 시 디버깅(Debugging)을 수행한다.
- 빈 사용 : 필요한 빈을 가져와서 사용하거나, 특정 타입의 빈들을 모두 가져와서 사용한다.
📚 빈 등록 방식
스프링 빈은 크게 두 가지 방식으로 컨테이너에 등록된다.
자동 빈 등록 (@Component 스캔)
스프링의 컴포넌트 스캔(Component Scan) 기능을 활용하여 자동으로 빈을 등록하는 방식이다. 주로 @Component
어노테이션과 이를 포함하는 @Service
, @Repository
, @Controller
등의 어노테이션을 사용한다.
@Component // 자동 등록 대상
public class MemberService { }
@Service // @Component 포함 (비즈니스 로직 담당)
public class OrderService { }
@Repository // @Component 포함 (데이터 접근 계층 담당)
public class MemberRepository { }
수동 빈 등록 (@Configuration + @Bean)
개발자가 @Configuration
클래스 내부에서 @Bean
어노테이션을 사용하여 빈을 직접 정의하고 등록하는 방식이다.
@Configuration // 스프링 설정 클래스임을 명시
public class AppConfig {
@Bean // 빈으로 등록될 메서드
public MemberService memberService() {
return new MemberServiceImpl(); // 직접 객체를 생성하여 반환
}
}
⚙️ 사전 필요 설정 : 스프링 컨테이너 생성
빈을 조회하기 위해서는 먼저 스프링 컨테이너(Spring Container)를 생성해야 한다. 스프링 빈은 스프링 컨테이너가 관리하는 객체이므로, 테스트 코드에서 스프링 컨테이너를 만들지 않으면 빈 조회가 불가능하다.
// 스프링 컨테이너 생성
// AnnotationConfigApplicationContext는 ApplicationContext의 구현체로,
// 어노테이션 기반 설정 클래스를 통해 빈을 로드한다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
참고 : AnnotationConfigApplicationContext
는 ApplicationContext
인터페이스의 구현체이다. ApplicationContext
는 빈 조회와 같은 기본적인 컨테이너 기능을 제공하는 핵심 인터페이스(Core Interface)이며, AnnotationConfigApplicationContext
는 이를 구현하여 어노테이션 기반(Annotation-based)의 설정 클래스를 사용하여 스프링 컨테이너를 초기화할 때 주로 사용된다. 빈 조회뿐만 아니라 빈 메타정보(BeanDefinition) 조회 등 더 풍부한 기능을 제공한다.
📌 빈 역할 구분
ApplicationContext
: 단순 빈 조회만 필요할 때 주로 사용하는 인터페이스 타입(Interface Type)으로 컨테이너를 선언한다.AnnotationConfigApplicationContext
: 빈 메타정보(BeanDefinition) 확인 등 더 상세한 컨테이너 기능이 필요할 때 사용하는 구현체 타입(Implementation Type)으로 컨테이너를 선언한다.
🗄️ 자주 사용되는 메서드
스프링 컨테이너에서 빈을 조회할 때 다음 메서드들이 주로 사용된다.
getBeanDefinitionNames()
: 컨테이너에 등록된 모든 빈의 이름(All Bean Names)을 조회한다.getBean()
: 특정 빈을 이름(Name) 또는 타입(Type)으로 조회한다.getBeansOfType()
: 특정 타입(Type)의 모든 빈(All Beans)을 조회한다.containsBean()
: 특정 빈의 존재 여부(Existence Check)를 확인한다.getType()
: 빈의 타입(Type)을 조회한다.getBeansWithAnnotation()
: 특정 어노테이션(Annotation)이 붙은 빈들을 조회한다.
🔍 빈 조회 방법
스프링 컨테이너에 등록된 빈을 조회하는 방법은 크게 단일 빈 조회(Single Bean Lookup)와 다중 빈 조회(Multiple Bean Lookup)로 나눌 수 있다.
단일 빈 조회 (Single Bean Lookup)
특정 빈 하나를 정확하게 조회할 때 사용한다.
- 타입으로 조회 :
ac.getBean(Type.class)
메서드를 사용하여 지정된 타입(Type)의 빈을 조회한다.
@Test @DisplayName("타입으로만 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
System.out.println("memberService = " + memberService);
}
조회하고 싶은 타입을 ac.getBean(타입.class)
의 타입으로 변경하면 된다. 이때 왼쪽의 변수 타입도 일치시켜야 하며, 일치시키지 않을 경우 컴파일 오류가 발생한다.
// OrderService 타입으로 조회할 경우
OrderService orderService = ac.getBean(OrderService.class);
// MemberRepository 타입으로 조회할 경우
MemberRepository memberRepository = ac.getBean(MemberRepository.class);
주의 사항 : 동일한 타입의 빈이 두 개 이상 존재하면 NoUniqueBeanDefinitionException
이 발생한다. 이러한 예외 상황에 대한 해결 방법은 'VII. 빈 조회 시 발생할 수 있는 주요 예외 상황' 섹션에서 상세히 다룬다.
- 빈 이름으로 조회 :
ac.getBean("beanName", Type.class)
메서드를 사용하여 지정된 빈 이름(Bean Name)과 타입(Type)으로 빈을 조회한다.
@Test @DisplayName("빈 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
System.out.println("memberService = " + memberService);
}
- 구체 타입으로 조회 :
ac.getBean(ConcreteType.class)
메서드를 사용하여 특정 인터페이스가 아닌 구체 클래스(Concrete Class)의 타입으로 빈을 조회한다. 이는 특정 구현체에 의존하는 경우에 사용될 수 있다.
@Test @DisplayName("구체 타입으로 조회")
void findBeanByConcreteType() {
MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
System.out.println("memberService = " + memberService);
}
다중 빈 조회 (Multiple Bean Lookup)
특정 조건에 맞는 여러 개의 빈을 한 번에 조회할 때 사용한다.
- 특정 타입의 모든 빈 조회 :
ac.getBeansOfType(Type.class)
메서드를 사용하여 지정된 타입(Type)에 해당하는 모든 빈을Map<String, T>
형태로 조회한다. 이 방법은NoUniqueBeanDefinitionException
을 회피하면서, 특정 인터페이스의 모든 구현체를 동적으로 처리해야 할 때 유용하다.
@Test @DisplayName("특정 타입 빈 출력")
void findAllBeanByType() {
Map<String, MemberService> beansOfType = ac.getBeansOfType(MemberService.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
}
- 특정 애노테이션이 붙은 빈 조회 :
ac.getBeansWithAnnotation(Annotation.class)
메서드를 사용하여 특정 어노테이션이 붙은 모든 빈을Map<String, Object>
형태로 조회한다. 이는@Component
,@Service
,@Repository
등과 같이 특정 역할을 나타내는 애노테이션이 적용된 빈들을 그룹으로 관리할 때 활용된다. 자동 등록된 빈을 조회할 때 특히 유용하다.
@Test void findComponentBean() {
// @Component 어노테이션이 붙은 모든 빈 조회
Map<String, Object> beansWithAnnotation =
ac.getBeansWithAnnotation(Component.class);
// ... (필요한 추가 로직)
// @Service 어노테이션이 붙은 빈들 조회
Map<String, Object> services =
ac.getBeansWithAnnotation(Service.class);
// ...
}
- 모든 빈 출력 (애플리케이션 빈 필터링 포함) :
ac.getBeanDefinitionNames()
를 사용하여 스프링 컨테이너에 등록된 모든 빈의 이름(All Bean Names)을 조회할 수 있다. 이는 주로 컨테이너에 어떤 빈들이 등록되어 있는지 전체적으로 파악(Overall Grasp)하거나 디버깅(Debugging) 목적으로 사용된다.
@Test @DisplayName("모든 빈 출력")
void findAllBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
- 사용자 정의 빈 출력 :
BeanDefinition.ROLE_APPLICATION
을 활용하여 사용자가 직접 정의한 애플리케이션 빈(Application Bean)만 필터링하여 조회할 수 있다.
// 사용자 정의 빈 출력
@Test @DisplayName("사용자 정의 빈 출력")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// 빈 역할 구분
// BeanDefinition.ROLE_APPLICATION : 사용자가 정의한 빈
// BeanDefinition.ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
}
참고 : BeanDefinition.ROLE_APPLICATION
을 BeanDefinition.ROLE_INFRASTRUCTURE
로 변경하면 스프링 내부 빈이 출력된다. 스프링에서 "Application"이라는 용어는 "사용자의 애플리케이션"을 의미하며, ROLE_APPLICATION
은 "애플리케이션 개발자(사용자)가 개발하기 위해 작성한 빈"을 의미한다. 반대로 ROLE_INFRASTRUCTURE
는 "스프링 프레임워크가 내부적으로 사용하는 빈"을 의미한다.
🚨 빈 조회 시 발생할 수 있는 주요 예외 상황
빈 조회 과정에서 발생할 수 있는 주요 예외들을 이해하고 적절히 처리하는 것이 중요하다.
NoSuchBeanDefinitionException
발생 원인 : 찾으려는 빈이 스프링 컨테이너에 존재하지 않는 경우 발생한다.
해결 방법 :
- 빈이 제대로 등록(Registered)되었는지 확인한다.
- 컴포넌트 스캔(Component Scan) 대상에 포함되었는지 확인한다.
- 조회 시 사용한 빈 이름이나 타입이 올바른지(Correct) 확인한다.
코드 예시 :
@Test
void noSuchBeanTest() {
// 존재하지 않는 빈 이름으로 조회 시도
assertThrows(NoSuchBeanDefinitionException.class, () ->
ac.getBean("wrongName", MemberService.class));
}
NoUniqueBeanDefinitionException
발생 원인 : 같은 타입의 빈이 스프링 컨테이너에 둘 이상(More Than One) 존재하는 경우 발생한다. 단일 빈 조회를 시도할 때 스프링이 어떤 빈을 주입해야 할지 결정할 수 없을 때 발생한다.
해결 방법 :
- 빈 이름을 지정(Specify Bean Name)하여 조회한다.
- 특정 빈에
@Primary
어노테이션을 사용하여 기본 빈(Primary Bean)으로 지정한다. @Qualifier
어노테이션을 사용하여 빈 이름을 명시적으로 지정하여 조회한다.
코드 예시 :
@Test
void duplicateBeanTest() {
// DiscountPolicy 타입의 빈이 여러 개일 때 예외 발생
assertThrows(NoUniqueBeanDefinitionException.class, () ->
ac.getBean(DiscountPolicy.class));
// 해결 방법 : 빈 이름 지정
DiscountPolicy rateDiscountPolicy =
ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
}
BeanCreationException
발생 원인 : 빈이 생성되는 과정에서 문제가 발생한 경우.
주요 원인 :
- 순환 참조(Circular Dependency) : 빈들 간에 서로를 의존하는 구조가 발생할 때.
- 필수 의존성 누락(Missing Required Dependency) : 특정 빈이 필요로 하는 다른 빈이 컨테이너에 없는 경우.
- 초기화 메서드 실패(Initialization Method Failure) : 빈의 초기화 로직(예 :
@PostConstruct
)에서 예외가 발생한 경우.
해결 방법 :
- 순환 참조(Circular Dependency)를 제거한다.
- 필수 의존성이 컨테이너에 제대로 제공(Provided)되었는지 확인한다.
- 빈의 초기화 로직을 검토(Review)하고 오류를 수정한다.
코드 예시 :
@Test
void beanCreationExceptionTest() {
// 순환 참조가 있는 설정 클래스로 컨텍스트 생성 시도 시 예외 발생
assertThrows(BeanCreationException.class, () ->
new AnnotationConfigApplicationContext(CircularConfig.class));
}
BeanDefinitionStoreException
발생 원인 : 빈 정의(Bean Definition)를 불러오는 과정에서 문제가 발생한 경우.
주요 원인 :
- XML 설정 파일 문법 오류(Syntax Error in XML Configuration) : XML 기반 설정에서 문법적 오류가 있을 때.
- 클래스를 찾을 수 없음(Class Not Found) : 빈 정의에 명시된 클래스를 클래스패스에서 찾을 수 없을 때.
해결 방법 :
- 설정 파일의 문법(Syntax)을 검증한다.
- 클래스패스(Classpath)를 확인하여 필요한 클래스가 올바르게 위치하는지 확인한다.
✅ Assertion을 이용한 빈 검증
빈 조회 테스트에서는 단순히 빈을 조회하는 것을 넘어서, 조회된 빈이 의도한 대로 존재하는지(Existence), 그리고 올바른 타입(Correct Type)인지 검증(Assertion)하는 것이 매우 중요하다. 이를 위해 Assertion(단언) 기능을 활용할 수 있다.
기본적인 빈 검증
containsBean()
메서드로 빈의 존재 여부를 확인하고, getBean()
으로 가져온 빈이 적절한 구현체 타입인지 검증한다.
@Test
void basicBeanAssertion() {
// containsBean()으로 "memberService" 빈이 존재하는지 검증
assertThat(ac.containsBean("memberService")).isTrue();
// getBean()으로 가져온 빈이 MemberServiceImpl 타입의 인스턴스인지 검증
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
빈 수량 검증
getBeansOfType()
을 사용하여 특정 타입의 모든 빈을 조회하고, 그 개수(Count)를 검증하거나, getBeanDefinitionNames()
로 등록된 전체 빈의 개수를 확인한다.
@Test
void beanCountAssertion() {
// getBeansOfType()으로 MemberService 타입의 모든 빈을 조회하여 개수가 1인지 검증
Map<String, MemberService> beansOfType = ac.getBeansOfType(MemberService.class);
assertThat(beansOfType.size()).isEqualTo(1);
// getBeanDefinitionNames()로 등록된 전체 빈 개수가 0보다 큰지 검증
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
assertThat(beanDefinitionNames.length).isGreaterThan(0);
}
예외 상황 검증
특정 예외가 예상대로 발생하는지(Expected Exception)를 검증한다. 이는 예외 처리 로직이 올바르게 작동하는지 확인하는 데 유용하다.
@Test
void exceptionAssertion() {
// 존재하지 않는 빈을 조회할 때 NoSuchBeanDefinitionException 발생 검증
assertThrows(NoSuchBeanDefinitionException.class, () ->
ac.getBean("xxxxx", MemberService.class));
// 동일한 타입의 빈이 여러 개 있을 때 NoUniqueBeanDefinitionException 발생 검증
assertThrows(NoUniqueBeanDefinitionException.class, () ->
ac.getBean(DiscountPolicy.class));
}
Assertion 사용 시 주의사항
- 테스트의 의도(Intention)를 명확히 드러내는 검증문을 작성해야 한다.
- 테스트 실패 시 원인을 쉽게 파악할 수 있는 명확한 오류 메시지(Clear Error Message)를 제공한다.
- 불필요한 검증(Unnecessary Assertion)은 피하고 핵심적인 부분만 검증한다.
- 테스트 코드의 가독성(Readability) 유지를 위해 적절한 들여쓰기와 포맷팅을 사용한다.
'Spring' 카테고리의 다른 글
스프링 프레임워크의 동작 방식 (2) | 2025.06.09 |
---|---|
[3편] 좋은 DI 설계: 스프링의 빈 탐색 원리와 OCP (0) | 2025.06.03 |
[2편] 스프링 DI의 3가지 방식 : 생성자 주입을 써야만 하는 이유 (0) | 2025.06.03 |
[1편] 의존성 주입(DI)과 제어의 역전(IoC), 왜 필요할까? (0) | 2025.06.02 |
스프링 내장 Order 어노테이션 자동 임포트 문제 (0) | 2025.05.27 |