스프링 컨테이너와 스프링 빈

스프링 컨테이너 생성

저번 과정에서 MemberApp에 적용한 코드를 보겠습니다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext 에 대해서 먼저 알아보겠습니다.

ApplicationContext 는 스프링 컨테이너고 인터페이스입니다. 즉, 다형성이 적용되어 있습니다.

스프링 컨테이너는 XML을 기반으로 만들 수 있고, 어노테이션 기반의 자바 설정 클래스로 만들 수 있습니다.

전에 사용했던 AppConfig를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것 입니다.

즉, AnnotationConfigApplicationContextApplicationContext 인터페이스의 구현체인 것 입니다.

스프링 컨테이너의 생성 과정

  1. 스프링 컨테이너 생성

스크린샷 2023-09-06 오후 5 30 58

먼저 new* AnnotationConfigApplicationContext(AppConfig.*class*); 에 정보를 줍니다.

그러면 스프링 컨테이너가 만들어지는데 스프링 컨태이너에는 스프링 빈 저장소가 있습니다.

해당 저장소 안에는 빈 이름(Key) 빈 객체(값)으로 저장됩니다.

스프링 컨테이너를 등록할 때구성정보(AppConfig)를 지정해줘야 합니다.

  1. 스프링 빈 등록

스크린샷 2023-09-06 오후 5 31 49

추가로 빈 이름 같은 경우애는 보통은 매서드 이름을 사용하지만. @Bean(name=”이름”)을 사용하여 직접 정해줄 수 있습니다. 빈 이름은 그리고 항상 다른 이름을 부여해야 합니다.

스프링 컨테이너가 스프링 빈 저장소에 스프링 빈을 등록하는데, 구성 정보를 보고 @Bean이 붙은 메서드를 보고 메서드 이름빈 이름 (Key)로 가지고 해당 메서드에서 생성한 객체빈 객체로 담습니다.

  1. 스프링 빈 의존관계 설정 - 준비

스크린샷 2023-09-06 오후 5 44 09

그다음으로는 의존관계를 넣어줍니다.

  1. 스프링 빈 의존관계 설정 - 완료

스크린샷 2023-09-06 오후 5 44 40

스프링 컨테이너는 설정 정보를 참고해서 의존관계(DI)를 주입합니다.

참고로 스프링은 빈을 생성하고 의존관계를 주입하는 단계가 나누어져 있습니다.

하지만 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리됩니다. 즉, 관계가 복잡해집니다.

컨테이너에 등록된 모든 빈 조회

Test 클래스에서 한번 조회해보겠습니다.

public 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("애플리케이션 빈 출력")
    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);
            }
        }
    }
}

먼저 컨테이너 등록을 위해 구성정보를 알려주고 ac에게 할당해 줍니다.

  • 모든 빈 출력하기

이 코드는 실행하면 스프링에 등록된 모든 빈 정보를 출력합니다.

ac.getBeanDefinitionNames()는 스프링에 등록된 모든 빈 이름을 조회합니다.

ac.getBean()는 빈 이름으로 빈 객체(인스턴스)를 조회합니다.

출력하면 아래 그림과 같이 Spring에서 자동 등록된 빈들과 우리가 설정 정보를 적용해서 등록한 빈들이 보입니다.

스크린샷 2023-09-06 오후 5 58 33
  • 애플리케이션 출력하기

만약 스프링에서 자동 등록된 빈을 제외한 내가 등록한 빈을 보고싶으면, getRole()을 사용하면 됩니다.

getRole()에서 사용하는 ROLE_APPLICATION은 일반적으로 사용자가 정의한 빈만 나타내고, ROLE_INFRASTRUCTURE스프링 내부에서 사용하는 빈만 나타냅니다.

ROLE_APPLICATION 적용해서 테스트 해보면 아래와 같이 AppConfig를 이용해 등록한 빈들만 보입니다.

스크린샷 2023-09-06 오후 6 02 53

스프링 빈 조회 -기본

  • 빈 이름으로 조회

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

스프링 컨테이너에서 빈을 이름으로 조회할 때는 ac.getBean(빈이름, 타입)을 이용해 조회할 수 있습니다.

  • 빈 이름 없이 타입으로만 조회

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

타입으로만 조회할 때는 그냥 ac.Bean(타입) 즉 name을 빼면 됩니다.

  • 객체 타입으로 조회

    @Test
    @DisplayName("객체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

그냥 인터페이스말고 바로 ac.getBean("memberService", **MemberServiceImpl.class**); 구현체를 넣어서 조회하면 됩니다.

하지만 이 방법은 구체 타입으로 조회하기 때문에 변경시 유연성이 떨어집니다. 즉 구현체에 의존하고 있어 좋지 않습니다.

  • 조회 대상 스프링 빈이 없으면 예외 발생

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
//        MemberService xxxxx = ac.getBean("xxxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx", MemberService.class));
    }

MemberService xxxxx = ac.getBean("xxxxx", MemberService.class); 코드로만 실행시 테스트에 실패합니다.

스크린샷 2023-09-06 오후 6 26 59

그림의 내용을 보면 xxxxx라는 이름의 빈이 없다고합니다. 즉, 우리는 AppCoinfiig에서 xxxxx라는 메서드를 만들지 않았습니다. 이름도 같아야 하는 것 입니다.

그럼 해당 코드를 주석하고 예외가 던져지는지 확인하는 코드를 실행시키면 태스트에 성공하는 것을 볼 수 있습니다.

