jhhan의 블로그

스프링 코어(1) - 스프링 컨테이너 & 빈 본문

Spring

스프링 코어(1) - 스프링 컨테이너 & 빈

jhhan000 2021. 1. 4. 22:24

'자바와 스프링 그 사이' 시리즈 포스트에 이어서 진행합니다.

 

스프링 컨테이너와 빈에 대해서 간략하게 알아보려고 합니다.

 

스프링 컨테이너1

지난번 포스트의 코드 중 일부입니다.

  • ApplicationContext를 스프링 컨테이너라고 한다.
  • ApplicationContext는 인터페이스
  • AnnotationConfigApplicationContext는 구현체이다.

실제 ApplicationContext와 AnnotationConfigApplicationContext의 관계를 본다면

AnnotationConfigApplicationContext implements ApplicationContext 인것을 알 수 있다.

 

스프링 컨테이너2

  • 스프링 컨테이너는 xml 기반일 수도 있고, 어노테이션 기반일 수도 있다.
  • 요즘은 xml은 잘 안쓰고, 어노테이션 기반이 대부분

스프링 컨테이너3

  • 사실 스프링 컨테이너는 2가지가 있다.
  • BeanFactory와 ApplicationContext
  • BeanFactory를 직접 사용하는 경우가 거의 없다.
  • why? -> ApplicationContext가 BeanFactory를 상속받음
  • 그래서 ApplicationContext를 거의 씀

 

스프링 빈1

@Configuration
public class AppConfig {

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

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

AppConfig를 통해(설정정보를 통해)

스프링 빈 등록을 진행합니다.

  • 그 과정에서 메서드 이름이 Bean 이름으로 등록됨
  • 그 안에 반환되는 객체를 실제 Bean 객체로 등록됨
  • Bean 이름은 메서드의 이름을 사용, 혹은 직접 부여 가능
  • 주의: Bean 이름은 항상 다른 이름으로 부여해야 함
  • 스프링 컨테이너는 설정 정보를 통해 의존관계 주입(DI)

 

그럼 이제 코드로 돌아와서

AppConfig에 Bean이 제대로 등록이 되었는지 확인을 해봅시다.
(물론 앞에서 다른 예제를 통해 등록이 제대로 되었는지는 확인이 이미 되었습니다... ㅋ)

프로젝트 구조입니다.

beanfind라는 패키지를 만들고, 그 밑에 ApplicationContextInfoTest라는 클래스를 하나 만듭니다.

public 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("bean = " + beanDefinitionName + ", object = " + bean);
        }
    }

}

위의 코드를 적어서 실행을 해봅니다.

그러면 등록된 빈들이 모두 나옵니다.

하지만, 우리가 등록한 빈은 밑에서 5개 뿐입니다.
(appConfig, memberRepository, discountPolicy, memberService, orderService)

그 외에는 사실 봐도 알아보기 힘듭니다.

그래서 저희가 등록한 빈만 알아봅시다.

    @Test
    @DisplayName("Application 빈 출력하기")
    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("Application bean = " + beanDefinitionName + ", object = " + bean);
            }
        }
        System.out.println("------------------------");
//        for (String beanDefinitionName : beanDefinitionNames) {
//            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//            if(beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
//                Object bean = ac.getBean(beanDefinitionName);
//                System.out.println("Infrastructure bean = " + beanDefinitionName + ", object = " + bean);
//            }
//        }
    }

위의 코드를 추가해봅니다. 그리고 실행해봅니다.

AppConfig에 등록한 빈들이 잘 나오는 것을 알 수 있습니다.

그리고 주석에 적었는데

  • ROLE_APPLICATION: 직접 등록한 빈
  • ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈

으로 구분할 수 있습니다.

아래 주석을 해제한 후 실행한다면 - 스프링 내부에서 사용하는 빈 목록만 볼 수 있습니다.

 

다음으로 스프링 빈 조회를 해보겠습니다.

스프링 컨테이너에서 스프링 빈이 등록되었다면, 조회도 가능해야 하겠죠?

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

2가지로 검색이 됩니다.

그리고 스프링 빈이 없으면 - NoSuchBeanDefinitionException이 발생합니다.

확인해보겠습니다.

아까 만든 beanfind 패키지 밑에 ApplicationContextBasicFindTest 클래스를 만듭니다.

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("Bean 이름으로 조회")
    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);
    }

}

먼저 빈 이름(memberService)으로 검색해봅니다.

실행을 해본다면, 조회가 잘 되는 것을 알 수 있습니다.

참고로 assertThat은

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

가 추가되면 자동으로 사용할 수 있습니다.

이번에는 이름없이 검색해보겠습니다.

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

        System.out.println("memberService = " + memberService);
        System.out.println("memberService.getClass() = " + memberService.getClass());

        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

