관리 메뉴

제뉴어리의 모든것

[스프링 핵심 원리] 스프링 컨테이너와 스프링 빈 본문

Spring Boot/스프링 핵심 원리

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

제뉴어리맨 2022. 8. 8. 21:25

스프링 컨테이너란?

스프링에서 지원하는 기술 중 하나라고 볼 수 있다.

한마디로 표현하자면, 스프링에서 DI (Dependency Injection) 를 지원하고 각 객체들의 Singletone을 보장하기 위하여 정의된 클래스들의 각 객체들을 생성하고 관리하여 담아 두는 공간 이라고 할 수가 있다.

DI 컨테이너라고도 부를 수 있다. 부르는 명칭은 다양한다.

 

 

스프링 컨테이너의 필요성

순수 자바로만 좋은 객체지향 프로그래밍을 하기에는 효율이 떨어지고, 좋은 객체지향 프로그래밍을 하기에 한계가 존재하므로 스프링을 사용한다.

그리고 그런 스프링이 더 편리하게 SOLID 원칙을 지킬 수 있게 지원하는 기능이 스프링 컨테이너이다.

그러므로 스프링 컨테이너를 쓴다.

 

스프링 컨테이너의 역할

스프링 컨테이너란? 에서 설명하였지만 아래와 같이 다양한 역할을 수행한다.

  • 정의된 클래스의 객체를 싱글톤으로 생성
  • 생성된 객체들을 컨테이너라는 곳에 담아 관리
  • 객체들의 소멸까지도 담당
  • 의존성 주입

스프링 컨테이너 생성 방법

  • 팩토리빈 역할을 하는 AppConfig
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {

        return new MemberServiceImpl(memberRepository());
    }

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

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

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

- @Configuration

해당 클래스가 스프링컨테이너의 설정파일임을 스프링에게 알리는 애노테이션.

해당 클래스내에서 생성되는 스프링빈의 싱글톤 타입을 보장해줌.

 

- @Bean

해당 메소드에서 리턴되는 객체가 스프링빈으로 등록될것이라고 스프링에게 알려주는 애노테이션

 

- @Bean이 붙은 메서드들의 리턴타입인

MemberService, MemberRepository, OrderService, DiscountPolicy가 스프링 컨테이너에 스프링 객체로 등록된다.

물론 참조변수의 타입이 인터페이스일뿐 실제 참조변수에 담기는 객체들은 구현체이다. (다형성)

 

- @Configuration은 쓰지 않고 @Bean 만 쓸 경우에는?

스프링빈으로 등록될 객체들의 의존성주입 부분에서

"Method annotated with @Bean is called directly. Use dependency injection instead. " 이란 에러가 발생되긴 하지만, 작동하긴 한다. 그러나, 프로그램 자체는 Run이 잘된다! (이 부분이 의문이다. 분명 빨간줄의 에러 표시인데 왜 Run이 되는걸까.. 직접 호출되진 않는 코드라고는 하지만 원래 프로젝트내에 컴파일에러가 존재하면 Run이 안되는게 정상인데.. 찾아봐야 할듯하다)

그리고 각 메소드의 리턴 객체들은 빈으로 등록이 잘됬지만, 싱글톤 보장이 되지않아서, 자바 코드의 흐름대로 new 로 객체를 할때마다 해당 객체들이 생성된다.

그러므로, @Configuration을 붙여줘야한다.

그럼 어떻게 스프링은 @Configuration 을 붙임으로써 여러번 new로 객체를 생성하는 자바코드의 흐름을 무시하고 싱글톤을 보장해주는걸까?

이후 게시될 해당강좌의 싱글톤 챕터를 확인하자.

 

 

  • 스프링 컨테이너 생성 코드
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)
  • ApplicationContext 를 스프링 컨테이너라 한다.
  • ApplicationContext 는 인터페이스이다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다. AppConfig 를 사용하는 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다
  • AnnotationConfigApplicationContext 클래스는 ApplicationContext의 구현체이다.
  • 스프링 컨테이너를 구성할 구성 정보(AppConfig.class) 를 매개변수로 전달해 준다.

 

