Spring 핵심 원리 - 스프링 컨테이너와 스프링 빈

김태훈·2023년 1월 12일
0

Spring 핵심 원리

목록 보기
11/15

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

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

스프링 컨테이너를 생성할 때에는 구성 정보를 지정해야 한다. = 여기에서는 "AppConfig class를 사용할 것이다."
우리가 앞선 게시글에서 봤듯, 스프링 컨테이너의 생성 코드는 다음과 같다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext : '스프링 컨테이너' 이면서 '인터페이스' 이다. 따라서 다형성을 적용할 수 있겠다.
따라서 이를 구현한 것 중 하나가 AnnotationConfigApplicationContext 인 것이다.

이렇게 되면, 빈 스프링컨테이너가 만들어진다.

스프링 빈 저장소
빈 이름 빈 객체
--
--
--
이 때, parameter로 들어온 AppConfig 구성정보를 토대로 빈 저장소에 Bean을 등록한다.

2. 스프링 빈 등록


Bean 어노테이션이 붙은 친구들을 이제 스프링 빈 저장소에 올린다.
이 때, 빈 이름은 '메소드이름'으로, 객체는 각 메소드 별로 return 되는 객체들이다.

스프링 빈 저장소
빈 이름 빈 객체
memberServiceMemberServiceImpl
orderServiceOrderServiceImpl
memberRepositoryMemoryMemberRepository
discountPolicyRateDiscountPolicy

이 때, 빈 이름은 항상 다른 이름으로 등록해야 함에 유의하자. (물론 메소드명 말고 다르게 Annotation을 이용하여 임의로 이름을 설정할 수 있다.)

3. 스프링 빈 의존관계 설정

등록된 Bean들끼리 의존되는 관계들을 설정한다.
1. memberService -> memberRepository 의존
2. orderService -> memberRepository, discountPolicy 의존
3. MemberRepository -> 의존관계 없음
4. DiscountPolicy -> 의존관계 없음

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

  • Bean 전체 조회

    test 에서 직접 컨테이너에 등록된 Bean을 조회해보자.
package Goat.core.beanFind;

import Goat.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

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

Tip (나머지는 단순한 메소드니까 다른 설명은 굳이 하지 않겠다)
'iter'을 치면 intelliJ에서 뜨는 추천 코드를 눌러서 beanDefinitionNames 객체를 iteration하는 코드를 자동으로 생성해준다.

결과 :

하지만 이 외에도 다른 Bean ( 스프링 내부에서 자동으로 등록한) 것도 같이보이게 된다.
이를 없애기 위해서,

	@Test
    @DisplayName("에플리케이션 빈 출력하기")
    public void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            //직접 내가 Bean으로 등록한 Bean들만 가져오기.
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " +  beanDefinitionName + " object = " + bean);
            }

        }
    }

로 변경하면 되겠다.

Role ROLE_APPLICATION : 직접 등록한 어플리케이션 빈
Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

  • 각 Bean 조회

1. 빈 이름으로 조회

package Goat.core.beanFind;

import Goat.core.AppConfig;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

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

2. 빈 타입으로 조회

-> 타입으로 할 경우 중복 Bean으로 인한 문제가 생기기도 한다.

	@Test
    @DisplayName("빈 타입으로 조회")
    void findBeanByType(){
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

3. 구체 타입으로 조회

-> 이전에는, 각 Bean의 반환타입인 인터페이스로 찾았다면, 메소드 내에 생성된 객체 형식으로 Bean을 조회할 수 있다. 하지만 추천하는 방법은 당연히 아니겠다.

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

4. 빈 이름으로 조회 실패

	@Test
    @DisplayName("빈 이름으로 조회X")
    void findByNameX(){
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,()->ac.getBean("xxxxx",MemberService.class));
    }

5. 같은 타입이 둘 이상이어서 중복된 Bean을 조회할 때

-> AppConfig에서는 같은 타입이 둘 이상인 것이 없으므로, 해당 TestCode에서 직접 Config를 정의하는 클래스를 생성하고 실행해보자.

클래스 내에 stataic 클래스를 선언하는 경우
해당 클래스 내에서만 static클래스를 사용하겠다는 뜻

package Goat.core.beanFind;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 중복 오류 발생")
    void findBeanByTypeDuplicate(){
        MemberRepository bean = ac.getBean(MemberRepository.class);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

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

해당 테스트 코드 실행시,

이런 오류가 뜬다.
이제 이를 막아보자.

	@Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 중복 오류 발생")
    void findBeanByTypeDuplicate(){
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class,()->ac.getBean(MemberRepository.class));
    }

그렇다면, 타입말고도, 빈 이름으로도 지정해주면 된다.

getBean("빈 이름", "타입 형") 이기 때문에 인자를 2개를 주자.

	@Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 존재하면, 빈 이름을 인자로 전달")
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        org.assertj.core.api.Assertions.assertThat(memberRepository).isInstanceOf(MemoryMemberRepository.class);
    }

6. 특정 타입을 모두 조회하기

 	@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));
            org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
            
        }
    }
  • 상속관계에 있는 Bean 조회

    부모 타입을 조회하면 자식 타입도 함께 조회된다.
    따라서, 기본적으로 모든 자바 Class들은 최고 부모인 Object에 상속되어있기 때문에, 'Object' 타입으로 조회하면 모든 Bean들을 조회할 수 있다.
