본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 핵심원리 : 기본편 - 스프링 빈 조회

by 방구석 대학생 2022. 2. 13.

"인프런의 스프링 핵심원리 - 기본편 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법을 알아보자.

- ac.getBean(빈 이름, 타입)

- ac.getBean(타입)

- 조회 대상 스프링 빈이 없으면 예외가 발생한다.

    - NoSucuBeanDefinitionException : No nean named 'xxxxx' available

 

test 폴더의 hello.core.beanfind 패키지 아래에 다음과 같이 ApplicationContextBasicFindTest 클래스를 만들어 코드를 작성해보자.

- ApplicationContextBasicFindTest.java

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

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

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

    // 항상 테스트는 실패 테스트도 함께 만들어야 한다.
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX(){
        // 존재하지 않는 스프링 빈 조회
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx", MemberService.class));
    }
}

1. 첫번째 테스트는 스프링 빈 객체의 이름을 기준으로 스프링 빈을 검색한 것으로, 가장 기본적인 스프링 빈 객체 검색 방법이다.

2. 두번째 테스트는 찾고자 하는 스프링 빈 객체의 이름없이, 해당 객체의 타입만으로 스프링 빈 객체를 검색하는 것으로 이 또한 문제없이 스프링 빈 객체를 가져온다는 것을 알 수 있다.

- 이것이 가능한 이유? 아직까지는 한 타입의 인터페이스를 구현하는 객체가 하나씩만 등록되고 있기 때문일 것이다. 동일한 타입이 둘 이상일 경우의 검색 방법은 뒤에서 설명한다.

3. 세번째 테스트는 구현 타입을 통해 스프링 빈 객체를 조회한 것이다.

- 추상 타입(인터페이스) 이 아니라, 구체 타입(구현 클래스) 으로 조회해도 상관없다.

- 스프링 빈 객체를 등록할 때 생성된 객체의 인스턴스 타입(구현체 타입) 을 보고 생성하기 때문에 구현체를 대상으로 조회해도 문제없이 스프링 빈 조회가 가능하다.

- 하지만 구현체를 대상으로 조회하는게 좋지만은 않다.

- 왜냐하면 결국 추상 타입이 아닌 구현 타입에 의존하여 코드를 작성한 것이여서 DIP 원칙 위반이 될 수 있기 때문이다.

(코드의 유연성이 떨어진다.)

4. 네번째 테스트는 스프링 빈 객체 조회의 실패 테스트이다.

- 스프링 빈 조회에 실패할 경우 NoSuchBeanDefinitionException 이 발생해야 한다.

- 예를 들어 존재하지 않는 이름의 스프링 빈 객체를 조회할 경우 위와 같은 익셉션이 발생해야 한다.

- 람다식을 통해 존재하지 않는 스프링 빈 객체를 조회하여 익셉션이 발생하도록 하였다.

 

 

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

동일한 타입의 스프링 빈 객체가 둘 이상인 경우

- 타입으로 스프링 빈 객체를 조회 시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.

- ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.

hello.core.beanfind 패키지에 다음과 같이 ApplicationContextSameBeanFindTest 클래스를 만들고 코드를 작성해보자.

- ApplicationContextSameBeanFindTest.java