스프링 컨테이너의 구조

AppConfig에 정의된 자바 객체들이 위와 같은 저장소에 저장이 되는데,

이때 빈 이름은 메소드명으로 등록된다.

물론, 이 빈 이름을 메소드명이 아니라 임의의 변경할 수도 있지만, 일반적으로 디폴트값인 메소드명으로 사용한다.

그릭 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.

 

 

하지만 현재 AppConfig의 내용과 스프링컨테이너의 생성 부분만으로는 어떻게 정의만 되어있는 클래스의 메소드들을 호출시켜 빈으로 등록하고, 코드의 흐름상에선 new로 MemberRepository가 여러번 생성되는데 싱글톤이 지켜지는지 감이 오지 않을 것이다.

그런 것들이 가능한 이유는 스프링컨테이너의 구현체들이 팩토리빈(AppConfig)를 읽어와서, 바이트코드를 조작하여 가능한것이다.  그리고 

일단은 무조건 스프링은 저렇게 팩토리빈의 원칙만 잘 지켜준다면, 싱글톤패턴과 DI를 정확하게 처리해 준다고 생각하자.

 

 

 

 

+ 빈(Bean)이란?
스프링 컨테이너가 생성하고 관리하는 자바 객체의 명칭.

 

+ 팩토리빈이란?

스프링컨테이너에 담길 빈이 정의되어 있는 클래스.

위에 보이는 AppConfig이다.

@Configuration 이란 애노테이션이 붙어 있어야하며, @Configuration이 붙은 클래스 또한 빈으로 등록된다.

그리고 내부에는 @Bean이 붙어 있는 메소드들이 존재하여 해당 메소드에서 리턴하는 객체들이 빈이 된다.

 

참조 : https://blog.naver.com/PostView.naver?blogId=ppuagirls&logNo=221881870678&parentCategoryNo=&categoryNo=51&viewDate=&isShowPopularPosts=true&from=search 

 

[Spring 기초] FactoryBean이란?

- FactoryBean이란? Spring을 사용하면서 new 연산자로 간단히 생성할 수 없는 의존성을 어떻게 주입할 ...

blog.naver.com

 

+ 그리고 참고로 스프링 컨테이너는 두가지가 있다.

하나는 BeanFactory, 나머지 하나가 ApplicationContext 이다.

ApplicationContext의 상위 클래스가 BeanFactory이며

스프링 컨테이너로써의 기본적인 기능을 가지고 있다.

그러나, BeanFactory의 기능 이외에 스프링을 사용하며 필요한 기능 (util적인 기능) 을 추가하여 만든 하위 클래스가 ApplicationContext이고, 앞으로 스프링으로 개발을 하며 대부분 사용할 스프링 컨테이너는 ApplicationContext이므로 일반적으로 ApplicationContext 을 스프링컨테이너라고 말한다.

 

스프링 컨테이너에 등록된 빈 출력

  • 등록된 빈 출력 코드 예제
