인프런/스프링핵심원리(기본)

11)컨테이너에 등록된 모든 빈 조회,내가 등록한 빈 조회, 스프링 빈 조회(기본), 스프링 빈 조회(동일한 타입이 둘 이상), 스프링 빈 조회(상속 관계)

backend dev 2022. 12. 27.

테스트 코드를 생성해서 등록된 빈 조회해보기

ApplicationContextInfoTest.java

class ApplicationContextInfoTest { // junit5부터는 public이 없어도 된다.

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //컨테이너에 등록된 빈 이름들 배열을 가져온다. 
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);// 받아온 빈의 이름을 가지고 하나씩 꺼내본다, 타입은 지정안했기때문에 object로 꺼내진다.
            System.out.println("name = " + beanDefinitionName + "    Object : "+bean);

        }
    }
}

결과

빨간색 부분은 스프링이 내부적으로 필요해서 등록한 빈들 그 밑에 애들이 등록한애들 (AppConfig도 들어가있다)

 

 

리스트나, 배열과 같은 for문으로 돌려서 내용을 하나씩 살펴보려고할때

iter 치고 탭누르면 자동완성 시켜준다.

String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //컨테이너에 등록된 빈 이름들 배열을 가져온다. 
for (String beanDefinitionName : beanDefinitionNames) {
    
}

string배열 밑에서 iter치고 탭누르면 생성되는 모습

 

 

내가 등록한 빈만 보고싶다면

 

스프링에서 내부적으로 필요해서 등록한 빈이 아닌 , 내가 개발하면서 필요해서 등록한 빈들만 보고 싶다면 

다음과 같이 코드를 짜면된다. 

@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //컨테이너에 등록된 빈 이름들 배열을 가져온다.
    for (String beanDefinitionName : beanDefinitionNames) {
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);// 빈에 대한 메타데이터정보를 가져오는 메소드

        //스프링이 필요해서 알아서 등록한 빈들이 아니라 내가 어플리케이션은 개발하기위해 등록한 빈들은 ROLE_APPLICATION이라는 role을 가진다.
        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);// 그런 빈들만 가져와서 출력해본다.
            System.out.println("name = " + beanDefinitionName + "    Object : "+bean);
        }
    }
}

멤버서비스라는 빈에는 멤버서비스임플이라는 구현체가 잘 등록되어있는것을 확인할 수 있다.

이렇게 어떤 빈들이 등록됬는지 눈으로 체크가능하다.

 

 

빈이름과 타입을주거나, 타입만 주거나

 

ApplicationContextBasicFindTest.java

기본적인 빈 조회 테스트들 생성해본다.

 

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
        AppConfig.class); //스프링 컨테이너 생성

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService",
            MemberService.class); // 이름과 타입으로 빈을 조회하고 가져온다.
//        System.out.println("memberService = " + memberService);
//        System.out.println("memberService.getClass() = " + memberService.getClass());
        assertThat(memberService).isInstanceOf(
            MemberServiceImpl.class);// 받아온 멤버서비스가 멤버서비스임플의 객체냐? 라고 물어보는 메소드
        //Assertions는 알트엔터로 static화 한다.
    }

    @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("이름 없이 빈 타입으로 조회")
void findBeanByType() {
    MemberService memberService = ac.getBean(MemberService.class); // 이름빼고 타입으로만 조회해서 가져와본다.
    assertThat(memberService).isInstanceOf(
        MemberService.class);// 
}

isInstanceOf(인터페이스.class)가 더 깔끔한것 같다. 어차피 해당 인터페이스의 구현체이므로.

테스트는 항상 실패 테스트도 생성해야한다.

    @Test // 조회가 안될때 -> 항상 테스트는 실패 테스트를 만들어줘야한다.
    @DisplayName("빈 이름으로 조회가 안될때")
    void findBeanByNameX() {
        MemberService memberService = ac.getBean("xxx", MemberService.class); // xxx란 빈을 등록한적없기에 오류가 발생할것이다.
    }

다음과 같이 코드를 짜고 실행 시켜보면 실패가 뜰것이다.

아래와 같은 예외를 보여주면서

이 예외가 떠야 성공라는것을 코드로 구현한다.

org.junit.jupiter.api.Assertions.assertThrows()

