본문 바로가기
Spring/Spring core basic

[Spring core basic] 19 - 스프링 빈 조회

by Kloong 2022. 5. 2.

참고

더보기

Spring core basic 시리즈는 김영한 님의 "스프링 핵심 원리 - 기본편" 강의를 정리한 글입니다. 글에 첨부된 사진은 해당 강의의 강의 자료에서 캡쳐한 것입니다. 제 Github에만 올려뒀다가, 정보 공유와 강의 홍보(?)를 위해 블로그에도 업로드합니다. 마크다운을 잘 쓰지 못해서 가독성이 조금 떨어지는 점 양해 바랍니다.

 

 

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

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

www.inflearn.com

스프링 빈 조회

가장 기본적인 스프링 빈 조회 방법

  1. ac.getBean("빈 이름", 타입)
  2. ac.getBean(타입)

조회 대상 스프링 빈이 없으면 예외가 발생한다.
NoSuchBeanDefinitionException: No bean named '빈 이름' available

ApplicationContextBasicFindTest.java

package com.kloong.corebasic1.findbean;

//import 생략

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(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("빈 이름으로 조회 실패")
    void findBeanByNameFailure() {
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("nobean", MemberService.class));
    }
}

스프링 빈 조회시 동일한 타입의 빈이 여러개일 경우

ac.getBean() 메소드를 사용해서 타입으로 조회를 할 경우, 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이 때는 빈 이름을 지정하여서 조회를 하면 된다.

아니면 ac.getBeansOfType() 메소드를 사용해서 해당 타입의 모든 빈을 조회할 수 있다.

ApplicationContextSameTypeBeanFindTest.java

package com.kloong.corebasic1.findbean;

import com.kloong.corebasic1.AppConfig;
import com.kloong.corebasic1.discount.DiscountPolicy;
import com.kloong.corebasic1.member.MemberRepository;
import com.kloong.corebasic1.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ApplicationContextSameTypeBeanFindTest {

    //AppConfig.class 아니고 SameBeanAppConfig.class를 넘겨줬다.
    AnnotationConfigApplicationContext ac =
    new AnnotationConfigApplicationContext(SameBeanAppConfig.class);

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

    //테스트를 위해 AppConfig를 수정하는 일이 발생하지 않도록
    //테스트용 내부 클래스를 새롭게 작성했음.
    @Configuration
    static class SameBeanAppConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

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

MemberRepository 타입의 빈이 2개 존재하므로 ac.getBean(MemberRepository.class)를 실행하면 NoUniqueBeanDefinitionException이 발생한다.

NoUniqueBeanDefinitionException 메세지

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

MemberRepository 타입의 빈이 "memberRepository1"과 "memberRepository2" 이렇게 두개가 존재한다고 한다.

*참고: 실제로 같은 타입의 빈을 여러개 등록하는 경우가 생길 수 있다. 필요에 따라 객체 생성자의 파라미터값을 다르게 해서 같은 타입의 빈을 여러 개 등록하는 경우가 있다.

특정 타입의 모든 빈 조회

ApplicationContextSameTypeBeanFindTest.java

package com.kloong.corebasic1.findbean;

//import 생략

public class ApplicationContextSameTypeBeanFindTest {
    AnnotationConfigApplicationContext ac =
    new AnnotationConfigApplicationContext(SameBeanAppConfig.class);

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeansByType() {
        //Map 형태로 반환된다.
        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);

        //MemberRepository 타입의 빈이 2개 존재한다
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanAppConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

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

굳이 특정 타입의 모든 빈을 조회할 일이 있을까? 라는 생각이 들지만, 내부적으로 이 기능이 활용된다고 한다. 나중에 배운다고 함.

상속 관계를 통한 스프링 빈 조회

여러 스프링 빈의 타입 중 상속 관계에 있는 타입이 있다고 하자. 이런 경우 부모 타입으로 스프링 빈을 조회하면, 자식 타입의 스프링 빈도 함께 조회된다.

따라서 Object 타입으로 조회하면 모든 스프링 빈을 조회할 수 있다.

ApplicationContextFindExtendTypeTest.java

package com.kloong.corebasic1.findbean;

//import 생략

public class ApplicationContextFindExtendTypeTest {

    //내부 클래스를 넘겨줘서 컨테이너를 생성한다.
    AnnotationConfigApplicationContext ac =
    new AnnotationConfigApplicationContext(ExtendTypeAppConfig.class);

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

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

    //구현체에 의존하는 것은 좋지 않다.
    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllBeansByParentType() {
        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 findAllBeansBytObjectType() {
        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 ExtendTypeAppConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

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

실제로는 빈 조회를 직접 할 일은 그리 많지 않다고 한다. 실무 개발을 한다고 하면, 클라이언트 코드의 실제 로직만 개발하고 의존관계에 대한 코드(AppConfig.java)는 건드릴 일이 많지 않다.

간혹 순수 Java 코드에 스프링 프레임워크를 적용하는 경우가 있는데 그런 경우에 빈 조회를 하면 된다.

댓글