스프링 핵심 원리 05] 스프링 컨테이너와 스프링 빈

72011 단어 SpringSpring

본 포스트는 Inflearn 김영한 선생님 강의를 정리한 것 입니다!

지난 포스트까지 객체 지향 프로그래밍에 대해 설명하였고, 스프링이 왜 만들어지게 되었는지, 어떤 일을 도와주는지에 대해서 알아보았습니다.

그럼 이번 포스트부터는 진짜 스프링에 대해 알아볼텐데요.

그중 가장 기본이 되는 스프링 컨테이너, 스프링 빈에 대해 알아보겠습니다.

1. 스프링 컨테이너 생성

지난 포스트에서 구현 객체들을 구성하는데 AppConfig 클래스를 사용했었습니다.

스프링에서는 이 AppConfig 클래스에 작성된 구성 정보를 이용해 구현 객체들을 Bean으로 만들어 스프링 컨테이너에 등록하였고, 이 스프링 컨테이너에서 우리는 필요한 구현객체들을 조회해 사용했었죠.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

이렇게 스프링 컨테이너를 생성할 수 있었습니다.

(위 AnnotationConfigApplicationContext는 인터페이스인 ApplicationContext의 구현체입니다.)

우리는 AppConfig에 @Configuration 어노테이션을 달고 이를 구성 정보로 사용었었는데, 스프링 컨테이너는 XML로 작성된 구성정보를 통해서도 만들수 있습니다.

그러나 최근에는 XML보다는 우리가 사용했던 어노테이션 기반 스프링 컨테이너를 사용하는 경우가 더 많습니다.

참고
ApplicationContext 상위에 BeanFactory 인터페이스가있습니다. 하지만 BeanFactory를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext를 스프링 컨테이너라고 합니다.

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

i) 스프링 컨테이너 생성

AppConfig.class XML같은 구성정보를 파라미터로 넘겨 new AnnotationConfigApplicationContext(AppConfig.class) 로 컨테이너를 생성합니다.


ii) 스프링 빈 등록

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

기본적으로 메소드 네임으로 빈 이름이 생성되지만

@Bean(name = "beanName") 을 이용해 직접 이름을 지정할 수 도 있습니다.

단, 빈 이름이 겹치게 되면 오버라이드 돼버리니, 항상 다른 이름으로 등록해야합니다.


iii) 스프링 빈 의존관계 설정

구성 정보를 참고해 의존관계를 주입합니다. (DI)

단순히 자바코드를 호출하고 반환되는 객체를 빈으로 등록하는 것은 아닙니다.

이 차이는 뒤에서 다시 다뤄보도록 하겠습니다.

참고
스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있습니다.

그러나 자바 코드로 빈을 등록하면 생성자가 호출되면서 의존관계 주입도 한번에 처리됩니다.

2. 컨테이너에 등록된 빈 조회

실제로 빈이 잘 등록되었는지 확인해보도록 하겠습니다.

package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

}

모든 빈 출력하기
스프링에 등록된 모든 빈 정보를 출력해 보았습니다.

  • ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회합니다 (key값)

  • ac.getBean(): 빈 이름으로 빈 객체를 조회합니다.

모든 빈 출력하기 실행 결과

name=org.springframework.context.annotation.internalConfigurationAnnotationProcessor object=org.springframework.context.annotation.ConfigurationClassPostProcessor@7e38a7fe
name=org.springframework.context.annotation.internalAutowiredAnnotationProcessor object=org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@366ef90e
name=org.springframework.context.annotation.internalCommonAnnotationProcessor object=org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@33e01298
name=org.springframework.context.event.internalEventListenerProcessor object=org.springframework.context.event.EventListenerMethodProcessor@31e75d13
name=org.springframework.context.event.internalEventListenerFactory object=org.springframework.context.event.DefaultEventListenerFactory@a5b0b86
name=appConfig object=hello.core.AppConfig$$EnhancerBySpringCGLIB$$17c5aae0@4b3c354a
name=memberService object=hello.core.member.MemberServiceImpl@78fb9a67
name=orderService object=hello.core.order.OrderServiceImpl@73ff4fae
name=memberRepository object=hello.core.member.MemoryMemberRepository@21aa6d6c
name=discountPolicy object=hello.core.discount.RateDiscountPolicy@b968a76

org.springframwork ... 이 객체들은 우리가 등록한 빈이 아니라 스프링 프레임워크 내부에서 사용되는 빈들입니다.

아래 appConfig, memberService, orderService ...
우리가 실습시간에 등록했던 빈이 잘 조회되는 것을 확인할 수 있습니다.

만약 우리가 등록한 애플리케이션 빈만을 조회하고 싶다면 아래 코드를 참고해, ROLE_APPLICATION로 필터링을 하면 됩니다.

@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);
        }
    }
}
출력 결과

name=appConfig object=hello.core.AppConfig$$EnhancerBySpringCGLIB$$17c5aae0@150ab4ed
name=memberService object=hello.core.member.MemberServiceImpl@3c435123
name=orderService object=hello.core.order.OrderServiceImpl@50fe837a
name=memberRepository object=hello.core.member.MemoryMemberRepository@3a62c01e
name=discountPolicy object=hello.core.discount.RateDiscountPolicy@7a8fa663

3. 스프링 빈 조회

이제 개발하는 동안 필요한 빈을 하나씩 조회하는 방법을 알아보겠습니다.

음.. 영한쌤이 말씀하시길 실무에서 스프링 컨테이너에서 직접 빈을 조회하는 일은 거의 없다고 합니다.

