[Spring] 스프링 컨테이너
김영한님의 스프링 핵심 원리 - 기본편을 바탕으로 작성하였습니다.
[Spring] DI와 IoC 그리고 컨테이너와 이어지는 포스팅입니다.
Spring 컨테이너로 변경
이전 포스팅에서 순수 Java로 DI 컨테이너를
Spring 컨테이너로 변경하는 작업을 해볼거다.
그리고 스프링컨테이너에 등록된 객체들을 조회하는 방법도 알아보려 한다.
전체코드는 깃헙에 올라가있음
AppConfig
우선 AppConfig
를 수정해보자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public static DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
그닥 바뀐게 없다.
AppConfig
클래스위에 Configuration 어노테이션이 붙었고,
모든 메서드에는 Bean 어노테이션이 붙었다.
스프링 컨테이너는 Configuration 어노테이션이 붙은 AppConfig
를 스프링의 구성(설정)정보로 사용한다.
스프링은 구성정보에 Bean 어노테이션이 붙은 메서드를 모두 호출 하여,
반환된 객체를 스프링 컨테이너에 등록한다.
이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈 이라고 한다
OrderApp
이전의 코드는 AppConfig
를 통해 필요한 객체를 조회 했지만,
스프링에서는 스프링 컨테이너를 통해 필요한 객체(스프링 빈)을 조회해야 한다.
코드는 아래와 같다.
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
// 아래와 같이 변경
// 스프링 컨테이너 생성
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 컨테이너에서 스프링빈 조회
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId,"itemA", 20000);
System.out.println(order);
System.out.println(order.calculatePrice());
}
}
여기서 ApplicationContext가 스프링 컨테이너 이다.
AnnotationConfigApplicationContext
의 생성자를 호출하여 스프링 컨테이너를 생성하면,
스프링 컨테이너 안에는 스프링 빈 저장소가 생기는데,
스프링 빈 저장소에는
파라미터로 넣어준 AppConfig.class
의 모든 메서드를 호출하여 반환된 객체(스프링 빈)를
스프링 빈 저장소에 저장한다.
기존에는 AppConfig에서 직접 필요한 객체를 찾아왔지만,
이제는 스프링 컨테이너에서 스프링 빈을 찾아 와야 한다.
ac.getBean()
메서드를 통해서 스프링 빈을 가져올 수 있는데,
두개의 파라미터를 필요로 한다.
첫번째는 Bean의 이름이다.
디폴트로 AppConfig
에 작성한 메서드의 이름이다.
두번째는 Bean의 타입이다.
반환되는 스프링빈의 타입을 입력해주면 된다.
그리고 돌려보면 이전과 같이 동작하는 것을 확인 할 수 있다.
스프링 빈 조회
스프링 컨테이너에서 스프링빈을 조회하는 방법에 대해서 좀 더 자세히 알아보자.
모든 빈 조회
테스트 코드를 통해 스프링 컨테이너에 등록된 모든 스프링 빈을 조회해보려 한다.
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
getBeanDefinitionNames
메서드를 통해 스프링 컨테이너에 등록된 모든 스프링빈의 이름을 배열 형식으로 가져올 수 있다.getBean
method에 인자값으로 스프링빈의 이름을 넣어서 호출하면,
해당 스프링가져온다.
내용을 출력해보면, 내가 직접 스프링 컨테이너에 등록한 빈 이외에도 여러 빈들이 출력되는 걸 확인할 수 있다.
스프링 내부에서 사용하도록 자체적으로 스프링 컨테이너에 들어있는 빈들인데,
이러한 빈들을 InfarStructure Bean 이라한다. 반면 내가 직접 등록한 빈을 애플리케이션 빈 이라 한다.
애플리케이션 빈만 조회
애플리케이션 빈만 조회 하기위해서는 해당 빈의 역할을 확인하면 된다.
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
}
아까의 코드와 별반 차이없다.
천천히 읽어보면 각각의 메서드가 무슨역할을 하는지 충분히 알 수 있으니 설명은 생략함
특정 빈만 조회
특정 빈을 조회할 때는 getBean
method를 사용하면 된다.
getBean은 오버로딩 되어 있어서 인자값을 1~2개 받을 수 있다.
getBean(빈이름, 타입)
과 getBean(타입)
이다.
테스트 코드를 통해 확인해보자.
@Test
@DisplayName("이름과 타입으로 조회")
void findBeanByNameAndType() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
@Test
@DisplayName("타입만으로 조회")
void findBeanByType() {
MemberService bean = ac.getBean(MemberService.class);
assertThat(bean).isInstanceOf(MemberService.class);
}
@Test
@DisplayName("구체 클래스로 조회")
void findBeanByType2() {
MemberService bean = ac.getBean(MemberServiceImpl.class);
assertThat(bean).isInstanceOf(MemberService.class);
}
이름과 타입, 그리고 타입, 마지막으로 구체클래스로 조회가 되는것을 모두 확인 할 수 있다.
만약 없는 빈이름이거나, 빈이름과 타입이 일치하는 경우는 어떠한지 확인해보자.
@Test
@DisplayName("없는 이름으로 조회")
void findBeanByWrongName() {
MemberService bean = ac.getBean("wrongName",MemberService.class);
// NoSuchBeanDefinitionException 예외 발생
}
@Test
@DisplayName("이름과 타입이 일치하지 않는 조회")
void findBeanByUnmatchedNameAndType() {
ac.getBean("memberService", OrderService.class);
// BeanNotOfRequiredTypeException 예외 발생
}
둘 다 각각 예외가 발생한다.
예외가 발생하는걸 테스트 코드로 작성하면 아래와 같이하면 된다.
@Test
@DisplayName("없는 이름으로 조회")
void findBeanByWrongName() {
assertThatThrownBy(() -> ac.getBean("wrongName", MemberService.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Test
@DisplayName("이름과 타입이 일치하지 않는 조회")
void findBeanByUnmatchedNameAndType() {
assertThatThrownBy(() -> ac.getBean("memberService", OrderService.class))
.isInstanceOf(BeanNotOfRequiredTypeException.class);
}
중복되는 타입의 빈 조회
getBean
메소드에 인자값으로 타입만 넘겨줄 때,
만약 스프링 컨테이너에 중복되는 타입이 있다면 어떻게 될까?
public class DuplicatedTypeBeanFindTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 중복되는 타입이 있으면 예외발생")
void findBeanByDuplicatedType() {
assertThatThrownBy(() -> ac.getBean(DiscountPolicy.class))
.isInstanceOf(NoUniqueBeanDefinitionException.class);
}
@Test
@DisplayName("중복되는 타입이 있으면 빈이름을 넣어주면 됨")
void findBeanByDuplicatedTypeWithName() {
DiscountPolicy fixDiscountPolicy = ac.getBean("FixDiscountPolicy", DiscountPolicy.class);
assertThat(fixDiscountPolicy).isInstanceOf(DiscountPolicy.class);
}
@Test
@DisplayName("중복되는 타입의 빈 모두 조회")
void findAllBeansDuplicatedType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key);
}
}
@Configuration
static class SameBeanConfig {
@Bean
public DiscountPolicy FixDiscountPolicy() {
return new FixDiscountPolicy();
}
@Bean
public DiscountPolicy RateDiscountPolicy() {
return new RateDiscountPolicy();
}
}
}
위의 테스트코드로 확인을 해볼 수가 있다.
우선 이전에 작성한 AppConfig
에는 중복되는 타입의 빈이 생성되지 않는다.
잘 만들어놓은거 굳이 수정하지말고, 테스트코드에 이너클래스로 새로 작성해주자.
첫번째 테스트를 보면 중복되는 타입이 있을 때, 타입만으로 빈을 조회하면NoUniqueBeanDefinitionException
예외가 발생한다는 것을 알 수 있다.
이렇게 중복되는 타입의 빈이 컨테이너에 등록되는 경우는
빈의 이름과 타입을 모두 인자로 넘겨 줘야 예외가 발생하지 않는다.
만약, 중복되는 타입의 모든 빈을 한번에 조회하고 싶다면,getBeans()
메소드를 사용하면 된다.
그러면 리턴값으로 Map<빈이름, 빈타입>이 반환 되는 것을 확인할 수 있다.
빈 조회 - 상속관계
첫번째 테스트코드에서 알 수 있는 사실이 있다.
바로 부모타입을 통해 조회하면, 자식타입의 빈이 모두 조회된다는 점이다.이러한 경우는 두번째 테스트코드에서 작성한 것 처럼
빈의 이름도 함께 사용하여 조회하면 된다.
BeanFactory
BeanFactory에 대해 알아보자.
개뜬금 없는거 같긴한데, 사실 뜬금없는 내용이 아니다.
BeanFactory
는 스프링컨테이너의 최상위 인터페이스이다.
컨테이너를 생성할때 호출했던 AnnotationApplicationContext
는ApplicationContext
interface를 상속받아서 구현한 구현클래스이다.
그리고 ApplicationContext
interface는 BeanFactory
interface를 상속받는다.
BeanFactory
는 스프링컨테이너는 스프링 빈을 관리하고 조회하는 역할을 한다.
위의 코드에서 사용했던 getBean
메서드는 BeanFactory가 제공하는 기능이다.
ApplicationContext
그럼 ApplicationContext
의 역할을 무엇일까
ApplicationContext는 BeanFactory의 빈관리기능 이외에도
ISP원칙에 따라 세부적으로 나뉘어진 여러가지 인터페이스로부터 기능들을 상속받고 있다.
ApplicationContext가 상속 받은 기능들은 아래와 같다
MessageSource
메시지 소스를 활용한 국제화 기능EnvironmentCapable
환경변수에 따른 로컬,개발,운영 등을 구분해서 처리ApplicationEventPublisher
애플리케이션 이벤트 : 이벤트를 발행하고 구독하는 모델을 편리하게 지원ResourceLoader
편리한 리소스 조회: 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
BeanDefinition
위의 코드에서 스프링 컨테이너에 구성정보를 입력해준 방식은
어노테이션기반의 자바코드로 작성한 AppConfig.class 를 전달해주는 방식이었다.
스프링의 놀라운 점은 구성정보를 java가 아닌 xml을 이용하여 작성해도 동일하게 동작한다는 점이다.
물론 xml작성할건 아니다. 그냥 된다고.
스프링은 어떻게 이런 다양한 설정 형식을 지원할 수 있는 걸까
그 중심에는 BeanDefinition
이라는 추상화가 있다.
스프링 컨테이너는 설정 정보를 바로 읽는 것이 아니라,
설정 정보를 바탕으로 생성된 BeanDefinition
의 메타정보를 읽고 스프링 빈을 생성한다.
직접 메타정보를 작성해서 직접 빈을 등록할 수도 있다고 하는데..
실제로 할일은 없을거 같으니 그냥 그렇게 할 수도 있구나 하고 넘어가면 될듯하다.