[Spring] Basic, 스프링 컨테이너와 스프링 빈

홍정완·2022년 8월 7일
0

Spring

목록 보기
14/32
post-thumbnail
post-custom-banner

스프링 컨테이너 생성


ApplicationContext를 스프링 컨테이너라 부른다고 했다.
이번 포스팅에서는 스프링 컨테이너가 생성되는 과정에 대해 알아보자.

ApplicationContext 인터페이스의 구현체

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class)

  • ApplicationContext는 인터페이스이다. 즉, 다형성이 적용되었다고 할 수 있다.
  • 그러기에 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수도 있다.
  • 이전 포스팅에서 다뤄본 방식(AppConfig)은 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.

✅ 더 정확히는 스프링 컨테이너는 BeanFactory, ApplicationContext로 구분해서 이야기한다. 하지만, BeanFactory를 직접 사용하는 경우는 거의 없기에 일반적으로 ApplicationContext를 스프링 컨테이너라 한다.


스프링 컨테이너 생성 과정

스프링 컨테이너 생성

new AnnotationConfigApplicationContext() 생성자 호출을 통해 스프링 컨테이너를 생성한다. 여기서 해당 생성자의 인자 값으로 구성 정보를 지정해 줘야 하는데, 여기서는 예제 코드로 작성한 구성 정보 클래스인 AppConfig.class를 전달해 준다.


스프링 빈 등록

스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.

여기서 빈 이름은 구성 정보에 있는 메서드 이름을 사용하며, 임의로 애노테이션 속성(name)을 사용해 지정해 줄 수도 있다. 다만, 여기서 빈 이름은 항상 다른 이름을 부여해야 하는데 그 이유는 빈 이름이 중복될 경우 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 에러가 발생할 수 있기 때문이다. 이럴 경우 애노테이션 속성으로 빈 이름을 바꿔줄 수는 있지만, 가장 좋은 건 빈 이름을 별개로 해놓는 거다.

AppConfig의 빈 정보를 읽어와 등록해 준다.


스프링 빈 의존관계 설정

이제 생성된 스프링 빈에 의존관계를 설정해 줘야 한다. 기존 AppConfig에는 MemberService와 OrderServicer가 있는데 이 두 클래스를 생성하기 위해서는 각각 할인정책, 회원 리포지토리를 의존관계를 주입해 줘야 한다. 이런 정보가 AppConfig라는 구성 정보 클래스에 담겨있기에 이를 베이스로 스프링 빈 의존관계를 설정해 준다.


스프링 빈 의존관계 설정


스프링 컨테이너는 위와 같이 설정 정보를 참고해 의존관계를 주입(DI) 해 준다.

이처럼 스프링은 빈을 생성하고 의존관계를 주입하는 단계가 있는데 이처럼 자바 코드를 통해 스프링 빈을 등록하면 생성자를 호출하며 의존관계 주입도 처리가 된다.



컨테이너에 등록된 모든 빈 조회


스프링 컨테이너에 실제로 등록된 스프링 빈들이 제대로 등록되어 있는지 확인해 보자.


public class ApplicationContextInfoTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBeans() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("beanDefinitionName = " + beanDefinitionName + " bean = " + bean);
        }
    }
}

실행하면 모든 빈 정보를 출력할 수 있다.

ac.getBeanDefinitionNames() 메서드에서 스프링에 등록된 모든 빈 이름을 조회하고 ac.getBean()에 해당 beanName을 인자로 넘겨 빈 객체(인스턴스)를 조회한다.


실행 결과를 보면 직접 만들어준 빈 말고도 많은 빈들이 출력되는 걸 볼 수 있다. 이는 스프링의 기능을 위해 생성되는 빈이다. 직접 등록한 빈들만 조회하고 싶다면 아래와 같이 사용을 할 수 있다.

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

이제 내가 만들어준 빈들만 출력되는 것을 볼 수 있다.

코드 중 getRole()을 통해 빈을 구분할 수 있는데, 여기서는 직접 만든 빈(혹은 외부 라이브러리에서 등록한 빈)을 구분하기 위해 BeanDefinitio.ROLE_APPLICATION을 사용해 준다.



