728x90
0. 흐름
- AppConfig 변화
- AppConfig의 return type이 구체 클래스여도 작동에는 문제가 없지만 설정파일에서는 역할과 구현이 분리가 되어야 한눈에 파악이 쉽기 때문에 스프링 추상클래스로 나타낸다.
@Configuration
public class AppConfig{
// service
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public Orderservice orderService(){
return new OrderServiceImpl(memberRepository(), discoutnPolicy())
}
// 객체
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository(); // 변경할 수 있는 부분
}
@Bean
public DiscountPolicy discoutnPolicy(){
return new FixDiscountPolicy(); // 변경할 수 있는 부분
}
}
- 스프링에서는 application이 동작할 때 자동으로 되는 부분들이 존재
- @SpringBootApplication이 스프링 컨테이너 생성하고 자동으로 @Configuration 찾아서 springContainer에 등록
- @SpringBootApplication이 스프링 컨테이너 생성하고 자동으로 @ComponentScan을 찾아서 springContainer에 등록
- 새로운 springContainer를 등록할 수 있지만 권장하지 않는다.
- test할 때만 임시적으로 springContainer 생성
1. 스프링 컨테이너와 스프링 빈
스프링 컨테이너 생성 방법 종류
- Annotation 기반 자바 설정 클래스 : default
- XML 기반
1.1. 스프링 컨테이너 생성
// 직접 springContainer 생성하는 code
// java code 이용한 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
- ApplicationContext: 스프링 컨테이너, 인터페이스
- AnnotationConfigApplicationContext: 스프링 컨테이너 인터페이스의 구현체
- 스프링 컨테이너 = BeanFactory + ApplicationContext를 상속받는 컨테이너
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스, 스프링 빈을 관리하고 조회하는 역할
- ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공
- 빈 관리 기능 + 수 많은 부가기능을 가짐 = BeanFactory를 직접 사용할 일은 거의 없다.
- 부가기능: 메시지소스 활용한 국제화, 환경변수, App 이벤트, 편리한 리소스 조회
- BeanFactory
- 스프링 컨테이너 상속관계도
- ApplicationContext는 BeanFactory의 기능을 상속받는다
- ApplicationContext는 빈 관리기능 + 부가 기능을 제공
- BeanFactory를 직접 사용하는 일은 거의 없다.
- ApplicationContext, BeanFactory 둘 다 springContainer이다.
- 스프링 컨테이너 생성 과정
-
- 스프링이 올라올 때 자동으로 생성
- @Configuration이 설정된 class file을 찾아서 전부 초기화 값으로 등록
- @Configuration 없으면 아무것도 없는 SpringContainer만 존재
- @Component 파일도 찾아서 등록( class 레벨 )
- 스프링 컨테이너 생성 개념 주의
- test code에서는 new AnnotationConfigApplicationContext() 해서 springContainer를 생성했지만 실제 spring을 동작하게 되면 자동으로 생성되기 때문에 실제로 container instance를 만들 필요가 없다.
- 내부에서 이런 식으로 생성되는구나 정도로만 이해하기
1.3. 스프링 빈 등록
- 스프링 컨테이너 생성 후 초기화 값으로 들어간 @Configuration 파일 조회
- @Bean 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록
- 빈 이름
- 메서드 이름을 사용
- default 값이다.
- default 값이다.
- 직접 부여 가능
- @Bean(name: "빈 이름 등록")
- 어지간하면 사용 안 하는 것을 추천 - 나중에 이름 중복 error 발생 가능성이 있다.
- 메서드 이름을 사용
- 빈 이름은 항상 다른 이름이여야지 관련 error를 최대한 막을 수 있다.
- 직접 빈이름을 등록하면 중복 이름이 생길 가능성이 생긴다.
- 설정 파일이 1개일 때는 메서드 이름 사용 시 중복되는 이름이 생길 수 없는데 설정 파일이 여러 개일 때는 중복 가능성이 존재한다.
- 결론 : 모든 빈의 이름을 다 다르고 명확하게 짓기
- 빈 이름
1.4. 스프링 빈 의존관계 설정
- 스프링 컨테이너 내부 동작 규칙
- 1) 빈 등록 2) 빈 의존관계 주입 단계로 나눠져 있다.
- 빈 등록 싹다하고 이후에 의존관계 주입이 원칙. 생성자로 생성시에는 동시에 수행
- 스프링 컨테이너 생성 및 의존관계 주입 방식에 따라서 차이가 존재
- Spring Cycle : 2단계로 분리
- Spring 자체적 작동 원리가 2단계로 분리되어 있다.
- Java code : 동시에 처리
- 생성자 주입을 통한 방식의 DI를 하는 모든 것은 동시에 처리된다.
- @Component : 빈 등록, @AutoWired : 빈 의존관계 주입 -> DI 동시 처리
- @Configuration의 @Bean으로 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계주입도 한 번에 처리
- 생성자 주입을 통한 방식의 DI를 하는 모든 것은 동시에 처리된다.
- Spring Cycle : 2단계로 분리
- 해당 차이점을 알아야 하는 이유
- 싱글톤이나 의존관계 자동주입에서 위의 개념의 차이를 알아야 한다.
- 스프링 핵심 원리 이해 4 - 싱글톤 컨테이너
- 스프링 핵심 원리 이해 6 - 의존관계 자동 주입
2. 빈 조회
- 실제 어플 내부적으로는 사용하지 않지만 알아야 하는 이유
- 기본기능
- 자동관계 의존 주입 시 빈의 조회 중 부모 자식 간의 관계를 알아야 한다.
- 순수 자바로 스프링 컨테이너 생성해서 사용하는 경우가 존재하는데 이 때는 springContainer 생성과 빈 조회를 수동을 진행해야 한다.
- 더보기
스프링 애플리케이션을 개발할 시, @Autowired나 생성자 주입 등을 통해 스프링 컨테이너에서 필요한 빈을 가져오는 것이 일반적이다.
이 방법들은 자동으로 스프링 컨테이너에서 해당 빈을 가져와서 주입해 주기 때문에 개발자가 별도로 getBean() 메서드를 호출하여 빈을 가져올 필요가 없다.
하지만 특별한 경우에는 getBean() 메서드를 사용해야 한다.1) 스프링 컨테이너가 아닌 외부 라이브러리나 프레임워크에서 스프링 빈을 가져와야 하는 경우
2) 동적으로 빈을 생성하고 관리해야 할 때 getBean()을 사용
하지만 @Autowired나 생성자 주입 등을 사용하여 스프링 컨테이너에서 필요한 빈을 가져오는 것을 권장
- 조회 시, 사용하는 기본 method 및 중복으로 bean이 호출되는 경우
- getBean(classType)
- 매개변수인 classType은 구체타입, 추상타입 다 가능하지만, 구체타입은 유연성이 떨어지므로 권장 안함
- 주의 : 해당 classType의 모든 bean을 반환
- classType으로 bean 호출 시 중복으로 bean이 나오는 경우
- 해당 bean type이 동일할 때
- classType의 자손 class type들이 등록 된 경우
- SpringApplicationContext 상수
- ROLE_APPLICATION : 직접 등록한 빈
- ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
- getBean(classType)
// 1. spring 등록된 All bean 조회
String[] getBeanDefinitionNames();
// 2.등록된 spring bean의 instance 반환
// 2.1. 등록된 spring bean 중 해당 class의 bean instance 반환
Object getBean(classType); // ex) getBean(Service.class), getBean(serviceImple.class)
// 동일 classType의 bean이 여러개 존재 시 사용
// 2.2. 해당 클래스에 여러개의 bean 1개의 bean만 반환
Object getBean(String beanName, classType);
// 2.3. 해당 클래스에 여러개의 bean 존재시 모든 빈 반환
Map<String,classType> getBeansOfType(classType);
// 3. 해당 bean instance의 meta 정보 반환
getBeanDefinition(String beanName);
// 3.1. meta 정보 반환된 객체에서 사용 가능 | bean meta 정보 중 어디서 등록된 빈인지 정보 반환
getRole(); // ROLE_APPLICATION 혹은 ROLE_INFRASTRUCTURE 반환
// 3.2. BeanDefinition : 해당 상수가 존재하는 class
beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE
// 4. 메타정보 정의들이 존재
getBeanDefinition(beanNames)
2.1. All bean 조회, App bean 조회
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);
}
}
@Test
@DisplayName("app bean만 출력하기")
void findApplicationBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName: beanDefinitionNames){
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// getBeanDefinition: getRole()로 구분하기 위해 필요한 Bean에 관한 meta data 정보 반환
// Role ROLE_APPLICATION: 직접 등록한 애플리케이선 빈
// Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + "object = " + bean);
}
}
}
}
2.2. 빈 조회 - 기본
- ac.getBean(빈이름, 타입) - 반환타입, 구체타입
- 스프링 빈은 반환타입의 구체타입을 보고 bean이 존재하는지 확인하기 때문에 구체타입도 가능하다.
- ac.getBean(타입)
- 동일 타입의 또 다른 빈 존재 시 문제 발생
- 없는 빈을 조회 시
- NoSuchBeanDefinitionException: No bean named 'xxxxx' available.라는 error 문구 뜬다.
class ApplicationContextBasicFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("bean이름, bean타입 으로 조회")
void findBeanByName(){
MemberService memberService = ac.getBean("memberService", MemberService.class);
System.out.println("memberService = " + memberService);
System.out.println("memberService.getClass = " + memberService.getClass());
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("bean이름, bean구체타입 으로 조회")
void findBeanByName2(){
MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("bean 타입으로만 조회")
void findBeanByType(){
MemberService memberService = ac.getBean(MemberService.class);
System.out.println("memberService = " + memberService);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("존재하지 않는 bean 조회")
void findBeanByNameX(){
// ac.getBean("xxxx", MemberService.class);
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("xxxx", MemberService.class));
}
}
2.3. 빈 조회 - 동일 타입이 둘 이상
- 발생 예외 : NoUniqueBeanDefinitionException
- ac.getBean(빈이름, 타입) - 반환타입, 구체타입
- 스프링 빈은 반환타입의 구체타입을 보고 bean이 존재하는지 확인하기 때문에 구체타입도 가능 - 하지만 유연성이 떨어진다.
- ac.getBeanOfType(타입) - 해당 타입의 모든 빈 반환, @Autowired에서 해당 기능 사용
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class );
@Test
@DisplayName("type으로 조회시 같은 type이 둘이상 있으면, 중복오류가 발생")
void findBeanBYTypeDuplicateX(){
// MemberRepository bean = ac.getBean(MemberRepository.class); //NoUniqueBeanDefinitionException error 발생
// 예외가 터지는 것을 바라는 Assertions
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
}
// 해결책 1
@Test
@DisplayName("type으로 조회시 같은 타입 둘 이상 있으면, 빈이름을 지정하면 된다")
void findBeanByTypeDuplicateO(){
MemberRepository memberRepositoryO = ac.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepositoryO).isInstanceOf(MemberRepository.class);
}
// 해결책 2 (중요)
@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);
}
// 내부 class로 Config = 기획자 만듬
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() { return new MemoryMemberRepository(); }
@Bean
public MemberRepository memberRepository2() { return new MemoryMemberRepository(); }
}
}
2.4. 빈 조회 - 상속 관계
- getBean(classType)으로 조회할 때 이 타입의 자손도 모두 다 조회가 된다.
- 자동관계 주입 때 중요 원리로 적용
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class );
@Test
@DisplayName("부모 타입으로 조회시,자식이 둘 이상 있으며, 중복 오류가 발생")
void findBeanByParentTypeDuplicate(){
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈이름을 지정하면 된다.")
void findBeanByParentTypeBeanName(){
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(DiscountPolicy.class); // instanceof의 형변환을 생각하면 왜 가능한지 알 수 있다.
}
@Test
@DisplayName("특정 하위타입으로 조회") // 별로 추천하지 않는 방법
void findBeanBySubType(){
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType(){
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()){
System.out.println("key = " + key + ", values = " + beansOfType.get(key));
}
}
@Test
@DisplayName("특정 type 모두 조회 - Object")
void findAllBeanByType(){
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(Object.class);
for (String key: beansOfType.keySet()){
System.out.println("key = " + key + ", value" + beansOfType.get(key));
}
}
// 내부 class로 Config = 기획자 만듬
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() { return new MemoryMemberRepository(); }
@Bean
public MemberRepository memberRepository2() { return new MemoryMemberRepository(); }
}
}
3. 스프링 컨테이너 생성 방법 -XML
- 요즘은 AppConfig.class로 사용하지만 오래된 회사에서 xml로 사용하는 경우가 있어서 적당히 알아두면 좋다.
- 방식
- 자바코드로 된 설정 정보 : new AnnotationConfigApplicationContext("AppConfig.class")
- xml 설정 정보 : new GenericXmlApplicationContext("AppConfig.xml")
- AppConfig.xml
- 파일 생성 tip
<?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">
// id : bean 이름
// class : 해당 id의 실제 bean 객체
<constructor-arg name="memberRepository" ref="memberRepository11"/>
// constructor-arg : DI 해야 할 bean
// ref : DI할 bean
// name :생성자의 매개변수와 일치하는 이름
</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.FixDiscountPolicy"/>
</beans>
- test code
public class XmlAppContext {
@Test
void xmlAppContext(){
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
- 요즘 안 쓰니깐 필요하면 https://spring.io/projects/spring-framework 이 문서 찾아보고 사용
4. 스프링 빈 설정 메타 정보
- 이론적 개념 - 추상화를 통해서 역할과 구현이 잘 분리된 사례
- BeanDefinition이 이거구나 정도로만 알면된다. 실제로 사용할 일이 없다.
- 역할
- 스프링 컨테이너는 BeanDefinition의 정보를 통해서 빈을 등록할 뿐 BeanDefinition 실제 구현체가 뭔지 모른다.
- 구현
- 해당 스프링 컨테이너 구현체들은 각각의 BeanDefinitionReader를 가지고 있다.
- 해당 reader를 이용해서 다르게 구현된 설정파일들을 읽고 각각의 BeanDefinition 인스턴스 생성
- 정리
- 스프링 컨테이너에 빈 등록 시, 설정파일을 보고 바로 등록하는 것이 아니라
- BeanDefinitionReader를 통해서 Bean meta 정보를 생성하고 이 메타정보를 기반으로 beanDefinition 객체를 생성
- 스프링 컨테이너는 BeanDefinition interface를 통해서 빈을 등록한다.
4.1. 모식도 및 메타 정보
- 코드
package hello.core.beandefinition;
public class BeanDefinitionTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
GenericXmlApplicationContext gc = new GenericXmlApplicationContext("AppConfig.xml");
@Test
@DisplayName("빈 설정 메타정보 확인")
void findApplication(){
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 정보 : 이걸 기반으로 실제 인스턴스 생성
- BeanClassName: 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음 - AppConfig 같은 것)
- factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
- factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
- Scope: 싱글톤(기본값)
- lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리 하는지 여부
- InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
- DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
- Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
4.2. Bean 등록 방식
public class BeanDefinitionTest {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(AppConfig.class); // 우회등록 방식
GenericXmlApplicationContext gc
= new GenericXmlApplicationContext("AppConfig.xml"); // 직접등록 방식
}
- 여러 등록 방식이 존재하지만 대표적으로 2가지 존재
- 우회 등록 방식 - Factory method 이용
- java 방식 : @Configuration, 생성자 주입
- annotation 방식 : @Component @AutoWired 방식
- 직접 등록 방식
- xml 방식
- 우회 등록 방식 - Factory method 이용
- 우회 등록 방식 meta 정보
- class 정보 자체를 등록하지 않고 어노테이션을 이용해서 등록하기 때문
# @Configuration @bean
# @Componet @bean
class=null, factoryBeanName=appConfig, factoryMethodName=memberService
- 직접 등록 방식 meta 정보
- xml에 직접 class 타입을 넣어서 bean 등록
- factorymethod 사용 필요가 없다.
# <bean id="memberRepository11" class="hello.core.member.MemoryMemberRepository"/> - class를 직접 등록
meta 정보에 class="hello.core.member.MemoryMemberRepository" factoryBeanName=[], factoryMethodName=[]
이전 발행글 : 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용
다음 발행글 : 스프링 핵심 원리 이해 4 - 싱글톤 컨테이너
출처: 인프런 스프링 핵심 원리 - 기본 편