그래도 정리해봅시다.

기본 조회

먼저 스프링 컨테이너를 생성하고

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
  • 빈 이름으로 조회
    MemberService memberService = ac.getBean("memberService", MemberService.class)

  • 빈 타입으로 조회
    MemberService memberService = ac.getBean(MemberService.class)

  • 구체 타입으로 조회 (구체 타입으로 찾는것은 좋지 못하다.)
    MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class)

존재하지 않는 빈을 조회한다면 NoSuchBeanDefinitionException.class 예외가 발생합니다.

테스트 케이스

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

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() {
        MemberServiceImpl memberService = ac.getBean("memberService",
                MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        //ac.getBean("xxxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class, () ->
                ac.getBean("xxxxx", MemberService.class));
    }
}

동일한 타입이 둘 이상일 때

타입으로 조회시 같은 타입이 둘 이상이면 NoUniqueBeanDefinitionException.class예외가 발생합니다.

이를 해결하기 위해서는 빈의 이름을 지정해 조회하거나 모든 빈을 collection으로 조회해야 합니다.

Ex) MemberRepository1, MemberRepository2 이렇게 두개가 존재할 경우.

  • MemberRepository memberRepository = ac.getBean("MemberRepository1", MemberRepository.class);
  • Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);

테스트 케이스

빈이름 중복을 발생시키기 위해 이 테스트 케이스에서만 사용되는 SameBeanConfig를 임의로 만들어서 사용하였습니다.

package hello.core.beanfind;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
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;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class ApplicationContextSameBeanFindTest {
    
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(MemberRepository.class);
        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);
        
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }
    
    @Configuration
    static class SameBeanConfig {
        
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

상속 관계

두 빈이 상속관계에 있을경우 부모 빈을 조회하면 자식 타입도 함께 조회됩니다.

따라서 모든 자바 객체의 최상위 클래스인 Object 타입으로 조회하면 모든 스프링 빈이 조회됩니다.

테스트 케이스

타입 중복을 발생시키기 위해 이 테스트 케이스에서만 사용되는 TestConfig를 만들어서 사용하였습니다.

package hello.core.beanfind;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.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.size()).isEqualTo(2);

        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value=" + beansOfType.get(key));
        }
    }

    @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 rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

4. Bean Factory와 ApplicationContext

BeanFactory는 스프링 컨테이너의 최상위 인터페이스입니다.

빈을 조회하고 관리하는 역할을 담당합니다. getBean()메소드를 제공하죠.

우리가 지금까지 사용했던 ApplicationContext는 BeanFactory를 상속받아 빈을 관리하고 조회하는 기능을 제공하고 거기에 다른 인터페이스를 추가로 상속받아 여러 부가기능을 제공하는 인터페이스입니다.

  • 메시지소스를 활용한 국제화 기능
    예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
    message

  • 환경변수
    로컬, 개발, 운영등을 구분해서 처리
    (application.properties 파일을 읽어준다.)

  • 애플리케이션 이벤트
    이벤트를 발행하고 구독하는 모델을 편리하게 지원

  • 편리한 리소스 조회
    파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

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

우리는 스프링 컨테이너를 만들 때 AppConfig라는 자바 코드를 구성정보로 넘겼습니다.

스프링 컨테이너는 자바 코드 뿐만아니라 XML을 구성정보로 넘길 수 있습니다.

이것도 역시 역할(ApplicationContext)과 구현을 나눴기 때문에 가능한 것입니다.

우리는 구현체로 AnnotationConfigApplicationContext를 사용했었습니다.

이 구현체는 자바코드로 된 구성 정보를 읽는 기능을 제공합니다.

이와 달리 또다른 구현체인 GenericXmlApplicationContext는 XML로된 구성 정보를 읽는 기능을 제공합니다.

그럼 실제로 XML설정을 사용해 보도록 하겠습니다

최근에는 스프링 부트를 많이 사용하면서 XML기반 설정은 잘 사용하지 않습니다만 아직 많은 레거시 프로젝트들이 XML로 이루어져 있기 때문에 한번 정리해보겠습니다.

src/main/resources/appConfig.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>

appConfig.xml파일을 GenericXmlApplicationContext의 파라미터로 넘겨 스프링 컨테이너를 생성해줍니다.
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

테스트 케이스를 만들어 사용해보면 appConfig.class와 동일한 것을 확인해볼 수 있습니다.

package hello.core.xml;

import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

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

public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

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

다양한 종류의 구성정보를 사용할 수 있도록 스프링 에서는 그것을 BeanDefinition으로 추상화 합니다.

쉽게말해 XML을 읽어 BeanDefinition을 만들거나 자바 코드를 읽어 만들면 스프링 컨테이너는 이게 XML 출신인지 자바 코드 출신인지 상관없이 BeanDefinition만 읽어 빈을 등록합니다.

이러한 BeanDefinition을 빈 설정 메타정보라고 합니다.

조금더 깊이 있게 들어가보겠습니다.

AnnotationConfigApplicationContext는 Reader를 사용해 자바 코드인 AppConfig.class를 읽고 BeanDefinition을 생성합니다.

이러한 BeanDefinition에는 아래와 같은 정보가 저장되어있습니다.

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

뭐뭐 많기도 한데 이도 실무에서 직접 등록하거나 사용할 일은 거의 없기 때문에 BeanDefinition은 스프링이 다양한 형태의 구성 정보(설정 정보)를 추상화한 것 이라고만 알고 계시면 될 것 같습니다.

좋은 웹페이지 즐겨찾기