스프링 컨테이너를 생성할 때에는 구성 정보를 지정해야 한다. = 여기에서는 "AppConfig class를 사용할 것이다."
우리가 앞선 게시글에서 봤듯, 스프링 컨테이너의 생성 코드는 다음과 같다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext : '스프링 컨테이너' 이면서 '인터페이스' 이다. 따라서 다형성을 적용할 수 있겠다.
따라서 이를 구현한 것 중 하나가 AnnotationConfigApplicationContext 인 것이다.
이렇게 되면, 빈 스프링컨테이너가 만들어진다.
스프링 빈 저장소 | |
빈 이름 | 빈 객체 |
- | - |
- | - |
- | - |
Bean 어노테이션이 붙은 친구들을 이제 스프링 빈 저장소에 올린다.
이 때, 빈 이름은 '메소드이름'으로, 객체는 각 메소드 별로 return 되는 객체들이다.
스프링 빈 저장소 | |
빈 이름 | 빈 객체 |
memberService | MemberServiceImpl |
orderService | OrderServiceImpl |
memberRepository | MemoryMemberRepository |
discountPolicy | RateDiscountPolicy |
이 때, 빈 이름은 항상 다른 이름으로 등록해야 함에 유의하자. (물론 메소드명 말고 다르게 Annotation을 이용하여 임의로 이름을 설정할 수 있다.)
등록된 Bean들끼리 의존되는 관계들을 설정한다.
1. memberService -> memberRepository 의존
2. orderService -> memberRepository, discountPolicy 의존
3. MemberRepository -> 의존관계 없음
4. DiscountPolicy -> 의존관계 없음
package Goat.core.beanFind;
import Goat.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac =new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
public void findAllBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
}
Tip (나머지는 단순한 메소드니까 다른 설명은 굳이 하지 않겠다)
'iter'을 치면 intelliJ에서 뜨는 추천 코드를 눌러서 beanDefinitionNames 객체를 iteration하는 코드를 자동으로 생성해준다.
결과 :
하지만 이 외에도 다른 Bean ( 스프링 내부에서 자동으로 등록한) 것도 같이보이게 된다.
이를 없애기 위해서,
@Test
@DisplayName("에플리케이션 빈 출력하기")
public void findApplicationBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//직접 내가 Bean으로 등록한 Bean들만 가져오기.
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
}
로 변경하면 되겠다.
Role ROLE_APPLICATION : 직접 등록한 어플리케이션 빈
Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
package Goat.core.beanFind;
import Goat.core.AppConfig;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;
public class ApplicationContextBasicFindTest {
AnnotationConfigApplicationContext ac =new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName(){
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
}
-> 타입으로 할 경우 중복 Bean으로 인한 문제가 생기기도 한다.
@Test
@DisplayName("빈 타입으로 조회")
void findBeanByType(){
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
-> 이전에는, 각 Bean의 반환타입인 인터페이스로 찾았다면, 메소드 내에 생성된 객체 형식으로 Bean을 조회할 수 있다. 하지만 추천하는 방법은 당연히 아니겠다.
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2(){
MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회X")
void findByNameX(){
Assertions.assertThrows(NoSuchBeanDefinitionException.class,()->ac.getBean("xxxxx",MemberService.class));
}
-> AppConfig에서는 같은 타입이 둘 이상인 것이 없으므로, 해당 TestCode에서 직접 Config를 정의하는 클래스를 생성하고 실행해보자.
클래스 내에 stataic 클래스를 선언하는 경우
해당 클래스 내에서만 static클래스를 사용하겠다는 뜻
package Goat.core.beanFind;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac =new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 중복 오류 발생")
void findBeanByTypeDuplicate(){
MemberRepository bean = ac.getBean(MemberRepository.class);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1(){
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2(){
return new MemoryMemberRepository();
}
}
}
해당 테스트 코드 실행시,
이런 오류가 뜬다.
이제 이를 막아보자.
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 중복 오류 발생")
void findBeanByTypeDuplicate(){
Assertions.assertThrows(NoUniqueBeanDefinitionException.class,()->ac.getBean(MemberRepository.class));
}
그렇다면, 타입말고도, 빈 이름으로도 지정해주면 된다.
getBean("빈 이름", "타입 형") 이기 때문에 인자를 2개를 주자.
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 빈 이름을 인자로 전달")
void findBeanByName(){
MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
org.assertj.core.api.Assertions.assertThat(memberRepository).isInstanceOf(MemoryMemberRepository.class);
}
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanByType(){
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = "+key + "value = " + beansOfType.get(key));
org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
}
}
package Goat.core.beanFind;
import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ApplicationContextExtendsFindClass {
AnnotationConfigApplicationContext ac =new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류 발생")
public void findBeanByParentTypeDuplicate(){
assertThrows(NoUniqueBeanDefinitionException.class,()->ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 ,빈 이름 지정")
public void findBeanByParentTypeBeanName(){
DiscountPolicy rateDiscountPolicy = ac.getBean("RateDiscountPolicy", DiscountPolicy.class);
Assertions.assertThat(rateDiscountPolicy).isInstanceOf(DiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanBySubType(){
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회")
void findAllBeanByParent(){
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
Assertions.assertThat(beansOfType.size()).isEqualTo(2);
for (String s : beansOfType.keySet()) {
System.out.println("key = " + s + " value = " + beansOfType.get(s));
}
}
@Test
@DisplayName("Object로 모두 조회")
void findAllBeanByObject(){
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String s : beansOfType.keySet()) {
System.out.println("key = " + s + " value = " + beansOfType.get(s));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy RateDiscountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy FixDiscountPolicy(){
return new FixDiscountPolicy();
}
}
}
다음은 BeanFactory와 ApplicationContext의 상속 관계를 나타낸 그림이다.
즉 BeanFactory(최상위)를 상속받은 ApplicationContext가 있다. 즉, ApplicationContext는 BeanFactory에서 부가기능을 더 첨가했다고 볼 수 있다.
다음은 ApplicationContext가 상속받은 부가 기능들을 나타낸 그림이다.
BeanFactory와 ApplicationContext 모두 스프링 컨테이너이다 !!
스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 유연하게 설계되었다.
ApplicationContext에는
AnnotationConfigApplicationContext에서 Appconfig.class 를 사용했는데, 이 때,
AnnotationConfig를 사용하는 ApplicationContext 말고, GenericXml을 사용하는GenericXmlApplicationContext에서 appConfig.xml를 사용하면 xml 문서를 설정정보로 사용할 수 있다.
물론, 우리가 임의의 파일(.xxx) 파일을 사용하는 ApplicationContext를 커스터마이징 할 수 있다.
test 에서 "xml" 패키지를 만들어 해당 패키지에 XmlAppContext 클래스를 만든다.
package Goat.core.xml;
import Goat.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
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);
}
}
<?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="Goat.core.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository"
class="hello.core.member.MemoryMemberRepository" />
<bean id="orderService" class="Goat.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
<constructor-arg name="discountPolicy" ref="discountPolicy" />
</bean>
<bean id="discountPolicy" class="Goat.core.discount.RateDiscountPolicy" />
</beans>
형식은 아주 비슷하다 !! AppConfig.class와 완전 비슷~
바로 "BeanDefinition"이라는 추상화가 있기 때문이다. 역할과 구현을 나누라는 김영한님의 말씀 그대로 BeanDefinition도 그러하다.
즉, XML을 읽어서 BeanDefinition을 만들거나 혹은, JAVA 코드를 읽어서 BeanDefinition을 만들거나 하는 것이다. 즉, 스프링 컨테이너 입장에서는 JAVA 인지 XML인지 모르고, 오로지 BeanDefinition만 알면 된다. (여태껏 배웠던 역할과 구현이 완벽히 나뉘어져 있는 예시이다)
여기서 "BeanDefinition"을 빈 설정 메타정보라고 한다. 그래서, 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다. = (스프링 컨테이너는 BeanDefinition<추상화>에만 의존하여 스프링 빈을 생성한다)
이렇게 ApplicationContext 안에 존재하는 AnnotationConfigApplicationContext 안에는 reader 와 scanner 가 존재하는데, 이 친구들이 설정정보 (AppConfig.class)를 읽고, BeanDefinition을 만들어낸다.
이런 식으로 XML도, 다른 파일도 설정정보를 읽어서 BeanDefinition을 만들어 낸다.
package Goat.core.beandefinition;
import Goat.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class BeanDefinitionTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
@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);
}
}
}
}