assertThrows라는 메소드를 사용할것인데 이 메소드는 junit안에 존재한다.

기니까 static import해서 줄여주고 사용한다.

        //assertThrows() 메소드를 사용할것인데 , 이 메소드는 Assertions 안에 있다. , 코드가 길어지니까 Assertions(Junit버전)도 static import해준다.
//        org.junit.jupiter.api.Assertions.assertThrows()

 

실패테스트 코드 전체

@Test // 조회가 안될때 -> 항상 테스트는 실패 테스트를 만들어줘야한다.
@DisplayName("빈 이름으로 조회가 안될때")
void findBeanByNameX() {
    //예외가 터져야 성공이니까 왼쪽에는 해당 예외를 적어주고, 오른쪽에는 어떤 로직을 실행했을때? 를 적는다.(람다식으로)
    assertThrows(NoSuchBeanDefinitionException.class,
        () -> ac.getBean("xxx", MemberService.class));
}

성공

 

 

스프링 빈 조회 (동일한 타입이 둘 이상일때)

ApplicationContextSameBeanFindTest.java

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
        SameBeanConfig.class); //스프링 컨테이너 생성할건데 기존에 있던 AppConfig.java를 손대서 중복빈을 생성하는것보다 아래서 간단하게 config를 생성해서 넣는다.

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


    @Configuration // 중복 빈을 만들기 위한 임시 config 클래스, 클래스안에 클래스를 생성한다는것은 겉에 클래스안에서만 이 클래스를 사용하겠다는 말이다.
    static class SameBeanConfig{
        @Bean
        public MemberRepository memberRepository1() { //빈이름은 다르지만 타입이 동일한 빈이 2개있다.
            return new MemoryMemberRepository();
        }
        // return new MemoryMemberRepository("1000");와 같이 다른 전달인자(파라미터)를 받아 새로운 성능을 가지는 리포지토리를 만들고싶을때
        // 이런 중복 타입의 빈을 가지는것은 흔하다고 한다.
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }

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

테스트를 돌렸을시 나오는 예외

 

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

라는 예외가 발생한다. ( 하나만 있어야하는데 두개있어서 유니크하지않다.)

 

예외를 가지고 실패 테스트를 생성해준다.

@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
void findBeanByTypeDuplicate() {
    Assertions.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);
    //iter 치고 tab 눌러서 아래 반복문 쉽게 생성하기.
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + "  value = " + beansOfType.get(key));
    } // key는 빈 이름 , value에는 타입이 나온다.
    System.out.println("beansOfType = " + beansOfType); // map 받은거 그냥 한번 출력도 해본다.

    //검증 , 디테일하게 해야하지만 간단하게한다 (2개맞는지 체크)
    assertThat(beansOfType.size()).isEqualTo(2);

}

 

 

 

스프링 빈 조회 (상속 관계)

1번으로 조회하면 1~7까지 나오고, 2로 조회하면 2~5, 4로 조회하면 4만 나온다.

스프링에서 부모타입으로 조회하면 자식타입들까지 다 끌려나온다. ( 대 원칙)

자바의 최상의 부모는 Object이다, 모든 클래스는 보이지않지만 extends Object.class를 하고있는것이다.

 

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 discountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class); //이름도 같이 전달
        assertThat(discountPolicy).isInstanceOf(DiscountPolicy.class); // DiscountPolicy의 객체(구현체)가 맞는지 체크
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회") // 안좋은방법이다. 구현체로 검색하기때문
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);//찾고자 하는 타입으로 바로검색( 하나밖에 없을떄)
        assertThat(bean).isInstanceOf(DiscountPolicy.class); // 인터페이스를 가지고 검증한다. 인터페이스를 상속한 구현체클래스일것이니까.
//        assertThat(bean).isInstanceOf(RateDiscountPolicy.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));
        }

        //검증  간단하게 2개인지 체크해서 검증한다.
        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();
        }

    }

}

 

 

테스트코드 작성시 출력하는코드는 빼야한다.

실제 테스트 케이스를 짤때는 출력물을 남기면 안된다. 출력물을 눈으로 확인안하려고 테스트를 짜는것이기 때문이다. 

시스템이 성공인지 실패인지 판단하게끔하려고 짜는것이기 때문에,

테스트 자체를 디버깅하고싶을때는 사용한다.

댓글