Spring Container
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- ApplicationContext를 스프링 컨테이너라 한다.
- 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제는 스프링 컨테이너를 통해서 사용한다.
MemberService memberServiceBefore = appConfig.memberService(); // 기존 방식 MemberService memberServiceAfter = context.getBean("memberService", MemberService.class); // Spring Container활용
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다.
- 여기서 @Bean이라 적힌 메서드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록
- 이렇게 스프링 컨테이너에 등록된 객체를 Spring Bean이라 한다.
- 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. (memberService, orderService)
- 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다.
- 스프링 빈은 applicationContext.getBean() 메서드를 사용해여 찾을 수 있다.
- 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경하였다.
Spring Container 생성
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- ApplicationContext는 인터페이스이다.
- Spring Container는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
- 직전에 AppConfig를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것
- 자바 설정 클래스를 기반으로 스프링 컨테이너(ApplicationContext)를 만들어보자
- new AnnotationConfigApplicationContext(AppConfig.class);
- 해당 클래스는 ApplicationContext 인터페이스의 구현체다.
- 스프링 컨테이너 생성 과정
- 스프링 컨테이너 생성
- Spring Container 생성(AppConfig.class)
- Spring Container는 AppConfig.class에 구성정보를 활용하여 스프링 빈 저장소를 만든다.
- Spring Container 안에 있는 스프링 빈 저장소에는 빈 이름과 빈 객체로 key, value 형태로 저장된다.
- 스프링 빈 등록
- 스프링 컨테이너에는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
- 빈 이름
- 빈 이름은 기본 메서드 이름을 사용한다.
- 빈 이름을 직접 부여할 수도 있다. ex) @Bean(name=”memberService”)
- 주의!! 빈 이름은 항상 다른 이름으로 부여해야 한다. 같은 이름을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 오류 발생
- 스프링 빈 의존관계 설정 - 준비
- 스프링 빈 의존관계 설정 - 완료
- 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다.
- 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. 이 차이는 뒤에 싱글톤 컨테이너에서 설명한다.
- ex) memberService -> memberRepository <- orderService -> discountPolicy
- memberService에서 memberRepository를 생성한다.
- orderService는 memberRepository와 discountPolicy를 생성한다.
- 스프링 컨테이너 생성
- 스프링 빈 의존관계 설정 준비 및 완료에서 참고할 부분
- 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나눠져 있다.
- 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.
- 여기서는 개념적으로 나누어 설명했다.
컨테이너에 등록된 모든 빈 조회
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 조회")
void findAllBean() {
String[] beanName = ac.getBeanDefinitionNames();
Arrays.stream(beanName).forEach((b) -> {
Object bean = ac.getBean(b);
System.out.println("name: " + b + ", object: " + bean);
});
}
@Test
@DisplayName("애플리케이션 빈 조회")
void findApplicationBean() {
String[] beanName = ac.getBeanDefinitionNames();
Arrays.stream(beanName).forEach((b) -> {
BeanDefinition beanDefinition = ac.getBeanDefinition(b);
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(b);
System.out.println("name: " + b + ", object: " + bean);
}
});
}
}
- findAllBean() -> 모든 빈 출력하기
- 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
- ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회한다.
- ac.getBean(): 빈 이름으로 빈 객체(인스턴스)를 조회한다.
- findApplicationBean() -> 애플리케이션 빈 출력하기
- 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력
- 스프링이 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
- BeanDefinition.ROLE_APPLICATOIN: 일반 사용자가 정의한 빈
- BeanDefinition.ROLE_INFRASTRUCTURE: 스프링 내부에서 사용하는 빈
스프링 빈 조회 - 기본
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회 실패")
void findBeanByNameFailed() {
assertThrows(NoSuchBeanDefinitionException.class, ()-> {
ac.getBean("xxx", MemberService.class);
});
}
@Test
@DisplayName("이름없이 타입으로 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByDefiniteType() {
MemberServiceImpl memberServiceImpl = ac.getBean(MemberServiceImpl.class);
assertNotNull(memberServiceImpl);
}
}
- 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
- ac.getBean(빈이름, 타입)
- ac.getBean(타입)
- 조회 대상 스프링이 빈이 없으면 예외 발생
- NoSuchBeanDefinitionException: No bean named ‘XXXX’ available
스프링 빈 조회 - 동일한 타입이 둘 이상
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("타입 조회 시 중복 타입이 있는 경우 오류 발생")
void findBeanByTypeDuplicate() {
ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
assertThrows(NoUniqueBeanDefinitionException.class, ()-> {
ac.getBean(MemberRepository.class);
});
}
@Test
@DisplayName("타입 조회 시 중복 타입이 있는 경우 이름으로 조회")
void findDuplicateBeanByName() {
ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
assertNotNull(memberRepository);
}
@Test
@DisplayName("특정 타입 모두 조회")
void findAllDuplicateBeans() {
ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
Map<String, MemberRepository> memberRepositories = ac.getBeansOfType(MemberRepository.class);
memberRepositories.keySet().forEach((name) -> {
System.out.println("key: " + name + ", bean: " + memberRepositories.get(name));
});
assertEquals(2, memberRepositories.size());
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new InMemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new InMemoryMemberRepository();
}
}
}
- 같은 타입의 빈이 두개 이상 등록
- NoUniqueBeanDefinitionException 발생, @Qualifier, @Primary 등을 활용하여 해결 필요
- ApplicationContext를 통하여 여러 상황에 맞게 조회할 수 있다.
- 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.
- ac.getBeanOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다. return 타입은 Map<String, Object>
스프링 빈 조회 - 상속 관계
- 부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
- 그래서 Object타입으로 조회하면 모든 스프링 빈을 조회한다.
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixedDiscountPolicy();
}
}
@Test
@DisplayName("부모 타입으로 조회, 자식이 둘 이상 있으면 중복 오류 발생")
void findBeanByParentTypeDuplicate() {
ac = new AnnotationConfigApplicationContext(TestConfig.class);
assertThrows(NoUniqueBeanDefinitionException.class, () -> {
ac.getBean(DiscountPolicy.class);
});
}
@Test
@DisplayName("부모 타입으로 조회, 자식이 둘 이상 있으면 빈 이름을 지정하면 된다.")
void findBeanByParentTypeBeanName() {
ac = new AnnotationConfigApplicationContext(TestConfig.class);
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanByChildType() {
ac = new AnnotationConfigApplicationContext(TestConfig.class);
RateDiscountPolicy rateDiscountPolicy = ac.getBean(RateDiscountPolicy.class);
assertNotNull(rateDiscountPolicy);
}
@Test
@DisplayName("부모 타입으로 자식 타입 모두 조회")
void findAllBeanByParentType() {
ac = new AnnotationConfigApplicationContext(TestConfig.class);
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertEquals(2, beansOfType.size());
beansOfType.keySet().stream().forEach((k)-> {
assertThat(beansOfType.get(k)).isInstanceOf(DiscountPolicy.class);
});
}
@Test
@DisplayName("Object 타입으로 자식 타입 모두 조회")
void findAllBeanByObjectType() {
ac = new AnnotationConfigApplicationContext(TestConfig.class);
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
beansOfType.keySet().stream().forEach((k)-> {
assertThat(beansOfType.get(k)).isInstanceOf(Object.class);
});
}
- TestConfig에서
- findBeanByParentTypeDuplicate: 단순히 부모타입으로 조회하면, 여러 자식타입이 있기 때문에 예외가 발생한다.
- findBeanByParentTypeBeanName: 필요로하는 자식타입을 지정하여 조회
- findBeanByChildType: 구체적인 자식 타입으로 조회
- findAllBeanByParentType: getBeansOfType을 통해 부모 타입으로 모두 조회하여 Map 객체로 가져온다.
- findAllBeanByObjectType: Object 타입으로 조회하면 스프링에 등록된 모든 빈을 가져온다.
BeanFactory와 ApplicationContext
- BeanFactory와 ApplicationContext 상속 구조
- BeanFactory <- ApplicationContext <- AnnotationConfigApplicationContext
- BeanFactory: interface
- ApplicationContext: interface
- BeanFactory <- ApplicationContext <- AnnotationConfigApplicationContext
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스
- 스프링 빈을 관리하고 조회하는 역할을 담당
- getBean()을 제공한다.
- 지금까지 우리가 사용했던 대부분의 기능(빈 조회)은 BeanFactory에서 제공하는 기능
- ApplicationContext
- BeanFactory 기능을 모두 상속받아 제공한다.
- 빈을 관리하고 검색하는 기능을 BeanFactory가 제공하고, 애플리케이션을 개발할 때 필요하는 수 많은 부가기능을 제공한다.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,MessageSource, ApplicationEventPublisher, ResourcePatternResolver
- MessageSource : 국제화 기능
- 국가별 언어를 제공하는 기능
- EnvironmentCapable: 환경 변수
- 로컬, 개발, 운영 등 환경을 구분하여 처리 가능
- ApplicationEventPublisher: 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원
- ResourceLoader: 편리한 리소스 조회
- 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
다양한 설정 형식 지원 - 자바 코드, XML
- 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
- 상속 구조
- BeanFactory <- ApplicationContext <- AnnotationConfigApplicationContext(AppConfig.class), GenericXmlApplicationContext(appConfig.xml), XxxApplicationContext(appConfig.xxx)
- BeanFactory: interface
- ApplicationContext: interface
- BeanFactory <- ApplicationContext <- AnnotationConfigApplicationContext(AppConfig.class), GenericXmlApplicationContext(appConfig.xml), XxxApplicationContext(appConfig.xxx)
- Annotation 기반 자바 코드 설정 사용
- 예제에서 계속 사용
- new AnnotationConfigApplicationContext(AppConfig.class)
- AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.
- XML 설정 사용
- 아직 레거시 프로젝트들에서는 XML로 되어있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있다.
-
GenericXmlApplicationContext를 사용하여 xml 설정파일을 넘기면 된다.
- XmlAppConfig 사용 자바 코드
- 테스트 코드 먼저 작성
- GenericXmlApplicationContext를 사용하여 XML 설정 파일을 읽어와 빈을 구성한다.
@Test void xmlAppContext() { ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml"); MemberService memberService = ac.getBean("memberService", MemberService.class); assertThat(memberService).isInstanceOf(MemberService.class); }
- GenericXmlApplicationContext를 사용하여 XML 설정 파일을 읽어와 빈을 구성한다.
- appConfig.xml
- src/main/resources/appConfig.xml 로, resources 밑에 xml 파일을 둔다. ```xml <?xml version=”1.0” encoding=”UTF-8”?>
```
- 추가적으로 정보가 필요하면 스프링 공식 레퍼런스를 참고
- 테스트 코드 먼저 작성
스프링 빈 설정 메타 정보 - BeanDefinition
- 스프링은 다양한 설정 형식을 지원하기 위해 BeanDefinition이라는 추상화를 사용했다.
- 쉽게 생각하면 역할과 구현을 개념적으로 나눈 것이다.
- xml을 읽어서 BeanDefinition을 만들면 된다.
- 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
- 자바, XML이 아니여도 BeanDefinition을 알면 도니다.
- BeanDefinition을 빈 설정 메타정보라고 한다.
- @Bean,
당 각각 하나씩 메타정보가 생성된다.
- @Bean,
- 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
- Spring Container -> Bean Definition <- AppConfig.class, appConfig.xml, appConfig.xxx
- 코드 레벨로 들어가보자
- Java 기반
- ApplicationContext 구현체인 AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 의존하고 있다.
public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry { private final AnnotatedBeanDefinitionReader reader; private final ClassPathBeanDefinitionScanner scanner; // ...
- AnnotatedBeanDefinitionReader는 AppConfig.class에 설정 정보를 읽어서 BeanDefinition(빈 메타 정보)를 생성한다.
- ApplicationContext 구현체인 AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 의존하고 있다.
- XML 기반
- GenericXmlApplicationContextsms는 XmlBeanDefinitionReader를 의존하고 있다.
- XmlBeanDefinitionReader는 appConfig.xml에 설정 정보를 읽어서 BeanDefinition(빈 메타 정보)를 생성한다.
- Java 기반
- BeanDefinition에 세팅된 정보 확인
public class BeanDefinitionTest { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); @Test @DisplayName("빈 설정 메타정보 확인") void findApplicationBean() { String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); Arrays.stream(beanDefinitionNames).forEach((name) -> { BeanDefinition beanDefinition = applicationContext.getBeanDefinition(name); if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { System.out.println("beanDefinitionName = " + name + ", beanDefinition = " + beanDefinition); } }); } }
- 자체적으로 생성한 빈 확인
// AppConfig.class beanDefinitionName = appConfig, beanDefinition = Generic bean: class [com.example.AppConfig$$EnhancerBySpringCGLIB$$34464e38]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null // MemberService.class beanDefinitionName = memberService, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=memberService; initMethodName=null; destroyMethodName=(inferred); defined in com.example.AppConfig // OrderService.class beanDefinitionName = orderService, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=orderService; initMethodName=null; destroyMethodName=(inferred); defined in com.example.AppConfig
- BeanDefinition 정보
- BeanClassName: 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
- factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
- factoryMethodName: 빈을 생성할 팩토리 메서드 지정 예) memberService
- Scope: 싱글톤(기본값)
- lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아닌, 실제 빈을 사용할 때까지 최대한 생성을 지연처리하는 지 여부
- InitMethodName: 빈을 생성하고, 의존 관계를 적용한 뒤에 호출되는 초기화 메서드 명
- DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
- Constructor arguments, Properties: 의존관계 주입에서 사용 (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
- FactoryBean으로 AppConfig로 등록되어 있다.
- 현재 AppConfig.java에서 등록한 방식처럼 @Bean을 메서드로 등록하는 방식을 FactoryBean 을 통해 등록하는 방식
- BeanDefinition 정보
- 출처: 김영한님의 스프링 핵심 기본편 강의 및 강의자료