class ApplicationContextBasicFindTest {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); //memberService 에 할당된 구현체가 MemberServiceImpl 형식이 맞는지
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); //memberService 에 할당된 구현체가 MemberServiceImpl 형식이 맞는지
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByType2() {
        MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); //memberService 에 할당된 구현체가 MemberServiceImpl 형식이 맞는지
    }

    @Test
    @DisplayName("빈 이름으로 조회 X")
    void findBeanByNameX() {
//        ac.getBean("xxx",MemberService.class);
        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () ->ac.getBean("xxx",MemberService.class)); //org.assertj.core.api.Assertions 가 아닌 junit 클래스의 메소드 써야함
    }

	@Test
    @DisplayName("Object 타입으로 꺼냄")
    void findBeanByObject() {
//        Object bean = ac.getBean(Object.class); //빈으로 등록된 객체들도 당연히 Object의 하위 클래스들이기 때문에, 뭘 찾아올지 몰라 에러발생
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {

            Object bean = ac.getBean(key);
            System.out.println("key = " + key + " , " +"bean = " + bean);
        }
    }
    
    @Test
    @DisplayName("스프링 컨테이너내에 애플리케이션 빈 출력")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //빈에 정의된 이름(key)들 가져옴
        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("beanName = " + beanDefinitionName + " object = " + bean);
            }

        }
    }
}

 

  • ac.getBean("memberService", MemberService.class);

    "memberService" 라는 key값을 가진 MemberService 데이터 타입 (인터페이스 참조타입)의 객체를 꺼냄.

  • ac.getBean(MemberService.class);

    MemberService이라는 타입으로만 꺼냄.

  • ac.getBean(MemberServiceImpl.class);

    구현체 타입으로 꺼냄.

  • ac.getBeansOfType(Object.class);

    부모객체 타입으로 꺼내면 하위객체들은 다 따라나와 꺼내진다.

  • ac.getBeanDefinitionNames();

    빈으로 등록된 객체들의 이름들을 가져 옴

  • 스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.
    - ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
    - ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

빈 꺼낼때 주의사항

타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.

BeanFactory와 ApplicationContext 그리고 AnnotationConfigApplicationContext

BeanFactory가 최상위 인터페이스이다.

 

BeanFactory

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

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 수많은 부가 기능을 제공한다.

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

    예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력

  • 환경변수

    로컬, 개발, 운영등을 구분해서 처리

  • 애플리케이션 이벤트

    이벤트를 발행하고 구독하는 모델을 편리하게 지원

  • 편리한 리소스 조회

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


정리

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


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

스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.

 

  • 애노테이션 기반 자바 코드 설정 사용
    - new AnnotationConfigApplicationContext(AppConfig.class)
    - AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다
    - 위에서 사용하는 방법


  • XML 설정 사용
    - GenericXmlApplicationContext 를 사용하면서 xml 설정 파일을 넘기면 된다.
    - 최근에는 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는다


    - XmlAppConfig (xml 파일) 을 사용하여 스프링 컨테이너를 만드는 방법
package hello.core.xml;

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

public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        //classpath인 resources 하위에서 appConfig.xml 파일을 찾아서 ApplicationContext로 만들어줌

        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.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>

java 코드를 제외한 것들은 모두 resources 하위에 둔다.

 

 

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

BeanDefinition은 인터페이스이다.

빈의 정의 내용들이 기록되 있는 정의서 같은것이다.

어디서 만들어졌는지, 타입이 싱글톤인지, 빈을 만들기 위한 여러 정보들이 있다.

그리고 스프링에서 Annotation 방식의 빈등록 방법이라던가,

xml 방식의 빈등록 방법이라던가, 이처럼 다양한 방법을 지원할 수 있는 이유는

BeanDefinition 덕분이다.

 

애노테이션 방식 (@Bean을 쓰는 자바 코드 방식) 으로 스프링 컨테이너를 만드는

AnnotationConfigApplicationContext 과

xml 파일을 빈 설정 정보로 가지는 

GenericXmlApplicationContext 객체 또한 모두 내부적으로 reader 라는 객체변수를 가지고 있다.

그리고 그 reader 라는 객체가 매개변수로 받은 자바파일 혹은 xml파일을 각자의 방법으로 읽어들여 

결국엔 어떤 방식의 빈등록 방법이든 BeanDefinition 이라는 타입을 가지는 객체 (구현체) 를 생성하는것이다.

 

@Bean , 당 각각 하나씩 메타 정보가 생성된다

 

+ 개발자 나름의 빈 정의 문서든, 파일이든 만들어서 Reader를 만들고 결국 BeanDefinition만 만들면

스프링 컨테이너에 빈으로 등록이 가능하다는 말이다.

 

 

BeanDefinition 정보

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