스프링 컨테이너 관련 정리

     

    스프링 컨테이너의 생성

    설정 정보를 저장한 AppConfig Class가 있다고 가정했을 때, 이 클래스로 스프링 컨테이너를 생성하는 명령어는 아래 코드다. ApplicationContext를 스프링 컨테이너라하고, ApplcationContext는 Interface다. ApplicationContext 인터페이스에 자바 형식으로 구현한 구현체가 AnnotationConfigApplicationContext다.

    그렇다면 아래 명령어를 치게 되었을 때, 스프링 컨테이너는 어떤 순서로 만들어지게 될까?

    ApplicationContext ac = new AnnotaionConfigApplicationContext(AppConfig.class);

     

    1. 먼저 위 그림처럼 스프링 컨테이너와 스프링 빈 저장소가 하나 만들어진다. 아직까지 빈 저장소에는 어떤 값도 들어있지 않은 상황이고, 기준 정보로 AppConfig.Class만 넘어간 상태다. 

    2. AppConfig에 있는 @Bean 어노테이션을 다 읽은 후, 객체를 생성해서 빈 이름과 Key, Value 형식으로 스프링 빈 저장소에 저장된다. 이 때, 빈 이름은 @Bean 어노테이션이 달린 메서드의 이름이 된다. 이 때, 빈 이름을 직접 부여할 수도 있는데 아래처럼 구성할 수 있다.

    @Bean(name = "abc")
        public MemberService memberService(){
            return new MemberServiceImpl(memberRepository());
        }

    3. 빈 저장소에 설정 정보 클래스에 있는 @Bean 메서드들이 다 읽어져 왔다면, 이 값들은 이제 한번에 의존관계 설정이 완료된다. 

     

     

    스프링에 등록된 빈 출력해보기. 

    1. 스프링 컨테이너에 등록된 모든 빈 출력해보기

        @Test
        @DisplayName("스프링에 등록된 빈 모두 출력해보기")
        void findAllBean(){
            String[] beanDefinitionNames = ac.getBeanDefinitionNames();
            for (String beanDefinitionName : beanDefinitionNames) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName + " Bean = " + Bean);
            }
        }
    • 스프링 컨테이너에 등록된 모든 빈 이름을 가져오는 메서드로 빈 이름을 모두 가져온다.
    • 반복문을 돌리면서, 빈 이름으로 빈을 가져온다
    • 빈을 출력한다

     

    2. 스프링 컨테이너의 어플리케이션 빈만 출력하기.

        @Test
        @DisplayName("스프링에 등록된 어플리케이션 빈 출력해보기")
        void findApplicationBean(){
            String[] beanDefinitionNames = ac.getBeanDefinitionNames();
            for (String beanDefinitionName : beanDefinitionNames) {
                
                // 아래 메서드는 AnnotaionContextApplication 구체의 지역 메서드임.
                BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
                
                // ROLE_APPLICATION : 직접 등록한 어플리케이션
                // ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
                if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                    Object bean = ac.getBean(beanDefinitionName);
                    System.out.println("beanDefinitionName = " + beanDefinitionName + " Bean = " + bean) ;
                }
            }
        }

    getBeanDefinitionNames 메서드로 스프링 컨테이너에 등록된 모든 빈의 이름을 가져온다.

    반복문을 돌리면서, 빈 이름으로 현재 빈 타입이 어떻게 정의되어져있는지를 가져온다.

    해당 빈 타입이 ROLE_Application일 경우, 사용자가 등록한 Application이므로 출력한다.

    3. 스프링 빈 이름 + 타입으로 조회하기

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

     

    4. 스프링 빈 타입으로 조회하기

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

     

     

    5. 스프링 빈 구현체 타입으로 조회하기.

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

     

     

    6. 스프링 빈 이름 검색 안될 때

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

     

    동일 타입의 빈이 여러 개가 있을 때? 

    동일 타입의 빈이 여러 개가 있다면, 빈 타입으로만 getBean을 하게 될 경우 에러가 발생한다. 이 경우에는 빈 타입과 이름으로 검색을 해서 출력을 해줄 수 있다. 이에 대한 테스트 코드는 아래를 참고 할 수 있다.

        @Test
        @DisplayName("같은 거 출력해서 안되기")
        void test1(){
            Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
                    () -> ac.getBean(MemberRepository.class)
            );
        }

     

    특정 타입을 모두 조회하고 싶을 수 있다. 이 때는 getBeansOfType 메서드를 이용해 내가 원하는 타입의 메서드를 전부 불러온다. 그리고 이 값을 하나씩 출력해보는 것도 할 수 있다.

    @Test
        @DisplayName("특정 타입 모두 조회하기")
        void test3(){
            Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
            System.out.println("beansOfType = " + beansOfType);
            for (String key : beansOfType.keySet()) {
                System.out.println("key = " + key + " value = " + beansOfType.get(key));
            }
            assertThat(beansOfType.size()).isEqualTo(2);
        }

     

    위의 테스트 코드 검증을 위해 아래 Static Config 클래스를 하나 만들었다.

       @Configuration
        static class TempConfig{
    
            @Bean
            MemberRepository memberRepository1(){
                return new MemoryMemberRepository();
            }
    
            @Bean
            MemberRepository memberRepository2(){
                return new MemoryMemberRepository();
            }
        }

     

    스프링 빈 조회 시, 상속관계라면?

    스프링 빈을 조회할 때, 타입으로만 조회할 때 고려해야하는 상황이다. 예를 들어 아래 같은 상속관계가 있다고 가정해보자. 결론부터 말하면 부모 타입으로 조회했을 경우, 부모 타입부터 모든 자식 클래스들이 한꺼번에 조회가 된다.

    예를 들어 '타입 1'로 조회했을 경우, 모든 타입의 빈이 나오게 된다. 또, '타입 4'로 조회했을 경우, 타입 4만 나오게 된다. 상속관계에서는 너무 복잡하게 생각하지 말고, 그 타입 밑으로는 다 나온다고 생각하자. 이에 대한 테스트 코드를 작성해본다.

    상속관계 빈 조회 테스트 코드

    public class ApplicationContextSameBeanTest {
    
        ApplicationContext ac = new AnnotationConfigApplicationContext(TempConfig.class);
    
        @Test
        @DisplayName("같은 거 출력해서 안되기")
        void test1(){
            Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
                    () -> ac.getBean(MemberRepository.class)
            );
        }
    
        @Test
        @DisplayName("타입 여러 개 중에 이름으로 설정해서 출력하기.")
        void test2(){
            MemberRepository memberRepository1 = ac.getBean("memberRepository1", MemberRepository.class);
            assertThat(memberRepository1).isInstanceOf(MemberRepository.class);
        }
    
        @Test
        @DisplayName("특정 타입 모두 조회하기")
        void test3(){
            Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
            System.out.println("beansOfType = " + beansOfType);
            for (String key : beansOfType.keySet()) {
                System.out.println("key = " + key + " value = " + beansOfType.get(key));
            }
            assertThat(beansOfType.size()).isEqualTo(2);
        }
    
        @Configuration
        static class TempConfig{
    
    		@Bean
            MemberRepository memberRepository1(){
                return new MemoryMemberRepository();
            }
    
            @Bean
            MemberRepository memberRepository2(){
                return new MemoryMemberRepository();
            }
        }
    
    
    }

    1. 부모 타입으로 조회할 경우, 부모 타입부터 밑에 있는 모든 타입의 빈이 조회된다. 따라서, getBean으로 할 경우 하나 이상의 빈이 검색되기 때문에 에러가 발생한다.

    2. 부모 타입으로 조회할 경우, 여러 개의 Bean이 조회된다. 이 때, getBean에 이름을 지정해줘서 필요한 메서드만 불러온다.

    3.부모 타입으로 조회하면, 여러 개의 Bean이 조회된다. 이것을 BeansofType으로 받아서 하나씩 모두 출력해본다.

     

     

    Bean Factory와 ApplicationContext

     

    Bean Factory(인터페이스)

    Bean Factory는 스프링 컨테이너의 최상위 인터페이스다.

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

    ApplicationContext(인터페이스, 스프링 컨테이너)

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

    어플리케이션 개발에는 빈 관리, 조회 뿐만 아니라 여러 부가 기능이 필요하다.

    메세지 소스를 활용한 국제화 기능 / 환경변수 / 애플리케이션 이벤트 / 편리한 리소스 조회

    빈 관리기능 + 편리한 부가 기능으로 정리할 수 있음.

     

    Bean 메타정보

    스프링은 어떻게 자바, XML 같은 다양한 설정 방식을 지원할 수 있는 것일까? 그 이유는 빈 메타정보에 있다. 스프링 컨테이너는 BeanDefinition 추상화 인터페이스에만 의존하기 때문이다. 예를 들어 어떤 정보를 BeanDefinition(대본)에 넘긴다면, 각 배우들은 그것을 구체화해준다. 이런 이유 때문에 스프링에서 여러 설정을 사용할 때, 크게 코드의 변화가 없다.

    스프링 컨테이너는 BeanDefinition이라는 추상화 인터페이스에 의존해서 빈을 생선한다. 그리고 BeanDefinition 추상화 인터페이스는 여러 구체 클래스에 의해 구체화 된다. 예를 들어 자바 Annotation으로 들어가는 것이라면 주로 AppConfig.Class 구체화 클래스로 구체화 된다. 이 때, 메타정보는 @Bean이 하나 있을 때 마다 만들어진다.

    예를 들어 스프링 컨테이너(Application Context)는 AnnotationConfigApplicationContext로 구체화 된다. AnnotationConfigApplicationContext를 열어보면 Annotated BeanDefinitionReader라는 값을 통해 AppConfig.Class에 대한 설정정보를 읽는다. 이 설정정보를 바탕으로 BeanDefinition(빈 메타 정보)를 생성한다. 

    빈 메타정보 확인해보기

    public class ApplicationContextBeanDefinitionReadTEst {
    
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
        @Test
        @DisplayName("모든 메타 정보 읽어오기")
        void test1(){
    
            String[] beanDefinitionNames = ac.getBeanDefinitionNames();
            for (String beanDefinitionName : beanDefinitionNames) {
                System.out.println("ac.getBeanDefinition(beanDefinitionName) = " + ac.getBeanDefinition(beanDefinitionName));
            }
        }
    }

    위는 빈 메타 정보를 모두 불러오는 테스트 코드이고, 테스트 결과는 아래 이미지에서 확인할 수 있다. 메타정보에는 아래와 같이 여러 형태의 정보를 가지고 있는 정보이며, 이 설정정보를 바탕으로 빈을 생성한다.

     

    빈 메타 정보를 등록하는 방법

    1. 직접 빈 메타정보 등록

    2. 팩토리 메서드를 이용한 등록

    직법 빈 메타정보를 등록하는 경우도 있긴 하지만 많지 않다고 한다. 일반적으로 @Configuration + @Bean을 통해서 사람들이 메타정보를 등록한다. 이런 것들은 팩토리 메서드를 이용한 등록과 유사하다고 한다. 아래 메타정보를 읽어보면 확인할 수 있다.

    Root Bean의 Class에는 null등록이 되어있다. 이 null이 등록되어있는데, 뒷쪽을 보면 factoryBeanName이나 factoryMethodName에는 특정 값이 들어있는 것을 볼 수 있다. 처음에 AppConfig를 통해 등록하고, 그 AppConfig에서 @Bean 정보를 읽어와 메타정보를 추상화 시킨 것을 알 수 있다. 

     

    댓글

    Designed by JB FACTORY