package Goat.core.beanFind;

import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import org.assertj.core.api.Assertions;
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.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextExtendsFindClass {
    AnnotationConfigApplicationContext ac =new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류 발생")
    public void findBeanByParentTypeDuplicate(){
        assertThrows(NoUniqueBeanDefinitionException.class,()->ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 ,빈 이름 지정")
    public void findBeanByParentTypeBeanName(){
        DiscountPolicy rateDiscountPolicy = ac.getBean("RateDiscountPolicy", DiscountPolicy.class);
        Assertions.assertThat(rateDiscountPolicy).isInstanceOf(DiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllBeanByParent(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        Assertions.assertThat(beansOfType.size()).isEqualTo(2);
        for (String s : beansOfType.keySet()) {
            System.out.println("key = " + s + " value = " + beansOfType.get(s));
        }
    }

    @Test
    @DisplayName("Object로 모두 조회")
    void findAllBeanByObject(){
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String s : beansOfType.keySet()) {
            System.out.println("key = " + s + " value = " + beansOfType.get(s));
        }
    }
    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy RateDiscountPolicy(){
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy FixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }
}

2. BeanFactory와 Application Context


다음은 BeanFactory와 ApplicationContext의 상속 관계를 나타낸 그림이다.
즉 BeanFactory(최상위)를 상속받은 ApplicationContext가 있다. 즉, ApplicationContext는 BeanFactory에서 부가기능을 더 첨가했다고 볼 수 있다.

1. BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할
  • getBean()을 제공

2. ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공
  • +a로, BeanFactory의 역할인 스프링 빈을 관리하고 조회하는 것보다 수많은 부가기능이 존재

3. ApplicationContext의 부가 기능

다음은 ApplicationContext가 상속받은 부가 기능들을 나타낸 그림이다.

  • MessageSource
    한국에서 들어오면 한국어로, 영어권에서 들어오면 영국어로 변환해줌
  • EnvironmentCapable
    로컬,개발,운영을 구분해서 처리
  • ApplicationEventPublisher
    이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • ResourceLoader
    파일,클래스패스,외부 등에서 리소스를 편리하게 조회

결론

BeanFactory와 ApplicationContext 모두 스프링 컨테이너이다 !!

3. 다양한 설정 형식 지원 - (자바,XML,Groovy)

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

ApplicationContext에는
AnnotationConfigApplicationContext에서 Appconfig.class 를 사용했는데, 이 때,
AnnotationConfig를 사용하는 ApplicationContext 말고, GenericXml을 사용하는GenericXmlApplicationContext에서 appConfig.xml를 사용하면 xml 문서를 설정정보로 사용할 수 있다.
물론, 우리가 임의의 파일(.xxx) 파일을 사용하는 ApplicationContext를 커스터마이징 할 수 있다.

1. 코드로 구현해보기

1. XmlAppContext 클래스 만들기

test 에서 "xml" 패키지를 만들어 해당 패키지에 XmlAppContext 클래스를 만든다.

package Goat.core.xml;

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

2. xml 파일 만들기(자바 코드가 아니므로 resources)

<?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="Goat.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    <bean id="memberRepository"
          class="hello.core.member.MemoryMemberRepository" />
    <bean id="orderService" class="Goat.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="Goat.core.discount.RateDiscountPolicy" />
</beans>

형식은 아주 비슷하다 !! AppConfig.class와 완전 비슷~

3. 결과

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

1. 스프링이 다양한 설정을 지원할수 있게된 이유는 ?

바로 "BeanDefinition"이라는 추상화가 있기 때문이다. 역할과 구현을 나누라는 김영한님의 말씀 그대로 BeanDefinition도 그러하다.
즉, XML을 읽어서 BeanDefinition을 만들거나 혹은, JAVA 코드를 읽어서 BeanDefinition을 만들거나 하는 것이다. 즉, 스프링 컨테이너 입장에서는 JAVA 인지 XML인지 모르고, 오로지 BeanDefinition만 알면 된다. (여태껏 배웠던 역할과 구현이 완벽히 나뉘어져 있는 예시이다)

여기서 "BeanDefinition"을 빈 설정 메타정보라고 한다. 그래서, 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다. = (스프링 컨테이너는 BeanDefinition<추상화>에만 의존하여 스프링 빈을 생성한다)

2. ApplicationContext (스프링 컨테이너) 의 심도 있는 분석



이렇게 ApplicationContext 안에 존재하는 AnnotationConfigApplicationContext 안에는 readerscanner 가 존재하는데, 이 친구들이 설정정보 (AppConfig.class)를 읽고, BeanDefinition을 만들어낸다.

이런 식으로 XML도, 다른 파일도 설정정보를 읽어서 BeanDefinition을 만들어 낸다.

3. BeanDefinitionTest 코드 작성하기

package Goat.core.beandefinition;

import Goat.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 BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    // GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
    @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);
            }
        }
    }
}

4. 결과

profile
기록하고, 공유합시다

0개의 댓글