스프링 빈 조회 - 기본


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

  • getBean(빈이름, 타입)

  • getBean(타입)

    만약 조회 대상 스프링 빈이 존재하지 않는다면 NoSuchBeanDefinitionException이 발생한다.

public class ApplicationContextBasicFindFirst {
    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 findBeanByImplements() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("등록되지 않은 빈 조회")
    void findBeanByWithoutBean() {
        assertThatThrownBy(()->{
            ac.getBean("xxxx", MemberService.class);
        }).isInstanceOf(NoSuchBeanDefinitionException.class);
    }
}

✅ 구체 타입으로 조회하면 변경 시 유연성이 떨어진다.



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


getBean을 통해 타입으로 빈을 조회할 수 있다고 하였는데, 여기서 해당 타입의 빈이 하나가 아니라 둘 이상이라면 어떻게 될까? 한번 실행해 보자.

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("동일한 타입이 둘 이상인 스프링 빈을 타입으로 조회할 경우 에러가 발생한다.")
    void findBeanByTypeDuplicate() {
        assertThatThrownBy(()->{
            DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        }).isInstanceOf(NoUniqueBeanDefinitionException.class);
    }


    @Configuration
    static class SameBeanConfig {
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }

        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
    }
}
  • 기존 AppConfig에는 중복 타입의 빈 등록이 없다. 하지만 테스트를 위해 프로덕션 코드를 수정하는 건 지양해야 하기에 테스트 클래스 내에 테스트용 구성 정보 객체인 SameBeanConfig 클래스를 만들어준다.

  • 실행 결과는 NoUniqueBeanDefinitionException이 발생한다.


그럼 동일한 타입의 빈이 여러 개 등록되어 있다면, 혹은 에러 없이 이 빈들을 다 꺼내고 싶다면 어떻게 해야 할까? 지정해서 꺼내는 건 이름을 지정해서 조회하는 방법과, 모두 조회하는 방법 두 가지를 모두 사용해 보자.

@Test
@DisplayName("동일한 타입이 둘 이상인 스프링 빈을 타입으로 조회할 빈 이름을 지정한다.")
void findBeanByName() {
    DiscountPolicy discountPolicy = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
    assertThat(discountPolicy).isInstanceOf(FixDiscountPolicy.class);
}

@Test
@DisplayName("특정 타입을 모두 조회한다.")
void findAllBeanType() {
    Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + "value = "+ beansOfType.get(key));
    }
    System.out.println("beansOfType = " + beansOfType);
    assertThat(beansOfType).hasSize(2);
}



스프링 빈 조회 - 상속관계

할인 정책 타입 DiscountPolicy이라는 인터페이스로 빈을 조회해도 구현체들이 모두 조회된다. 이는 부모 타입으로 조회할 경우 자식 타입도 함께 조회되기 때문인데, 그렇기의 자바의 최고 조상 객체인 Object 타입으로 조회하면, 모든 스프링 빈을 조회하게 된다.


public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationContextSameBeanFindTest.SameBeanConfig.class);


    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다. ")
    void findBeanByParentTypeDuplicate() {
        assertThatThrownBy(()->{
            ac.getBean(DiscountPolicy.class);
        }).isInstanceOf(NoUniqueBeanDefinitionException.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).hasSize(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 {
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }

        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
    }
}



BeanFactory와 ApplicationContext


아래 그림을 살펴보면, 최상단에 BeanFactory라는 인터페이스가 있다. 이는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 조회 및 관리하는 역할을 한다. 우리가 지금까지 사용한 getBean 메서드들이 모두 BeanFactory가 제공하는 기능이다.

ApplicationContext 인터페이스는 간단히 말하면 BeanFactory의 기능에 추가적인 기능을 덧붙여 제공하는 인터페이스인데, 실제 해당 인터페이스를 보면 EnvironmentCapable, ListableBeanFactory, MessageSource 등 많은 인터페이스들을 다중 상속받고 있다.


즉, ApplicationContext는 BeanFactory의 기능뿐 아니라 그 밖의 다양한 부가기능을 같이 제공한다고 볼 수 있다.