import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
    void findBeanByTypeDuplicate(){
        MemberRepository bean = ac.getBean(MemberRepository.class);
    }

    // 지금 테스트 클래스 내부에서만 사용할 설정 클래스
    @Configuration
    static class SameBeanConfig{


        // 동일한 타입의 스프링 빈 객체를 2개 이상 만든다.
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

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

- 내부 static 클래스인 SameBeanConfig 를 통해 동일한 타입을 가지는 두 개의 스프링 빈 객체를 등록했다.

- 해당 설정 파일을 통해 스프링 컨테이너를 만들고, 스프링 컨테이너에 등록된 두 개의 스프링 빈 객체의 타입을 대상으로 객체 조회를 해보았다.

- 그 결과 아래와 같은 로그를 출력하는 것을 확인할 수 있었다.

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

NoUniqueBeanDefinitionException, 즉 정의된 빈 객체가 유일하지 않다.(NoUnique) 는 뜻의 익셉션이다.

친절하게도 그 뒤엔 MemberRepository 타입의 객체가 하나가 아니라 두 개가 반환되었고, 자세하게 어떤 빈들이 반환 되었는지를 알려주고 있다.

 

그렇다면 이와 같이 동일한 타입의 스프링 빈 객체가 2개 이상 등록 되었을 경우 해당 빈 객체들의 조회는 어떻게 해야할까?

아래의 코드를 살펴보자.

- ApplicationContextSameBeanFindTest.java

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

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

    // 동일한 타입을 가지는 두 객체를 조회할 때 문제없이 조회하려면 어떻게 해야할까?
    // 첫번째 방법 : 그냥 이름을 지정해서 꺼낸다.
    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findByName(){
        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{


        // 동일한 타입의 스프링 빈 객체를 2개 이상 만든다.
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

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

1. 첫번째 테스트는 타입만으로 스프링 빈 객체를 조회할 시, 해당 타입의 스프링 빈 객체가 2개 이상인 경우 익셉션이 발생하는 것에 대한 테스트이다.

2. 두번째 테스트는 동일한 타입을 가지는 두 객체를 조회할 때, 문제 없이 스프링 빈 객체를 조회하는 첫번째 방법으로 그냥 이름을 직접 지정하여 하나씩 조회함으로서 익셉션 발생을 방지한다.

3. 세번째 테스트는 이번 글의 진짜 목적인 동일한 타입의 두 객체를 한번에 조회해 오는 방법이다.

- getBeansOfType() 메소드를 통해 동일한 타입을 가지고 있는 스프링 빈 객체들을 Map 형태로 반환받는다.

- 반복문을 통해 해당 객체들의 이름과, 해당 객체들의 타입을 출력한다.

- 마지막으로는 Map 에 반환된 스프링 빈 객체의 갯수가 2개가 맞는지 검증하면서, 정상적으로 동일한 타입의 스프링 빈 객체들을 모두 반환 받은것인지 확인한다.

 

위와 같이 getBeansOfType() 메소드를 활용하면, 동일한 타입을 가지는 스프링 빈 객체가 2개 이상 이더라도 정상적으로 스프링 빈 객체들을 조회하여 가져올 수 있다.

 

 

스프링 빈 조회 - 상속관계

- 스프링 빈을 조회할 때 기본 원칙으로 부모 타입을 조회하면, 자식 타입도 함께 조회한다.

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

hello.core.beanfind 패키지 아래에 ApplicationContextExtendsFindTest 클래스를 만들고 다음과 같이 코드를 작성하자.

- ApplicationContextExtendsFindTest.java

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 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 findAllBeanByParentType(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);

        for (String key : beansOfType.keySet()) {
            // 참고로 출력문의 경우 학습 때문에 출력문을 작성해주는 것이지
            // 실무에서 테스트 코드를 작성할 경우 출력문을 작성하지 않는 것이 좋다.
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanObjectType(){
        // 스프링 내부에 있는 모든 스프링 빈 객체들이 전부 튀어나온다.
        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();
        }
    }
}

1. 내부 설정 클래스인 TestConfig 를 통해 동일한 타입을 가지는 두 개의 스프링 빈 객체를 생성하여 스프링 컨테이너에 등록하였다.

2. 첫번째 테스트는 부모 타입으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생하는 것을 확인하는 테스트이며, 두번째 테스트는 그럴 경우 앞선 바와 같이 객체의 이름을 직접 지정하여 정상적으로 스프링 빈 객체를 조회하는 테스트이다.

3. 세번째 테스트는 특정 하위 타입으로 스프링 빈 객체를 조회하는 테스트인데, 이는 그다지 좋은 방법은 아니다. - 코드가 구현체에 의존하기에 DIP 원칙에 위배될 여지가 있다.

4. 네번째 테스트는 진짜 목적 중 하나로, 부모 타입으로 스프링 빈 객체를 조회할 시 자식 타입의 스프링 빈 객체들까지 모두 조회하는 테스트이다.

- 참고로 이번에 테스트 코드를 작성하는 동안 사용한 출력문의 경우 학습 때문에 출력문을 사용하는 것이지 실무에서 테스트 코드를 작성할 경우엔 출력문을 작성하지 않는 것이 좋다.

5. 다섯번째 테스트는 모든 클래스의 부모 클래스인 Object 타입을 통해 자식 타입의 스프링 빈 객체를 조회하는 테스트로, 조회결과 스프링 빈 내부에서 필요에 의해 만들어진 스프링 빈 객체들 까지모두 조회되는 것을 확인할 수 있다.

 

다섯번째 테스트 결과 :