본문 바로가기
Back-end/java spring

[Spring] 스프링 컨테이너와 스프링 빈 개념 정리

by kkkdh 2022. 12. 31.
728x90

스프링 컨테이너

  • ApplicationContext스프링 컨테이너라고 한다.
  • 기존에는 개발자가 AppConfig를 사용해 직접 객체를 생성해서 DI 했지만, 스프링 컨테이너를 사용하면 스프링 컨테이너가 이 역할을 대신해 준다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 이때 이 안에서 @Bean이라는 어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 모두 스프링 컨테이너에 등록한다.
  • 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 부른다고 한다.

 

  • 스프링 컨테이너를 사용하게 되면, 기존의 AppConfig를 사용한 조회와 다르게 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)을 찾도록 바뀌어야 한다.
  • 필요한 스프링 빈은 applicationContext.getBean() method를 사용해서 찾을 수 있다.

AppConfig만을 이용해 직접 DI 하는 방식에 비해 스프링 컨테이너를 사용할 시 훨씬 많은 코드가 돌아가고 오히려 느려지는 것 같은데, 그럼에도 스프링 컨테이너를 사용하는 것에 굉장히 큰 이점이 있다고 한다.

 

하나씩 정리해 보자.


스프링 컨테이너의 생성 과정

먼저 스프링 컨테이너의 생성 과정부터 정리해 보자.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

이렇게 긴 이름의 코드를 통해서 스프링 컨테이너가 생성된다.

 

  • ApplicationContext를 스프링 컨테이너라고 한다.
  • ApplicationContext는 인터페이스이다. -> 여기에도 DIP가 적용되어 있음을 알 수 있다.
    • 즉, AnnotationConfigApplication도 구현체 중 하나임을 알 수 있다.

지금 AnnotationConfigApplication method를 사용한 이유는 AppConfig가 Java config 설정을 기반으로 작성되었기 때문이다.

  • XML 기반으로도 스프링 컨테이너를 만들 수 있고, 위의 경우처럼 어노테이션 기반의 자바 설정 클래스로도 스프링 컨테이너를 만들 수 있다.
참고: 더 정확하게는 스프링 컨테이너를 부를 때, BeanFactory, ApplicationContext로 구분해서 이야기한다. 이 부분은 뒤에서 설명한다고.. 하지만, BeanFactory를 직접 사용하는 경우가 거의 없기 때문에, 일반적으로 ApplicationContext를 스프링 컨테이너라고 한다.

 

이제 실제로 스프링 컨테이너가 생성되는 개념적인 과정을 그림과 함께 정리해 보자.

1. 스프링 컨테이너의 생성

  • 예제의 new AnnotationConfigApplicationContext(AppConfig.class) 호출
  • 스프링 컨테이너를 생성할 때에는 구성 정보를 지정해줘야 하고, 여기서는 AppConfig.class가 이에 해당한다.

2. 스프링 빈 등록

스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해 스프링 빈을 등록한다. @Bean 어노테이션이 붙은 메서드를 모두 실행해서 반환되는 객체를 스프링 컨테이너 내에 있는 빈 저장소에 등록한다.

 

빈 저장소에는 빈 이름(메서드 이름이 기본값)과 빈 객체의 key, value쌍으로 저장된다.

 

빈 이름 지정 방식

  • 메서드의 이름을 기본값으로 사용한다.
  • 물론 빈의 이름을 직접 부여할 수도 있다.
주의: 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나 기존 빈을 덮어버리는 등 설정에 따라 오류가 발생할 수 있다. >>> 우리는 문제를 단순화할수록 좋기 때문에 그냥 실무에서는 무조건 빈의 이름을 다르게 부여하는 식으로 하자.

3, 4번 과정에서 보이듯이 스프링 컨테이너는 설정 정보(여기서는 AppConfig)를 참고해 의존관계를 주입한다. (DI)

단순히 자바 코드를 호출하는 방식(기존의 방식)과는 차이가 있는데, 이 차이는 차후에 정리해 보자.

 

실제로는 스프링 빈을 등록하면, 생성자를 호출하면서 의존관계 주입도 한 번에 처리된다고 하는데, 이해를 돕기 위해 개념적으로 나누어 설명된 것이라고 한다.