해당 코드에서 사용한 NoSuchBeanDefinitionException는 Spring이 요청된 빈 이름에 대한 빈 정의를 찾을 수 없거나 동일한 이름을 가진 여러 빈 정의가 있을 때 발생하는 예외입니다.

스프링 빈 조회 - 동일한 타입이 둘 이상

타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생하는데, 이때는 빈 이름을 지정하면 됩니다.

예를 들어서 임의로 테스트용 Config를 만들어서 테스트해보겠습니다.

  • 테스트용 Config

    @Configuration
    static class SameBeanConfig {

        @Bean
        public MemberReposiotry memberRepository1() {
            return new MemoryMemberRepository();
        }
        @Bean
        public MemberReposiotry memberRepository2() {
            return new MemoryMemberRepository();
        }

    }
  • 타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류 발생

 @Test                                                                 
 @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생")                   
 void findBeanByTypeDuplicate() {                                      
       MemberReposiotry bean = ac.getBean(MemberReposiotry.class);     
     assertThrows(NoUniqueBeanDefinitionException.class,               
             () -> ac.getBean(MemberReposiotry.class));                
 }
  • 타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름 지정

@Test                                                                                                   
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")                                                
void findBeanNyName() {                                                                                 
    MemberReposiotry memberReposiotry = ac.getBean("memberRepository1", MemberReposiotry.class);        
    assertThat(memberReposiotry).isInstanceOf(MemberReposiotry.class);                                  
}

빈 이름을 직접 지정해서 조회하면 됩니다.

  • 특정 타입을 모두 조회

@Test                                                                                               
@DisplayName("특정 타입을 모두 조회")                                                                        
void findAllBeanByType() {                                                                          
    Map<String, MemberReposiotry> beansOfType = ac.getBeansOfType(MemberReposiotry.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);                                                    
}

ac.getBeanOfType()을 사용하면 해당 타입의 빈을 모두 조회할 수 있습니다.

스프링 빈 조회 - 상속 관계

부모타입으로 조회시 자식 타입도 함께 조회됩니다.

그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회합니다.

스크린샷 2023-09-07 오후 12 08 17

마찬가지로 테스트용 Config를 만들어 놓고 테스트해보겠습니다.

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
  • 부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류 발생

    @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);
    }
  • 특정 하위 타입으로 조회

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }
  • 부모 타입으로 모두 조회

    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
        }
    }

이 부분에서 새로운 코드가 보이는데, 부모 타입으로 모두 조회 시에는 부모 타입 중 가장 최상위 부모 타입인 Object를 이용하면 됩니다.

BeanFactory와 ApplicationContext

스크린샷 2023-09-07 오후 12 15 51

BeanFactory

스프링 컨테이너의 최상위 인터페이스입니다.

스프링 빈을 관리하고 조회하는 역할을 담당합니다.

getBean()을 제공합니다.

앞에서 사용했던 기능둘운 대부분 BeanFactory가 제공한 기능입니다.

ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공합니다.

빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 차이점은 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능들이 필요한데, 그 부가기능들을 제공 해줍니다.

스크린샷 2023-09-07 오후 12 16 06
  • 메시지소스를 활용한 국제화 기능

    • ex) 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력

  • 환경변수

    • ex) 로컬, 개발, 운영등을 구분해서 처리

  • 애플리케이션 이벤트

    • ex) 이벤트를 발행하고 구독하는 모델을 편리하게 지원

  • 편리한 리소스 조회

    • ex) 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

다양한 설정 형식 지원 - 자바 코드, XML

스프링 컨테이너다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있습니다. 즉 자바 코드, XMl, Groovy 등 다양합니다.

스크린샷 2023-09-07 오후 12 33 37

즉, 그림처럼 ApplicationContext를 통해 다양한 설정 정보들을 사용할 수 있습니다.

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);
    }
}
  • appConfig.xml

<?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>

해당 appConfig.xml 코드와 앞에서 자바로 만든 AppConfig.java 설정 정보를 비교해보면 거의 비슷하다는 것을 알 수 있습니다.

스프링 빈 설정 메타 정보 - BeanDefinition

그러면 스프링은 어떻게 이렇게 다양한 설정 형식을 지원하는지 궁금증이 생기는데, 이유는 BeanDefinition이라는 추상화에 있습니다.

즉, 역할과 구현을 개념적으로 나눈 것 입니다.

XML을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만들면 된다.

스프링 컨테이너는 자바 코드인지 XML인지 몰라도 됩니다. 오직 BeanDifinition만 알면 됩니다.

BeanDefinition빈 설정 메타정보라 하는데, @Bean, <bean> 당 각각 하나씩 메타 정보가 생성됩니다.

스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성하는 것 입니다.

스크린샷 2023-09-07 오후 1 00 57

좀더 깊이 있게 보면,

스크린샷 2023-09-07 오후 1 01 55

AnnotationConfigApplicationContextAnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성합니다.

GenericXmlApplicationContextXmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성합니다.

그리고 새로운 설정 정보가 추가되면 다른 BeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 됩니다.

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우

    • ex) appConfig

  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정

    • ex) memberService

  • Scope: 싱글톤(기본값)

  • lazylnit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연 처리 하는지 여부

  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명

  • DestoryMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명

  • Constructor arguments, Properties: 의존관계 주입에서 사용한다.(자바 설정 처럼 팩토리 역할의 빈을 사용)

    @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("beanDefinition = " + beanDefinition + " beanDefinition = " + beanDefinition);
            }
        }
    }

BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 있습니다.

참고 자료

Last updated