Spring/SpringCore - basic

스프링 핵심 원리 - 스프링 컨테이너와 스프링 빈(컨테이너 생성 원리 및 다양한 Bean 조회 방법들)

_Jin_ 2024. 12. 27.
Repeat is the best medicine for memory

 

 

지금까지 다뤘던 내용들은 객체 지향적인 관점을 기준으로 스프링이 하는 역할과 핵심 원리을 이해하기 위한 과정이었다.

이제, 스프링 자체에 대한 이해와 학습을 위한 내용들을 정리해보겠다.

스프링 컨테이너 생성 방법과 작동 원리 

먼저, 스프링 컨테이너가 생성되는 과정에 대해서 알아보자.

 

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Appconfig.class);

 

위의 코드 부분이 이전 포스팅에서 스프링 컨테이너를 만드는 로직에 해당한다.

코드의 내용은 ApplicationContext 타입의 변수에 사용할 구현체로 어노테이션 기반의 자바 설정을 클래스를 만드는 것이다.

( ApplicationContext는 인터페이스이며, 다형성이 적용되었기에 구현체를 골라서 만들 수 있다. )

 

스프링 컨테이너는 XML또는 어노테이션 기반의 구현체를 사용할 수 있는데, 앞서 AppConfig에 사용했던 방식이 어노테이션 기반의 구현체를 사용하여 스프링 컨테이너를 만든 과정이었다.

 

이를 그림으로 나타내자면, 아래와 같은 과정으로 이해할 수 있다.

 

스프링 컨테이너 내부에는 스프링 빈 저장소가 존재한다.

 

그리고 스프링 컨테이너 생성에 구성 정보를 지정해주어야 이를 활용하는 데, 앞서 파라미터로 AppConfig.class를 넘겨준 것에 해당한다.

 

이후, 스프링 컨테이너는 해당 설정 클래스(AppConfig) 정보를 보고 컨테이너의 빈 저장소 인스턴스 객체들을 담는다.

( 여기서 Bean의 이름은 항상 다른 이름을 부여해야 한다. 만약 Bean의 이름을 별도로 지정하고 싶다면, @Bean(name="memberService2") 이런 방식으로도 지정 가능하다. )

 

 

이와 같이 스프링 컨테이너 내부의 빈 저장소에 필요한 인스턴스들을 담은 다음 이제 스프링은 각 인스턴스의 의존 관계를 설정해준다. 이는 아래의 그림과 같은 모습으로 보면 되겠다.

 

 

 

이와 같은 과정을 통해 스프링 컨테이너의 생성과 설정 정보를 참고하여 스프링 빈 등록 및 의존관계 설정 과정이 이뤄진다.

 

등록된 Bean을 조회하는 다양한 방법들

그런데, 정말 컨테이너 내부에 제대로 빈이 등록되었나? 확인해보자.

 

모든 Bean 조회

등록된 모든 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);
        }
    }
}

조회 결과, appconfig부터 discountpolicy까지 빈 등록이 되었음을 확인할 수 있다.

( 나머지 빨간 부분은 스프링 내부적 필요에 의해 등록된 빈 )

 

 

내가 등록한 Bean만 조회

만약, 내가 등록한 Bean만 보고 싶은 경우, 즉 스프링 내부적인 필요에 의한 빈이 아닌 개발의 과정에서 등록한 Bean들만 보고 싶다면 아래의 코드와 같이 확인할 수 있다.

 

@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {

		// 컨테이너에 등록된 빈 이름들 배열로 가져오기 
    String[] beanDefinitionNames = ac.getBeanDefinitionNames(); 
    for (String beanDefinitionName : beanDefinitionNames) {
		    // 빈에 대한 메타데이터정보를 가져오는 메소드
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

        // 개발 과정에서 등록한 빈들은 ROLE_APPLICATION이라는 role을 가진다.
        // 만약 스프링 내부에서 사용하는 Bean을 조회하려면 ROLE_INFRASTRUCTURE role을 가진다.
        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + "    Object : "+bean);
        }
    }
}

 

조회 결과, 코드를 통해 등록한 Bean들만 조회되는 모습을 볼 수 있었다.

 

다양한 Bean 조회 방법들

이제, Bean 이름이나 타입 등을 조회 기본적으로 Bean을 조회하는 방법에 대해서 알아보자.

Bean을 조회하는 기본적인 방법은 다음과 같다.

 

‘ (1) 컨테이너 변수.getBean(빈이름, 타입) 또는 (2) 컨테이너 변수.getBean(타입) ‘의 방법이 있다.

 

이를 실습하기 위한 테스트 코드에 해당한다.

