[Spring] Chapter 4. 스프링 컨테이너와 스프링 빈

joyful·2021년 7월 9일
0

Java/Spring

목록 보기
8/28
post-thumbnail

들어가기 앞서

이 글은 김영한 님의 스프링 핵심 원리 - 기본편(https://www.inflearn.com/스프링-핵심-원리-기본편/dashboard)을 수강하며 학습한 내용을 정리한 글입니다. 모든 출처는 해당 강의에 있습니다.


📖 스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext = new
        AnnotationConfigApplicationContext(AppConfig.class);
  • 스프링 컨테이너 → 인터페이스
    • BeanFactory
      • 최상위 인터페이스
      • 직접 사용하는 경우 거의 x
    • ApplicationContext
      • AnnotationConfigApplicationContext → 구현체
  • 생성 방법
    • XML 기반 생성
    • 어노테이션 기반 생성 → AppConfig

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

1. 스프링 컨테이너 생성

  • new AnnotationConfigApplicationContext(AppConfig.class)
  • 스프링 컨테이너 생성 시 구성 정보(AppConfig.class) 지정 필요

2. 스프링 빈 등록

  • 파라미터로 넘어온 설정 클래스 정보를 사용하여 스프링 빈 등록

    📝 빈 이름

    • 메서드 이름 사용
    • 직접 부여 가능 ex) @Bean(name="memberService2")
    • 주의 사항
      • 빈 이름은 항상 다른 이름을 부여해야 함
      • 같은 이름 부여 시 문제점
        • 다른 빈 무시
        • 기존 빈 덮어쓰기
        • 설정에 따라 오류 발생

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


4. 스프링 빈 의존관계 설정 - 완료

  • 설정 정보를 참고하여 의존관계 주입(DI)

💡 스프링은 빈을 생성하고 의존관계를 주입하는 단계가 나누어져 있으나, 자바 코드로 스프링 빈을 등록하면 생성자 호출과 의존관계 주입이 한 번에 처리 된다.



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

[src/test/java/hello/core/bean/find/ApplicationContextInfoTest.java]

package hello.core.bean.find;

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;

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

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

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

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + ", object = " + bean);
            }
        }
    }
}

  • 모든 빈 출력

    • 스프링에 등록된 모든 빈 정보 출력
    • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름 조회
    • ac.getBean() : 빈 이름으로 빈 객체(인스턴스) 조회
  • 애플리케이션 빈 출력

    • 스프링 내부 사용 빈 제외한 사용자가 등록한 빈만 출력
    • getRole() : 빈 구분
      • ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
      • ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈


📖 스프링 빈 조회 - 기본

  • 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법은 다음과 같다.

    • ac.getBean(빈이름, 타입)
    • ac.getBean(타입)
  • 조회 대상 스프링 빈이 존재하지 않으면 예외가 발생한다.
    NoSuchBeanDefinitionException: No bean named 'xxxxx' available

✅ 예제 코드

[src/test/java/hello/core/bean/find/ApplicationContextBasicFindTest.java]

package hello.core.bean.find;

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.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

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() {
        MemberService 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));
    }
}

💡 구체 타입으로 조회하면 변경 시 유연성이 떨어짐



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

  • 타입으로 조회 시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생
    → 빈 이름 지정 필요
  • ac.getBeansOfType() : 해당 타입의 모든 빈 조회

✅ 예제 코드

[src/test/java/hello/core/bean/find/ApplicationContextSameBasicFindTest.java]

package hello.core.bean.find;

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 타입으로 조회 → 모든 스프링 빈 조회

✅ 예제 코드

[src/test/java/hello/core/bean/find/ApplicationContextExtendsFindTest.java]

package hello.core.bean.find;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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 ApplcationContextExtendsFindTest {

    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() {
        DiscountPolicy 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")  //스프링 내부 Bean까지 조회
    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();
        }
    }
}


📖 BeanFactory와 ApplicationContext

✅ BeanFactory

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

✅ ApplicationContext

스프링 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진

  • BeanFactory 기능을 모두 상속 받아 제공
    → 상속된 Context 컴포넌트들에 접근 가능
  • Resource를 직접 가져올 수 있음
  • 이벤트를 직접 발행 가능하며 이벤트는 context에 등록된 Listener에 작용
  • 리플렉션과 유사하게 명시된 이름 주로 사용

    💡 리플렉션

    • 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
    • 컴파일한 클래스 정보를 활용해 동적으로 프로그래밍이 가능하도록 지원하는 API
  • 싱글톤 레지스트리 : 싱글톤 저장 및 관리

    💡 싱글톤

    소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제 생성 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다.


📝 ApplicationContext가 제공하는 부가기능

  • 메시지소스를 활용한 국제화 기능
    ex) 한국에서 접속 시 한국어로, 영어권에서 접속 시 영어로 출력

  • 환경변수 : 로컬, 개발, 운영 등을 구분해서 처리

  • 애플리케이션 이벤트 : 이벤트 발행 및 구독 모델 편리하게 지원

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



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

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게끔 유연하게 설계되어 있음
    ex) 자바 코드, XML, Groovy 등


✅ 어노테이션 기반 자바 코드 설정 사용

  • 현재 많이 사용하는 방식
  • new AnnotationConfigApplicationContext(AppConfig.class)
  • AnnotationConfigApplicationContext 사용하여 자바 코드로 작성된 설정 정보 전달

✅ XML 설정 사용

  • 과거에 사용되었던 방식
  • GenericXmlApplicationContext 사용하여 xml 설정 파일 전달

📝 xmlAppConfig 사용 자바 코드

[src/test/java/hello/core/xml/XmlAppContext.java]

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.assertThat;

public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new
                GenericXmlApplicationContext("appConfig.xml");

        MemberService memberService =
                ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

📝 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>


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

  • Bean 정보를 추상화 시킨 것 → 인터페이스
  • 역할과 구현을 개념적으로 나눈 것
  • 빈 설정 메타 정보
    • @Bean / <Bean> 당 하나씩 메타 정보 생성
    • 메타 정보 기반으로 스프링 빈 생성


📝 심화) 코드 레벨로 파악하기


✅ BeanDefinition 살펴보기

📝 BeanDefinition 정보

항목설명
BeanClassName◾ 생성할 빈의 클래스 명
◾ 팩토리 역할의 빈 사용 시 존재 x
factoryBeanName팩토리 역할의 빈 사용하는 경우 이름
ex) appConfig
factoryMethodName빈을 생성할 팩토리 메서드 지정
ex) memberService
Scope싱글톤(기본값)
lazyInit실제 빈을 사용할 때 까지 최대한 빈 생성을 지연처리 하는지 여부
InitMethodName빈 생성 및 의존관계 적용 후 호출되는 초기화 메서드 명
DestroyMethodName빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
Constructor arguments,
Properties
◾ 의존관계 주입에서 사용
◾ 팩토리 역할의 빈 사용 시 존재 x

📝 예시

[src/test/java/hello/core/bean/definition/BeanDefinitionTest.java]

package hello.core.bean.definition;

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;
import org.springframework.context.support.GenericXmlApplicationContext;

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

💡 BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 있으나, 실무에서는 직접 정의하거나 사용할 일이 거의 없다.

profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글