컨테이너에 등록된 모든 빈을 조회하는 방법

다음과 같은 테스트 코드를 작성해서 JUnit을 이용해 스프링 컨테이너에 올라간 모든 빈을 조회할 수 있다.

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈을 출력해보자.")
    void findAllBean(){
        // getBeanDefinitionNames method를 이용해 모든 빈의 이름을 반환받는다.
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
        }
    }
}

여기서 빨간색 부분은 우리가 원하지 않는 스프링 환경 세팅을 위한 빈 정보이다.

빨간색 정보를 제외하고 사용자가 등록한 빈 만을 조회하기 위해서는 다음과 같은 코드를 사용할 수 있다.

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

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

여기서는 BeanDefinition을 이용해 빈의 역할에 따라 직접 등록한 애플리케이션 빈만을 조회할 수 있도록 구현할 수 있었다.

이렇게 내가 등록한 빈만 전체 조회가 가능하다.


스프링 빈 조회 - 기본

가장 기본적인 스프링 빈을 조회하는 방법을 하나씩 알아보자.

 

1. 빈 이름과 object type으로 찾는 방법

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

@Test
@DisplayName("빈 이름으로 조회하기")
void findBeanByName(){
    // bean name, bean object type을 인자로 받는다.
    Object memberService = ac.getBean("memberService", MemberService.class);

    // 반환받은 객체가 MemberServiceImpl class의 instance인지를 검증한다.
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}

위와 같이 getBean method에 두 개의 인자(bean 이름, bean 객체 type)를 전달해서 빈을 조회할 수 있다.

 

2. 이름 없이 타입으로만 조회하는 방법

@Test
@DisplayName("이름 없이 타입으로만 조회하기")
void findBeanByType(){
    // type으로만 찾을수도 있다.
    Object memberService = ac.getBean(MemberService.class);

    // 반환받은 객체가 MemberServiceImpl class의 instance인지를 검증한다.
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}

위와 같이 이름 없이 타입만으로도 조회할 수 있으나, 같은 타입의 빈이 여러 개인 경우 문제가 발생할 수 있다.

 

3. 추상이 아닌 구체의 타입으로 조회하는 방법

@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2(){
    // bean name, bean object type을 인자로 받는다.
    Object memberService = ac.getBean("memberService", MemberServiceImpl.class);

    // 반환받은 객체가 MemberServiceImpl class의 instance인지를 검증한다.
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}

이렇게 구체에 의존해서 찾을 수 있지만, 권장되는 코드는 아니다. 물론 살면서 예외는 있기 때문에 정리하고 가자.

 

마지막으로 항상 실패에 대한 테스트 코드 또한 작성되어야 한다. 여기서 쓰인 Assertions는 Junit에서 제공하는 것이니 이 점에 유의해야 한다.

@Test
@DisplayName("빈 이름으로 조회X")
void findByNameX(){
    // lambda를 이용해 다음 과정이 실행될 때, 첫 번째 인자로 들어간 오류가 터져야
    // 성공하는 테스트를 작성할 수 있다.
    assertThrows(NoSuchBeanDefinitionException.class,
            () -> ac.getBean("xxxx", MemberService.class));
}

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

간단하게, 생각할 수 있는 방법은 그냥 스프링 빈의 이름을 이용해 원래 방식대로 조회하면 된다.

 

일단 중복된 타입의 빈을 다음과 같이 테스트 환경에서 만들어준다.