class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService",
            MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("이름 없이 타입만으로 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberServiceImpl memberService = ac.getBean("memberService",
            MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        //ac.getBean("xxxxx", MemberService.class);
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () - >
            ac.getBean("xxxxx", MemberService.class));
    }
}

각 코드의 조회 파라미터에서 차이가 존재한다. 이름만 넣어주는 경우도 있고, 타입만 넣어주는 경우도 있으며, 구체 타입으로 조회하는 경우도 있다.

( 단, 구체 타입으로 조회하는 것은 일반적으로 좋지 않은 예제이다. 구현체에 의존하지 않는 것이 좋으니깐 )

 

그리고 마지막에는 예외가 발생하는 경우에 대한 테스트 코드로 assertThrows메서드를 사용하여 존재하지 않는 빈 이름으로 조회하는 경우 예외의 발생을 검증하고 있다.

 

타입으로 Bean 조회의 경우, 중복 발생 예외

그런데, 만약 타입으로만 조회하는 경우 같은 타입을 가진 Bean 들이 많다면 스프링은 어떻게 특정 Bean을 선택하고 이를 사용하도록 구현되었을까??

 

먼저, 결론적으로 타입으로 조회시 같은 타입의 스프링 Bean이 둘 이상이라면 에러가 발생한다.

이를 해결하기 위한 방법으로는 Bean의 이름으로 조회 또는 getBeansOfType() 메서드를 활용하여 해당 타입의 모든 Bean들을 조회 가능하다.

 

아래의 코드를 통해 더 자세하게 알아보자.

class ApplicationContextSameBeanFindTest {
    
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() {
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () - >
            ac.getBean(MemberRepository.class));
    }
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1",
            MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.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));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }
    
    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

위의 코드에서 SameBeanConfig 클래스는 Configuration으로 동일 타입의 빈을 2개 등록하는 클래스이다.

그리고 해당 클래스를 사용하여, 스프링 컨테이너 생성의 파라미터에 넣어주었다.

 

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demo.member.MemberRepository' available: expected single matching bean but found 2: memberRepository1,memberRepository2

이후 findBeanByTypeDuplicate을 통해 타입으로 Bean을 조회하면, NoUniqueBeanDefinitionException 에러가 발생함을 테스트 코드로 확인하였다.

 

해당 예외에 대하여 Bean 이름으로 조회 또는 getBeansOfType()을 활용하여 같은 타입을 지닌 모든 Bean들을 조회하고 Map으로 받아 확인하는 테스트 코드를 통해 예외가 아닌 정상적 실행을 확인할 수 있었다.

 

이처럼 타입으로 조회 시, 동일 타입의 등록된 Bean이 두 개 이상인 경우, 충돌이 발생함을 알아보았고 해결 방법까지 알아보았다.

 

상속 관계에서의 Bean 조회

그렇다면, 더 나아가 부모 타입을 조회하는 경우 상속 관계에 있는 자식 타입에게도 영향을 끼칠까?

스프링 빈 조회에 있어 상속 관계인 경우 조회 범위는 아래와 같다.

스프링에서 부모타입을 조회하면 자식타입들까지 전부 조회된다.

그리고 자바의 최상위 부모 타입은Object이기에, Object를 조회하면 모두 끌려나온다.

 

이와 관련한 테스트 코드는 아래와 같이 작성하여 확인할 수 있다.

public class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시 , 자식이 둘 이상이 있으면 중복오류가 발생한다.")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시 , 자식이 둘 이상이 있으면 빈 이름을 지정하면 된다.")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy discountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class); 
        assertThat(discountPolicy).isInstanceOf(DiscountPolicy.class); 
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회") // 구현체로 검색하는 것은 좋지않다.
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class); // 해당 타입이 하나인 경우 예외가 발생하지 않는다. 
        assertThat(bean).isInstanceOf(DiscountPolicy.class); // 인터페이스로 검증한다. (부모타입)
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);

        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
        }

        assertThat(beansOfType.size()).isEqualTo(2);

    }

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

    @Configuration
    static class TestConfig {

        //DiscountPolicy로 조회한다면 해당 인터페이스를 상속한 두 빈들이 조회될것이다.
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }

    }

}

위에서 findBeanByParentTypeDuplicate() 의 메소드의 경우 동일 부모 타입으로 조회 시,

TestConfig 구성 정보를 활용한 컨테이너 인스턴스를 사용하기에 DiscountPolicy 부모 타입의 Bean이 두 개 등록되었다.

 

따라서 getBean(DiscountPolicy.class) 을 호출하면, NoUniqueBeanDefinitionException 예외가 발생함을 테스트 코드로 검증한 것이다.

 

또한 나머지 메소드에서는 해당 예외를 발생시키지 않고, Bean을 조회하는 방법들에 대한 테스트 코드에 대해서 알아보았다.

댓글