ApplicationContext를 스프링 컨테이너라고 한다.
자바로만 개발했을 시점에서 개발자가 AppConfig라는 클래스를 통해 객체를 생성하고 DI 해주었지만 이제는 스프링 컨테이너를 사용한다.
스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다.
설정 정보 클래스인 AppConfig클래스 안에 @Bean이 적힌 메서드를 모두 호출한 뒤 반환된 객체를 스프링 컨테이너에 등록한다. 등록된 객체를 스프링 빈이라고 한다.
스프링 빈은 @Bean이 붙은 메서드 명을 스프링 빈의 이름으로 사용한다.
이전에 개발자가 AppConfig를 사용해 필요한 객체를 직접 조회했지만, 이제는 스프링 컨테이너를 통해 스프링 빈을 찾아야 한다.
스프링 컨테이너의 생성 과정
public static void main(String[] args){
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(AppConfig.class);
}
ApplicationContext를 스프링 컨테이너라고 하고 이것은 인터페이스이다. 이 인터페이스를 구현한 클래스 중 하나가 위의 AnnotationConfigApplicationContext이다.
스프링 컨테이너는 xml 혹은 애노테이션 기반 자바 설정 클래스로 만들 수 있다.
스프링 컨테이너는 BeanFactory와 ApplicationContext로 구분해서 얘기한다. BeanFactory를 직접 사용하는 일은 거의 없으므로 일반적으로 ApplicationContext를 스프링 컨테이너라고 한다.
1. 스프링 컨테이너 생성
new AnnotationConfigApplicationContext(AppConfig.class);
스프링 컨테이너를 생성할 때 구성정보를 지정해주어야 한다.
스프링 컨테이너에는 bean : 객체로 key:value형식으로 되어있는 스프링 저장소가 들어있다.
2. 스프링 빈 등록
AppConfig.class의 @Bean들을 찾아 메서드를 호출한 뒤 생성되는 객체들을 스프링 빈으로 등록한다.
key값에는 메서드의 이름 , value는 반환하는 객체가 등록되어있다.
@Bean(name = "memberService2")
빈 이름의 기본 값은 메서드 이름이지만 위처럼 직접 부여할 수도 있다.
참고로 빈 이름은 항상 다르게 부여해야 한다. 같은 이름을 부여하면 다른 빈이 무시되거나, 기존 빈을 덮어버리거나, 설정에 따라 오류가 발생한다. (헷갈리게 하지 말자)
3. 스프링 빈 의존 관계 설정
스프링 컨테이너가 스프링 빈을 등록한 뒤에는 의존 관계를 넣어주어야 한다.
설정 정보를 참고하여 memberService와 orderService의 의존 관계에 memberRepository를 넣어준다.
단순히 자바 코드를 호출하는 것 같지만 차이가 있다. 어떤 차이인지는 곧 설명할 것이다.
스프링은 빈 생성과 의존 관계 주입 단계가 나눠져 있다.
스프링 빈 기본 조회
나의 콩들이 잘 등록되었나 살펴보자.
class ApplicationContextInfoTest {
AnnotationConfigApplicationContext 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);
}
}
}
위의 테스트 메서드로 스프링 컨테이너에 있는 모든 빈을 출력할 수 있다.
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
// ROLE_INFRASTRUCTURE: 스프링 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + ", object = " + bean);
}
}}}
스프링 내부에서 쓰는 빈을 제외하고 내가 직접 등록한 빈만 출력할 수도 있다.
getRole()을 이용하여 구분할 수 있다.
- getRole(ROLE_APPLICATION) : 직접 등록한 애플리케이션 빈 (사용자가 정의함)
- getRole(ROLE_INFRASTRUCTURE) : 스프링 내부에서 사용하는 빈
조회대상 스프링 빈이 없으면 NoSuchBeanDefinitionException: No Bean named '***' available 예외가 발생한다.
ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
ac.getBean(빈 이름, 타입);
ac.getBean(타입);
getBean을 통하여 빈 이름으로 조회할 수도 있다.
*구체 타입으로 조회도 가능하다.
기본적으로 인터페이스로 조회하면 인터페이스의 구현체가 조회 대상이 된다. isInstanceOf()에 구현체를 넣어 사용할 수 있다.
예를 들어 memberService라는 인터페이스가 AppConfig에서 MemberService를 반환하지만 실제로는 MemberServiceImpl이라는 인스턴스를 보고 결정하기 때문에 구체 클래스로도 조회할 수 있다.
그러나 구체 타입으로 조회하면 역할과 구현을 구분하는 일이 힘들어진다. 또한 유연성도 떨어지므로 별로 좋지 않은 방법이다.
*ac.getBeansOfType()을 이용하면 해당하는 타입의 빈들을 모두 조회할 수 있다.
그러나 같은 타입이 둘 이상일 때는 조회 시 오류가 난다. 빈 이름을 지정해주어 헷갈리지 않도록 하자.
BeanFactory와 ApplicationContext
BeanFactory
- 스프링 컨테이너의 최상위 인터페이스
- 스프링 빈을 관리하고 조회하는 역할
- getBean()을 제공한다.
- 지금까지 사용했던 대부분의 기능이 빈 팩토리의 기능이다.
ApplicationContext
- 빈 팩토리 기능을 모두 상속받아서 제공한다.
- BeanFactory의 빈 관리기능 + 추가적인 부가기능을 제공한다.
- 애플리케이션을 개발 시 빈 관리, 조회에 추가로 필요한 수많은 부가 기능을 가지고 있다.
ApplicationContext의 부가기능
- 메시지 소스를 활용한 국제화 기능
한국에서 요청 시 한국어로, 영어권에서 요청시 영어로 출력 - 환경 변수
로컬, 개발, 운영 등을 구분하여 처리한다. - 애플리케이션 이벤트
이벤트를 발행하고 구독하는 모델을 편리하게 지원한다. - 편리한 리소스 조회
파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회할 수 있다.
다양한 설정 형식 지원 - 자바 코드, XML
스프링 컨테이너는 다양한 형식의 설정 정보를 받을 수 있도록 유연하게 설계되어 있다.
예) 자바 코드, xml, Groovy 등..
애너테이션 기반 자바 코드 설정
AnnotationConfigApplicationContext(AppConfig.class)로 자바 코드로 된 설정 코드를 넘긴다.
XML 설정
스프링 부트가 보편화되면서 xml설정은 잘 사용하지 않는다. 컴파일 없이 빈 설정 정보를 바꿀 수 있는 장점이 있다.
GenericXmlApplicationContext에 xml 설정 파일을 넘기는 방식이다.
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac =
new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService =
ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
<?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="hello.core.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
</bean>
<bean id="memberRepository"
class="hello.core.member.MemoryMemberRepository"/>
<bean id="orderService" class="hello.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
<constructor-arg name="discountPolicy" ref="discountPolicy"/>
</bean>
<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
</beans>
언젠간 써먹을지도 모르니 소스로 남겨두겠다.
스프링 빈 설정 메타 데이터 - BeanDefinition
스프링이 다양한 설정 형식을 지원할 수 있는 것은 BeanDefinition이라는 추상화 덕분이다.
역할과 구현을 개념적으로 나눴다고 보면 된다.
- XML을 읽어서 BeanDefinition을 만든다.
- 자바 코드를 읽어서 BeanDefinition을 만든다.
그럼 스프링 컨테이너는 자바 코드인지 xml인지 알 필요 없이 오직 BeanDefinition만 알면 된다. 그리고 이걸 기반으로 빈 정보를 생성한다.
BeanDefinition을 빈 설정 메타 정보라고 하는데 @Bean, <bean> 당 각각 하나씩 메타 정보가 생성된다.
코드 레벨로 들어가 보자.
AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해 AppConfig.class의 설정 정보를 읽고 BeanDefinition을 생성한다.
xml도 같은 방식으로 자신에게 맞는 리더와 설정 정보 파일을 읽어 들여 빈 메타정보를 생성한다.
BeanDefinition 살펴보기
public class BeanDefinitionTest {
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);
}
}
}
}
이 테스트 코드를 돌려보자.
- BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
- factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름
예) appConfig - factoryMethodName: 빈을 생성할 팩토리 메서드 지정
예) memberService
*AppConfig처럼 메서드를 통해 빈을 등록하는 방식을 팩토리메서드방식이라고 한다.
외부에서 해당 메서드를 호출하여 주입하는 방식이다. - Scope: 싱글톤(기본값)
- lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
- InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
- DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
- Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의빈을 사용하면 없음)
정리
BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다. 하지만 실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다고 영한쌤이 그러셨다.
BeanDefinition에 대해서는 너무 깊이 있게 이해하기보다는,, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.
가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼 때, BeanDefinition이라는 것이 보일 때가 있다. 이때 이러한 메커니즘을 떠올리면 된다.
'Spring' 카테고리의 다른 글
싱글톤 컨테이너 (2) | 2022.10.04 |
---|---|
JPA - 영속성관리 (1) | 2022.09.30 |
1. JPA 실습 - 프로젝트 설정 및 JPA 기초 개념 (1) | 2022.09.30 |
JPA가 뭘까? (0) | 2022.09.29 |
Spring-OOP와 스프링 (0) | 2022.09.25 |