1. 스프링 컨테이너 생성
이전 글에서 스프링을 쓰기위해 스프링 컨테이너인 ApplicationContext를 아래와 같이 선언했다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
이때 ApplicationContext는 하나의 인터페이스이고, 이 인터페이스를 구현한 클래스 중 하나가 AnnotationConfigApplicationContext이다. 이와 같은 방식은 @Configuration이라는 자바 어노테이션을 기반으로 스프링 컨테이너를 만드는 방식이다. 이외에도 XML을 기반으로도 만들 수 있다. 하지만 최근에는 XML을 기반으로 만드는 방식은 잘 사용하지 않는다. 왜냐하면 스프링 부트 자체가 어노테이션 기반으로 편리하게 작동되도록 되어있기 때문이다. 즉, 최근에는 보통 어노테이션 방식을 사용한다.
- 스프링 컨테이너를 부를 때 보통 BeanFactory와 ApplicationContext로 구분하여 이야기한다. 하지만 BeanFactory는 직접 사용하는 경우가 거의 없어서 일반적으로 ApplicationContext를 스프링 컨테이너라고 부른다.
스프링 컨테이너 생성 과정을 아래 그림을 통해 살펴보자.
먼저 ApplicationContext를 생성한다. 이때 AnnotationConfigApplicationContext에 AppConfig의 정보를 전달한다. 이후 스프링 컨테이너가 만들어지고 이 안에 스프링 빈 저장소라는 것이 있다. 이후 구성 정보를 활용하여 key는 빈 이름이 되고 값은 빈 객체가 된다. (A라는 메서드가 B를 반환하면 A는 빈의 이름, B가 빈 객체가 된다.) 이때는 AppConfig를 전달했으니, AppConfig를 구성 정보로 활용하게 된다.
- 구성 정보를 활용하여 빈을 등록하게 되는데 이때 @Bean으로 등록한 녀석들을 모두 스프링 빈으로 등록한다.
- 만약 메서드 이름이 아니라 다른 이름을 사용하고 싶다면 @Bean(name="{원하는 이름}")으로 어노테이션을 사용하면 된다.
구성 정보를 활용해 빈을 모두 등록했다면 의존 관계 설정을 준비한다. 이때도 설정 정보를 참고하여 (AppConfig를 참조하여) 의존 관계를 주입한다. 단, 이때 싱글톤 방식을 사용한다는 것이 단순히 자바 코드를 호출하는 것과의 차이이다 .
2. 모든 빈 조회
모든 빈을 조회하려면 아래와 같은 테스트 코드를 사용할 수 있다.
public class ApplicationContextInfoTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력")
void findAllBean() {
// 스프링 내부에서 자동으로 생성되는 빈도 출력됨.
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = "+beanDefinitionName+" / object = "+bean);
}
}
}
- ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회함.
3. 빈 조회 - 기본
스프링 컨테이너에서 스프링 빈을 조회하는 가장 간단한 방법은 getBean을 사용하는 방법이다.
- ac.getBean(name, type)
- ac.getBean(type)
위 두 방식 중 하나를 선택하여 사용하면 된다. 예시 코드는 아래와 같다.
public class ApplicationContextBasicFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 조회 방법 1 (이름, 타입)")
void findByNameAndType(){
MemberService memberService = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 조회 방법 2 (타입)")
void findByInterfaceType(){
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 조회 방법 3 (타입 [구체 타입으로])")
void findByImplType(){
MemberService memberService = ac.getBean(MemberServiceImpl.class);
Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("존재하지 않는 빈 조회")
void findNotRegiteredBean(){
org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("notRegisteredBean"));
}
}
- isInstanceOf(MemberService.class) 도 동일하게 테스트를 통과한다.
- 마지막 예시 코드를 보면 존재하지 않는 빈을 조회하면 무조건 에러가 발생한다. 이와 같이 에러가 무조건 발생하는 상황에서 예상되 에러가 발생하는지 확인할 때에는 assertThrows() 메서드를 사용한다.
- 단, 주의할 점은 같은 타입으로 지정된 동일한 빈이 있다면 타입만 전달시 에러가 발생한다는 것이다. 이런 경우 빈의 이름을 함께 넘겨줘야 한다. 만약 같은 타입으로 지정된 동일한 빈을 확인하고 싶다면 아래와 같이 조회해볼 수 있다.
4. 빈 조회 - 상속 관계
스프링 빈을 부모 타입으로 조회하면, 자식 타입도 같이 조회한다. 즉, 1이 부모고 2와 3이 자식일 때 1을 조회하면 2, 3을 모두 조회한다. 따라서 모든 자바 객체의 최고 부모인 Object 타입을 조회하면 모든 스프링 빈을 조회할 수 있다. 아래는 예시 코드다.
public class ApplicationContextExtendsFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입 조회시 여러 자식이 있으면 중복 오류 발생")
void findBeanByParentTypeDuplicate() {
// DiscountPolicy 인터페이스 객체 조회시 두 객체가 조회되어 중복 오류 발생
Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 모두 조회하면 2개의 빈이 조회되어야 한다.")
void findAllBeanByParentType(){
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
}
// DiscountPolicy 인터페이스의 구현 클래스 두 개를 각각 빈으로 등록함.
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
대신 아래와 같이 부모 타입(인터페이스)을 통해 자식 타입을 가져올 때 사용할 수도 있다.
@Test
@DisplayName("부모 타입으로 조회시, 원하는 자식을 가지는 빈 이름을 등록하면 된다.")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
}
보통 위와 같이 인터페이스를 통해 조회하는 방법이 하위 클래스(RateDiscount.class)로 조회하는 방식보다 좀 더 좋다. (하위 클래스로 조회하는 방법은 구현체에 의존하기 때문이다.)
5. BeanFactory와 ApplicationContext
스프링 컨테이너의 최상위 인터페이스로 BeanFactory가 있고, 이 인터페이스를 상속받은 ApplicationContext 인터페이스가 있다. 그래서 이 ApplicationContext란 BeanFactory에 부가 기능을 추가한 것이다. 그리고 ApplicationContext 아래에 AnnotationConfigApplicationContext와 같은 구현 클래스가 있다. BeanFactory와 ApplicationContext의 특징은 다음과 같다.
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스
- 스프링 빈을 관리하고 조회하는 역할을 하며, getBean()을 제공함.
- ApplicationContext
- BeanFactory의 기능을 모두 상속받아 제공함.
- 빈을 관리하고 검색하는 기능은 BeanFactory가 제공함. 하지만 어플리케이션을 개발할 때는 빈을 관리하고, 조회하는 기능말고도 여러 기능이 필요한데 이러한 기능을 추가로 제공하는 것이 ApplicationContext임.
- 이때 추가 기능으로 MessageSource, EnvironmentCapable, ApplicationEventPublisher, ResourceLoader 같은 것들이 존재함. 이 이러한 기능들은 모두 인터페이스임.
- MessageSource (메시지소스를 활용한 국제화 기능): 한국에서 들어오면 한국어, 영어권에서 들어오면 영어로 출력함.
- EnvironmentCapable (환경변수): 로컬, 개발, 운영 등을 구분해서 처리함.
- ApplicationEventPublisher (애플리케이션 이벤트): 이벤트를 발행하고 구독하는 모델을 편리하게 지원함.
- ResourceLoader (편리한 리소스 조회): 파일, 클래스패스, 외부 등에서 리소스를 편하게 조회함.
정리를 하면 ApplicationContext는 BeanFactory의 기능을 상속받는다. 그리고 ApplicationContext는 BeanFactory의 빈 관리기능과 추가적인 기능들을 제공한다. 따라서 거의 BeanFactory를 사용하지 않고 ApplicationContext를 사용한다.
- BeanFactory나 ApplicationContext 둘 다 스프링 컨테이너라고 부른다.
6. 다양한 설정 형식 지원 - 자바 코드, XML
스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되었다.
우리가 사용하던 ApplicaitonContext를 구현한 것 중 하나가 AnnotationConfigApplicationContext이다. 해당 구현 클래스는 자바 코드의 어노테이션을 사용해 환경을 구성한다. 또한 GenericXmlAplicationContext는 XML이라는 문서를 설정 정보로 사용한다. 그리고 임의의로 우리가 구현하여 따로 만들어 사용할 수도 있다. 최근에는 스프링 부트를 많이 활용하여 보통 자바 코드를 활용한다. xml 파일은 다음과 같이 <bean>을 통해 빈을 등록할 수 있다. 만약 생성자로 전달해야할 인자들이 있다면 <constructor-arg>로 하나씩 넘겨주면 된다. (2개라면 2개의 <constructor-arg>로 넘긴다.)
<?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="com.example.projectEx1.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
</bean>
<bean id="memberRepository" class="com.example.projectEx1.member.MemoryMemberRepository"/>
</beans>
해당 xml 파일의 설정 내용은 아래 자바 코드와 동일하다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
아래와 같은 테스트 코드를 동작시키면 잘 동작하는 것을 확인할 수 있다.
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService ms = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(ms).isInstanceOf(MemberService.class);
}
}
7. 스프링 빈 설정 메타 정보 - BeanDefinition
위와 같이 스프링은 다양한 설정 형식을 지원할 수 있는 이유는 "BeanDefinition"이라는 추상화 때문이다. 스프링 컨테이너는 자바 코드든, xml이든 단지 BeanDefinition을 알면 된다. (스프링 컨테이너는 AppConfig.class나 appConfig.xml이 아니라 BeanDefinition이라는 하나의 추상화에 의존한다. 즉, BeanDefinition 자체가 하나의 인터페이스다.) 이 BeanDefinition을 "빈 설정 메타 정보"라고 하며 "@Bean"혹은 "<bean>" 하나당 하나의 메타 정보가 생성된다. 스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다. 이름 그림으로 나타내면 다음과 같다.
- AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용하여 AppConfig.class를 읽고 BeanDefinition을 생성한다.
- GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용하여 AppConfig.xml을 일고, BeanDefinition을 생성한다.
- 다른 파일을 설정 파일로 사용하고 싶다면, 해당 파일을 읽을 수 있는 xxxBeanDefinitionReader를 따로 만들어서 BeanDefinition을 생성하도록 하면 된다.
아래 코드를 실행하면 내가 만든 스프링 빈들의 BeanDefintion을 조회해볼 수 있다.
public class BeanDefinitionTest {
// 만약 AnnotationConfigApplicationContext가 아닌 ApplicationContext로 하면 getBeanDefinition을 할 수 없다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 설정 메타 정보 확인")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames){
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
System.out.println("beanDefinitionName = " + beanDefinitionName +
" / beanDefinition = " + beanDefinition);
}
}
}
}
위처럼 BeanDefinition들을 조회해보면 scope, abstract, autowireMode 등 여러 메타 정보들을 살펴볼 수 있다.
- 일반적으로 AppConfig.class와 같은 자바 클래스로 빈을 생성하는 방식을 factoryBean을 사용한 방식이라고 한다. 또한 이 안에서 메서드를 사용해 빈을 생성할 텐데 memberService 등의 메서드를 사용하여 bean을 생성하는 방법을 factoryMethod를 사용한 방법이라고 한다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 컴포넌트 스캔 (0) | 2023.08.28 |
---|---|
[Spring] 싱글톤 컨테이너 (0) | 2023.08.23 |
[Spring] IoC, DI, 컨테이너 (0) | 2023.08.20 |
[Spring] DIP 위반 문제 해결 (DI의 등장) (0) | 2023.08.20 |
[Spring] test 코드 작성 기초 (0) | 2023.08.18 |