이 역시 실행해본다면, 조회가 잘 되는 것을 확인할 수 있습니다.

그러면 타입을 인터페이스가 아닌 구현체를 사용해도 될까요?

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

        System.out.println("memberService = " + memberService);
        System.out.println("memberService.getClass() = " + memberService.getClass());

        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

위의 코드를 실행해보면, 조회가 잘 됩니다.

하지만 이런 검색은 추천되지 않습니다.

-> 역할이 아닌 구현에 의존하고 있기 때문입니다.
    (항상 강조되는 말입니다.. ㅎㅎㅎ 인터페이스만 사용한다고 생각하는 것이 좋겠습니다.)

-> 물론 추천되지 않을 뿐, 필요하다면 써야겠죠 ㅎㅎ

지금까지는 모두 검색이 되는 조건에서 진행했습니다.

검색이 안되는 경우도 고려해야 합니다.

    @Test
    @DisplayName("Bean 이름으로 조회X")
    void findBeanByNameX() {
//          ac.getBean("xxxxx", MemberService.class); -> NoSuchBeanDefinitionException 예외 발생 : 저런 이름을 가진 빈이 없다!
//        MemberService xxxxx = ac.getBean("xxxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx", MemberService.class));
    }
  • xxxxx 라는 이름을 가진 빈을 찾게 했습니다.
  • 주석 처리가 되어있는데, 만약 해제하고 실행한다면
  • NoSuchBeanDefinitionException이 일어나면서 실패하게 될겁니다.
  • 그래서 이번에는 assertThrows를 사용했습니다. -> import static org.junit.jupiter.api.Assertions.*; 를 추가하면 됩니다.
  • 저 코드를 실행하면 성공으로 끝나게 되는 것을 알 수 있습니다.

 

타입으로 빈을 검색해봤는데, 코드를 짜다보면 동일한 타입이 2개 이상 있을 수도 있습니다.

  • 만약 2개 이상이 된다면 오류가 발생합니다.
  • 이 때는 빈 이름을 지정해서 검색합니다.
  • 혹은 ac.getBeansOfType()을 사용해서 조회합니다.

beanfind 패키지 밑에 ApplicationContextSameBeanFindTest 클래스를 만듭니다.

그리고 그 안에 static class를 하나 만듭니다.

    @Configuration
    static class SameBeanConfig {

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

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

이렇게 되면 MemberRepository 타입을 갖는 빈이 2개가 됩니다.

그리고 기존의 방식으로 검색을 한다면?

public 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));
    }

}

주석을 해제하고 실행한다면 오류가 발생합니다.

그래서 assertThrows로 진행했습니다.

그럼 빈 이름으로 검색해봅니다.

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 빈 이름을 지정하면 됨!")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

memberRepository1으로 검색을 했습니다. -> 테스트가 성공을 한 것을 알 수 있습니다.

그럼 이것을 역이용해서 특정 타입만을 조회할 수도 있습니다.

    @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);
    }

getBeansOfType의 결과값은 Map 형태로 나오게 됩니다.

for문으로 제대로 나왔는지 테스트 해봅니다.

그리고 MemberRepository라는 타입을 가진 녀석이 2개가 있는지 확인해봅니다.
(memberRepository1, memberRepository2 두개를 작성했기 때문에 2개가 나오는 것이 정상입니다.)

테스트를 돌려본다면?

잘 나오는 것을 확인할 수 있습니다.

 

이렇게 스프링 컨테이너, 스프링 빈, 빈 조회 까지 해봤습니다.

사실 빈 조회의 경우 상속 관계를 조회하는 것도 있습니다.

그건 다음 포스트에 진행합니다.

 

 

오늘 진행한 코드들은 전부 테스트 상에서 이루어지는 코드들입니다.

그래서 불필요하다고 느끼실 수도 있습니다.

하지만 실제 현업에서는 테스트 코드 작성이 활발하니 이런 코드를 알아두는 것도 유용할 것 같습니다.

그리고 스프링 컨테이너에 빈이 제대로 등록되었는지, 조회가 가능한지에 대해서도 알 수 있었기 때문에

중요한 부분이라고 생각됩니다.

 

그럼 이렇게 글을 마무리하겠습니다.

 

 

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한

'Spring' 카테고리의 다른 글

스프링 코어(2) - 스프링 컨테이너 & 빈  (0) 2021.01.23
Spring - Excel 파일 다운로드(간단)  (4) 2021.01.06
Spring - AOP  (0) 2020.10.22
Spring - JdbcTemplate  (0) 2020.10.17
Spring - 웹 기능(MVC)  (0) 2020.09.28