@Configuration
static class SameBeanConfig{
    // 여기서만 사용하는 spring bean을 만들어보자.
    @Bean
    public MemberRepository memberRepository1(){
        return new MemoryMemberRepository();
    }

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

두 개의 스프링 빈(memberRepository1, memberRepository2)이 MemberRepository라는 같은 타입을 갖는 구조이다.

 

같은 타입이 있음에도 getBean을 이용해 빈을 조회하는 경우 NoUniqueBeanDefinitionException error가 발생하고, 위와 같은 테스트 코드로 실패 유형에 대한 테스트 작성이 가능하다.

 

이렇게 이름을 이용해서 하나의 스프링 빈을 조회하는 것이 가능하다.

이름을 이용한 빈의 조회

혹은 같은 타입을 갖는 여러 개의 빈을 모두 조회하는 방법 또한 가능하다.

같은 타입을 갖는 모든 빈을 조회
이렇게 결과로 모두 조회됨을 확인 가능


스프링 빈 조회 - 상속 관계에 있는 빈

대원칙은 부모 타입으로 스프링 빈을 조회하는 경우 자식 타입이 모두 조회된다는 점이다. 이 개념만 정리하면, 모든 실습 예제들을 이해할 수 있다.

 

그래서 모든 자바 객체의 최고 부모인 Object 타입으로 스프링 빈을 조회하는 경우, 모든 스프링 빈을 조회한다.

상속 관계가 왼쪽과 같을 때, 오른쪽과 같이 조회한다고 함

import ...

public class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.")
    void findByParentTypeDuplicate(){
        // assertThrows를 이용한 에러 발생 검증 테스트 구현
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class)
        );
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하자.")
    void findByParentTypeBeanName(){
        DiscountPolicy fixDiscountPolicy = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);

        assertThat(fixDiscountPolicy).isInstanceOf(FixDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType(){
        // 이건 별로 좋은 방법이 아니다. 구체에 의존하기 때문이다.
        RateDiscountPolicy rateDiscountPolicy = ac.getBean(RateDiscountPolicy.class);
//        FixDiscountPolicy fixDiscountPolicy = ac.getBean(FixDiscountPolicy.class);

        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllBeanByParentType(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);

        // beansOfType의 size가 2이면, 성공하는 테스트로 구현하자.
        assertThat(beansOfType.size()).isEqualTo(2);

        // 지금은 공부용으로 출력하도록 짜는 것이지, 실무에서는 출력하지 않는 것이 원칙이다.
        // 시스템 규모가 커지면, 일일이 보고있을 수 없기 때문이다.
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key+ " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object type")
    void findAllBeanByObjectType(){
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key+ " value = " + beansOfType.get(key));
        }
        // spring에 있는 내부적인 bean까지 전부 튀어나온다.
    }

    @Configuration
    static class TestConfig{
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }

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

특별히 추가적으로 설명하지 않고, 앞서 정리한 내용을 기반으로 이해 가능해서 일단 넘어감

 

사실 스프링 빈을 우리가 개발하는 코드 상에서 조회하는 경우는 거의 없기 때문에, 스프링 빈 조회에 대한 개념을 많이 활용할 일이 없지만, 구조는 확실하게 파악해놓아야 한다.

 

가끔 순수 자바 애플리케이션에서 스프링 컨테이너를 생성해서 쓸 일이 있는데, 이러한 경우를 위한 정리라고 한다.


BeanFactory와 ApplicationContext 개념 정리

상관 관계는 다음과 같다.

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고, 조회하는 역할을 담당한다.
  • getBean()을 제공한다.
  • 지금까지 우리가 사용했던 대부분의 기능들은 BeanFactory가 제공하는 기능이다.

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해 주는데, 그러면 둘의 차이가 무엇일까??
  • 애플리케이션을 개발할 때에는 빈을 관리하고 조회하는 기능은 물론이고, 수많은 부가 기능이 필요하다.
  • 단순하게 Bean을 관리하는 것을 떠나서, 애플리케이션을 개발하기 위해 공통적으로 필요한 많은 부가 기능을 제공하기 위해 ApplicationContext를 사용한다.

AnnotationConfigApplicationContext는 ApplicationContext의 구현체 중 하나에 해당한다.

다양한 interface들을 상속 받아 부가 기능들을 제공한다.

부가 기능들 하나씩 정리해 보자. (보통 애플리케이션 개발을 위해 공통적으로 필요한 부가 기능 들이다.)

  • 메시지소스를 활용한 국제화 기능
    • 예를 들어 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력하는 부가 기능을 제공한다.
  • 환경변수
    • 실제 운영 단계에 들어서면 3가지 환경(로컬, 개발, 운영)이 있다.
    • 환경 별로 로컬, 개발, 운영들을 구분해서 처리한다.
  • 애플리케이션 이벤트
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원한다.
  • 편리한 리소스 조회
    • 파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회할 수 있도록 한다.
    • 추상화해서 편리하게 쓸 수 있도록 도와준다.

