본문 바로가기

Spring/Spring Basic

스프링 빈 조회

스프링 빈 조회

스프링 컨테이너에 스프링 빈을 등록했다면 조회할 수도 있습니다. 스프링 컨테이너에서 스프링 빈을 조회하는 가장 기본적인 방법은 getBean(빈이름, 타입) 또는 getBean(타입) 함수를 사용하는 것입니다.

 

만약 조회 대상 스프링 빈이 없다면 NoSuchBeanDefinitionException 예외를 발생합니다.

 

먼저 스프링 컨테이너에 등록된 모든 스프링 빈을 조회하는 코드를 살펴봅시다.

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= " + beanDefinitionNames + " 0bject= " + bean);
     }
 }

 

스프링 컨테이너인 ac를 생성하고 getBeanDefinitionNames 함수를 통해 스프링 컨테이너가 가진 스프링 빈들의 이름을 문자열 배열로 리턴 받습니다. for 루프로 각 스프링 빈 이름을 getBean(빈이름) 함수로 빈 객체를 조회합니다.

 

스프링 컨테이너에 등록된 빈은 개발자 또는 라이브러리에서 직접 등록한 빈(ROLE_APPLICATION)과 스프링 내부적으로 등록된 빈(ROLE_INFRASTRUCTURE)으로 구분할 수 있습니다.

 

애플리케이션 빈만 출력하는 코드를 살펴봅시다.

@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
    // 스프링에 등록된 모든 빈 이름을 조회한다.
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for(String beanDefinitionName: beanDefinitionNames) {
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            
        // Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
        // Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name= " + beanDefinitionNames + " 0bject= " + bean);
        }
    }
}

 

모든 빈을 조회할 때와 마찬가지로 getBeanDefinitionNames 함수로 모든 빈의 이름을 조회합니다.

하지만 BeanDefinition이라는 타입이 등장합니다. getBeanDefinition(빈이름) 함수를 통해 특정 빈에 대한 BeanDefinition을 얻을 수 있습니다. 

 

BeanDefinition은 빈에 대한 메타데이터 정보가 저장되어 있어 빈이 애플리케이션 빈인지 개발자가 직접 등록한 빈인지에 대한 정보도 getRole 함수를 통해 얻을 수 있습니다. BeanDefinition은 빈 설정 메타정보로 빈 하나당 하나씩 생성됩니다. 

 

빈을 조회할 때는 이름으로 조회하거나 이름 없이 타입으로만 조회할 있고 추상화 인터페이스가 아닌 구체 타입으로도 조회할 있습니다.

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() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("존재하지 않는 빈 이름으로 조회X")
    void findBeanByNameX() {
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                    () -> ac.getBean("xxxxx", MemberService.class));
    }
}

 

동일한 타입의 빈을 조회

하지만 타입으로만 조회할 경우 동일한 타입이 둘 이상일 경우 NoUniqueBeanDefinitionException 예외가 발생합니다. 

이처럼 타입으로 조회 시 같은 타입이 둘 이상 있을 경우 빈 이름을 지정하면 해결됩니다.

 

아래 코드로 확인해 봅시다.

class ApplicationContextSameBeenFindTest {
    
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 중복 오류가 발생한다.")
    void findBeanByTypeDuplicate() {
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
                    () -> ac.getBean(MemberRepository.class));
    }
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findAllBeanByName() {
        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);
    }
    
    // 테스트를 위한 Config.class
    @Configuration
    static class SameBeanConfig {
        
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }
        
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

 

부모 타입으로 빈 조회

스프링 빈을 getBean으로 조회하는 방법 말고 getBeansOfType 함수를 통해 특정 타입에 해당되는 모든 빈을 조회할 수 있습니다.

이때 부모 타입으로 조회하면 자식 타입 빈들도 함께 조회됩니다. 이는 스프링 빈 조회의 대원칙입니다.

 

따라서 자바 객체의 최고 부모인 Object 타입으로 빈을 조회하면 모든 스프링 빈이 조회됩니다.

 

getBeansOfType(특정 타입) 함수로 특정 타입과 자식 타입에 해당하는 빈들을 Map 구조로 리턴합니다.

스프링 빈이 컨테이너에서 Map 구조로 저장되기 때문에 리턴도 Map 구조 그대로 리턴됩니다.

 

아래 코드로 부모 타입으로 조회할 경우 자식 타입의 빈까지 조회되는 코드를 살펴봅시다.

public class ApplicationContextExtendsFindTest {
    
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    
    @Test
    @DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류가 발생한다.")
    void findBeanByParentTypeDuplicate() {
        Assertions.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 rateDiscountPolicy = ac.getBean(RateDiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }
    
    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        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 {
        
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

 

위 ApplicationContextExtendsFindTest 클래스의 TestConfig 클래스에서 rateDiscountPolicy 함수의 리턴 타입이 인터페이스인 DiscountPolicy로 되어있습니다.

 

하지만 rateDiscountPolicy 함수가 스프링 빈에 등록될 때의 타입은 리턴되는 RateDiscountPolicy가 됩니다. 사실 스프링 빈을 getBean 함수로 조회할 일은 많지 않습니다. 가끔 순수한 자바 애플리케이션에 스프링 컨테이너를 생성해서 써야 일이 있을 사용합니다. 하지만 기본적인 스프링 컨테이너의 기능이니 알아두도록 합시다.