ApplicationContext가 제공하는 부가기능


  • MessageSource, 메시지 소스를 활용한 국제화 기능

    • 국가별로 그 나라에 맞는 언어로 나오도록 하는 기능
  • EnvironmentCapable, 환경 변수

    • 로컬, 개발, 운영 등을 구분해서 처리할 수 있게 하는 기능
  • ApplicationEventPublisher, 애플리케이션 이벤트

    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 기능
  • ResourceLoader, 편리한 리소스 조회

    • 파일 클래스 패스, 외부 등에서 리소스를 편리하게 조회하게 지원하는 기능

정리

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



다양한 설정 형식 지원 - 자바 코드, XML


  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 유연하게 설계되어 있다.
  • 스프링 컨테이너는 자바뿐 아니라 XML이나 Groovy 등 다양한 방식으로 스프링 컨테이너를 생성할 수 있다.

지금까지는 자바 코드를 이용해 설정 정보를 등록해 줬다면, 이번에는 XML을 사용해 설정 정보를 등록해 보자.


XmlAppContext

@DisplayName("XML을 이용한 스프링 컨테이너 생성을 시도한다.")
public class XmlAppContext {

    @Test
    @DisplayName("XML 파일을 설정 정보로 넘겨준 뒤 빈을 조회한다.")
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

xml 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
        <constructor-arg name="discountPolicy" ref="discountPolicy"/>
    </bean>

    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>

</beans>

위 xml 설정 파일 역시 기존 자바로 작성한 appConfig와 거의 유사하다.
하지만 최근 거의 사용하지 않는 방식이기에 더 자세히 들어가지는 않는다.



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


스프링 컨테이너를 생성할 때 자바, XML, Groovy등 다양한 방식의 설정 형식을 모두 지원하는데, 그 중심에는 BeanDefinition이라는 추상화가 있다.

우리가 XML, 자바 코드 등으로 설정 정보를 전달하면 해당 코드를 읽어와서 BeanDefinition을 만든다.

그렇기에 스프링 컨테이너는 이게 자바 코드 인지, XML 인지 알 필요 없이 전달된 BeanDefinition만 알면 된다. 즉, DIP(의존성 역전 원칙) 원칙을 지킴으로써 얻는 이점이라 할 수 있다. 그럼 이 BeanDefinition은 무엇일까?


BeanDefinition

빈 설정 메타정보로 자바에서는 @Bean, XML에서는 <bean>이 부여된 정보별로 메타 정보를 생성한다.
그리고 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.



조금 더 깊이 있게 들어가 보자.

각각의 설정 정보 클래스에 맞는 BeanDefinitionReader를 사용해 해당 설정 정보를 읽어서 빈 메타정보인 BeanDefinition을 생성해 전달한다. 만약 여기에 없는(ex: JSON 등) 형식의 설정 정보를 추가하고 싶다면 해당 형식을 읽을 수 있는 XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 된다.


이번에는 코드로 BeanDefinition을 실제로 확인해 보자

public class BeanDefinitionTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean() {

        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("beanDefinitionName = " + beanDefinitionName +
                        " beanDefinition = " + beanDefinition);
            }
        }
    }
}

위 코드를 실행하면 결과로 나오는 부분이 AppConfig.class에 작성된 빈들의 BeanDefinition인데, 각각 필드에 대한 간단한 설명을 하면 다음과 같다.

  • BeanClassName : 생성할 빈의 클래스명
  • factoryBeanName : 팩토리 역할의 빈을 선택할 경우 이름(ex: appConfig)
  • factoryMethodName : 빈을 생성할 팩토리 메서드 지정(ex: memberService)
  • Scope : 싱글톤(기본값)
  • lazyInit : 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아닌 빈을 실제 사용할 때 생성하는 지연 로딩
  • InitMethodName : 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName : 빈의 생명주기가 끝나서 제거하기 직전 호출되는 메서드 명
  • Constructor argument, Properties : 의존관계 주입에서 사용한다.(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)

물론, 실무를 하면서 BeanDefinition을 직접 정의해서 사용하는 경우는 흔치않다.

BeanDefinition에 대해서는 너무 깊이 있게 이해하기보다는, 스프링이 다양한 형태의 설정 정보를
BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.

profile
습관이 전부다.
post-custom-banner

0개의 댓글