정리!

  • ApplicationContext는 BeanFactory의 기능들을 상속받는다.
  • ApplicationContext는 빈 관리 기능 + 편리한 부가 기능을 제공한다.
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가 기능이 포함된 ApplicationContext를 대신 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라고 부른다.

다양한 설정(구성) 형식을 지원 - 자바 코드, XML

스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계됨

  • 자바 코드 (지금 구현 중인 방식)
  • XML
  • Groovy 등등..

앞서 살펴본 상속 관계의 연장선

위 그림을 보면 알 수 있듯 다양한 구현체를 통해 ApplicationContext(Spring container)를 만들 수 있다.

 

Annotation 기반 자바 코드 설정 사용

  • 앞서 계속해서 사용해온 방식이다. (@Configuration, @Bean 등의 annotation을 사용한다.)
  • new AnnotationConfigApplicationContext(AppConfg.class)와 같은 코드로 설정한다.
  • AnnotationConfigApplicationContext 클래스를 사용하면서, 자바 코드로 된 설정 정보를 넘기면 된다.

XML 설정 사용

  • 최근에는 스프링 부트를 많이 사용하면서, XML 기반의 설정은 잘 사용하지 않는다고 한다. 하지만, 아직 많은 legacy project들이 XML 기반으로 되어 있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있어 한번쯤 배워두는 것도 좋다고 한다.
  • GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다.

AppConfig 기반, 자바 코드 기반으로 설정
xml 파일 기반, 위의 코드와 동일한 설정및 구성 정보를 담고 있다.

xml 기반 설정에 대한 자세한 정보는 아래의 스프링 공식 레퍼런스 문서를 참고하자.

Spring Framework

 

Spring Framework

 

spring.io


스프링 빈 설정 메타 정보 - BeanDefinition

이렇게 다양한 방식으로 스프링 빈 설정 정보를 세팅할 수 있는 이유가 뭘까?

 

이것은 바로 이 과정에도 BeanDefinition이라는 추상화를 적용했기 때문이다. 개념을 더 자세하게 정리해 보자.

 

  • 스프링이 이렇게 다양한 설정 형식을 지원하는 것의 중심에는 BeanDefinition이라는 추상화가 있다.
  • 쉽게 이야기해서 이것 또한 역할과 구현을 개념적으로 나눈 것이다!!
    • XML을 읽어 BeanDefinition을 만들면 된다!
    • 자바 코드를 읽어 BeanDefinition을 만들면 된다!
    • 스프링 컨테이너는 자바 코드 기반인지, XML 기반인지 몰라도 된다. 오직 BeanDefinition만 알면 된다. (BeanDefinition에만 의존하고 있다.)
  • BeanDefinition을 빈 설정 메타정보라고 한다.
    • @Bean, <bean> 당 각각 하나의 메타 정보가 생성되는 구조이다.
  • 스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다.

이런 식으로 추상화가 이뤄져있습니다.

  • AnnotationConfigApplicationContextAnnotatedBeanDefinitionReader를 사용해 AppConfig.class(인자로 들어옴)를 읽고, BeanDefinition을 생성한다.
  • GenericXmlApplicationContextXmlBeanDefinitionReader를 사용해 appConfig.xml 설정 정보를 읽어 BeanDefinition을 생성한다.
  • 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어 BeanDefinition을 생성하도록 직접 구현할 수도 있다.

BeanDefinition 살펴보기

직접 테스트 코드를 돌려보면, getBeanDefinition method를 이용해서 BeanDefinition이 갖고 있는 다양한 빈에 대한 정보를 확인할 수 있습니다.

test 코드를 돌려보면, Bean 정보를 확인해볼 수 있다.

정리

  • BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수도 있다. 하지만, 실무에서 거의 사용할 일은 없기 때문에, 참고만 하자!
  • BeanDefinition에 대해서는 너무 깊이 있게 이해하기보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.
  • 가끔 스프링 관련 코드나 오픈 소스 코드를 볼 때, BeanDefinition이라는 것이 보일 때가 있는데, 이때 이러한 메커니즘을 떠올리면 된다고..

 

728x90

댓글