특정 스프링 빈 조회
스프링 컨테이너에서 이름이나 타입으로 특정 빈을 조회하는 방법이다.
ApplicationContextBasicFindTest
package spring.basic.beanfind;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AppConfig;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
public class ApplicationContextBasicFindTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("Bean 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
//System.out.println("memberService = " + memberService);
//System.out.println("memberService.getClass() = " + memberService.getClass());
}
@Test
@DisplayName("Bean 이름 없이 Type으로만 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 Type으로 조회")
void findBeanByName2() {
MemberService memberService = ac.getBean(MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("Bean 이름으로 조회 실패")
void findBeanByNameFailed() {
//ac.getBean("hello", MemberService.class);
assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("hello", MemberService.class));
}
}
앞서 사용했던 getBean() 메소드에 스프링 빈 이름과 타입을 명시하면 특정 빈을 조회할 수 있다. 빈 이름은 중복되면 안 되니 이름으로만 검색해도 원하는 빈을 조회할 수 있지만, 타입을 명시함으로 조금 더 정확한 결과를 얻을 수 있다. 또한 타입에 .class를 붙이는 이유는 스프링이 타입을 명확하게 알 수 있어 타입에 맞는 빈을 반환하고, 개발자는 별도의 캐스팅 없이 사용할 수 있기 때문이다.
스프링 빈 조회 시 동일한 타입이 둘 이상일 때
ApplicationContextSameBeenFindTest
package spring.basic.beanfind;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.member.MemberRepository;
import spring.basic.member.MemoryMemberRepository;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ApplicationContextSameBeenFindTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("같은 Type이 둘 이상 있으면 중복 오류 발생")
void findBeanByTypeDuplicate() {
//MemberRepository bean = ac.getBean(MemberRepository.class);
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("같은 Type이 둘 이상 있을땐 Bean 이름을 지정")
void findBeanByName() {
MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
@Test
@DisplayName("같은 Type을 모두 조회")
void findAllBeanByType() {
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType.size()).isEqualTo(2);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
}
우선 기존에 사용하던 AppConfig에는 중복된 타입이 없어서 테스트 코드에 @Configuration 어노테이션을 단 static 클래스로 설정 역할을 할 SameBeanConfig를 작성하고, ApplicationContext 생성 시 인자로 넘긴다.
해당 테스트 코드를 실행시키면 스프링 빈 팩토리에는 memberRepository1과 memberRepository2, 두 개의 빈이 저장된다. 이 상태에서 getBean() 메소드로 MemberRepository.class 타입의 빈을 조회하면 NoUniqueBeanDefinitionException이 발생하게 된다. 즉, 조회 결과가 유니크하지 않아서 발생하는 오류다.
같은 타입의 빈을 조회하려면 getBeansOfType() 메소드를 사용해서 조회하며, 결과는 Map<String(빈 이름), Type> 형태로 반환된다. 해당 테스트 실행 결과는 아래와 같다.
key = memberRepository1 value = spring.basic.member.MemoryMemberRepository@4879dfad
key = memberRepository2 value = spring.basic.member.MemoryMemberRepository@4758820d
beansOfType = {memberRepository1=spring.basic.member.MemoryMemberRepository@4879dfad, memberRepository2=spring.basic.member.MemoryMemberRepository@4758820d}
스프링 빈 상속 관계 조회
ApplicationContextExtendsFindTest
package spring.basic.beanfind;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.discount.DiscountPolicy;
import spring.basic.discount.FixDiscountPolicy;
import spring.basic.discount.RateDiscountPolicy;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ApplicationContextExtendsFindTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다")
void findBeanByParentTypeDuplicate() {
//DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 Bean 이름을 지정한다")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 Type으로 조회")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 Type으로 모두 조회")
void findAlllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
}
@Test
@DisplayName("부모 Type으로 모두 조회하기(Object)")
void findAllBeanByObjectType() {
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
기본적으로 부모 타입으로 조회하면 자식 타입도 함께 조회된다. 그래서 모든 자바 객체의 부모인 Object 타입으로 조회하면 모든 스프링 빈이 조회된다. 이번에도 TestConfig를 작성하여 인자로 넘겼기 때문에, getBean() 메소드로 DiscountPolicy.class 타입 조회 시NoUniqueBeanDefinitionException이 발생하게 된다. 따라서 특정 자식 타입으로 조회하거나, getBeansOfType() 메소드로 조회하면 된다.
BeanFactory와 ApplicationContext
그림과 같이 BeanFactory는 스프링 컨테이너의 최상위 인터페이스이며, ApplicationContext는 BeanFactory를 상속한 인터페이스이다. AnnotationConfigApplicationContext 등은 ApplicationContext를 상속한 구현체이다. 각각의 역할과 특징은 아래와 같다.
- BeanFactory의 역할과 특징
- 스프링 컨테이너의 최상위 인터페이스
- 빈의 생성, 조회, 의존성 주입 등 기본적인 기능 제공
- 위에서 사용한 getBean 메소드 제공
- ApplicationContext의 역할과 특징
- BeanFactory를 상속한 인터페이스
- BeanFactory의 모든 기능을 포함하며 애플리케이션에 필요한 다양한 부가 기능 제공
- 메시지 소스(MessageSource) : 국제화 지원
- 이벤트 발행(ApplicationEventPublisher) : 이벤트 기반 프로그래밍 지원
- 리소스 로딩(ResourcePatternResolver) : 다양한 경로의 리소스 로딩
- 환경 변수 관리(EnvironmentCapable)
- 계층적 빈 팩토리(HierarchicalBeanFactory)
- 어노테이션 기반 DI 지원, 모든 빈 스코프 지원
XML 기반의 설정
앞서 언급했듯 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정을 읽을 수 있도록 유연하게 개발되었고, 지금까지는 대부분 어노테이션 기반의 AnnotationConfigApplicationContext를 사용해 Java로 설정 파일을 만들었다. 최근에는 XML 기반의 설정을 잘 사용하지 않지만 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있다. GenericXmlApplicationContext를 사용한다.
appConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="memberService" class="spring.basic.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository" class="spring.basic.member.MemoryMemberRepository" />
<bean id="orderService" class="spring.basic.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
<constructor-arg name="discountPolicy" ref="discountPolicy" />
</bean>
<bean id="discountPolicy" class="spring.basic.discount.RateDiscountPolicy" />
</beans>
XmlAppContext
package spring.basic.xml;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import spring.basic.member.MemberService;
import static org.assertj.core.api.Assertions.assertThat;
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
테스트 코드를 보면 알 수 있듯이 appConfig.xml과 계속 사용하던 AppConfig.class는 거의 유사하다.
스프링 빈 설정 메타 정보(BeanDefinition)
스프링 컨테이너가 다양한 형식의 설정을 읽을 수 있는 이유는 BeanDefinition이라는 빈의 생성, 설정, 관리에 필요한 메타정보를 담아두는 추상화가 있기 때문이다. 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정 파일을 읽어 BeanDefinition 객체로 변환한 뒤, 이 메타 정보를 기반으로 실제 빈 객체를 생성하고 관리한다. 각 @Bean 또는 태그마다 하나의 BeanDefinition이 생성된다.
방금 말한 것과 같이 BeanDefinition의 동작 흐름은 1. 개발자가 Java, XML, Groovy 등 설정 파일을 통해 빈을 정의 → 2. 스프링 컨테이너가 설정 정보를 읽어(각 형식에 맞는 Reader가 이 역할을 한다) BeanDefinition 객체로 변환 → 3. BeanDefinition에 담긴 메타 정보를 참고해 빈을 생성, 의존성 주입, 라이프사이클 관리 등을 수행하게 된다.
BeanDefinition이 담고 있는 주요 정보는 아래와 같다.
- beanClassName : 생성할 빈의 클래스 명
- factoryBeanName : 빈을 생성하는 팩토리 역할의 빈 이름
- factoryMethodName : 빈을 생성하는 팩토리 메소드 이름
- scope : 빈의 스코프
- lazyInit : 지연 초기화 여부(빈을 실제 사용할 때까지 생성 지연)
- initMethodName : 빈 초기화 메소드 이름
- destroyMethodName : 빈 소멸 전 호출할 메소드 이름
- constructor arguments, properties : 빈 생성 시 주입할 생성자 인자와 프로퍼티 값
- autowireCandidate, primary : 자동 주입 대상 여부, 우선순위
하지만 실무에서 BeanDefinition을 직접 다룰 일은 거의 없고 자동으로 생성, 관리되는대로 사용한다고 한다.
'스터디 > Spring' 카테고리의 다른 글
Spring 스터디2 - 7 (0) | 2025.04.29 |
---|---|
Spring 스터디2 - 6 (0) | 2025.04.27 |
Spring 스터디2 - 4 (1) | 2025.04.17 |
Spring 스터디2 - 3 (0) | 2025.04.13 |
Spring 스터디2 - 2 (0) | 2025.03.30 |