본문 바로가기

Backend/Spring

스프링 컨테이너와 스프링 빈

스프링 컨테이너 생성

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

여기서 ApplicationContext를 스프링 컨테이너라고 한다.

ApplicationContext는 인터페이스이다.

 

그리고 스프링 컨테이너는 XML 기반으로 생성할 수도 있고, 애노테이션 기반 자바 설정 클래스로 만들 수도 있다.

위 방식은 애노테이션 기반 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.

 

스프링 컨테이너 생성 과정

1. 스프링 컨테이너 생성

스프링 컨테이너 생성 시 구성 정보를 지정해주어야 하는데, 여기서는 AppConfig.class로 지정했다.

 

2. 스프링 빈 등록

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

빈 이름은 메서드 이름을 사용한다.

 

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

 

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

스프링 컨테이너는 설정 정보를 참고해서 의존 관계를 주입한다.

 

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

하지만, 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존 관계 주입도 한 번에 처리된다.

자세한 건 이후에 설명하겠다.

 

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

// ApplicationContextInfoTest.java
package ghdtjgus76.core.beanFind;

import ghdtjgus76.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);
        }
    }
    
    @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);
            }
        }
    }
}
// 애플리케이션 빈 출력 결과
name = appConfigobject = ghdtjgus76.core.AppConfig$$SpringCGLIB$$0@319dead1
name = memberServiceobject = ghdtjgus76.core.member.MemberServiceImpl@21526f6c
name = memberRepositoryobject = ghdtjgus76.core.member.MemoryMemberRepository@49f5c307
name = orderServiceobject = ghdtjgus76.core.order.OrderServiceImpl@299266e2
name = discountPolicyobject = ghdtjgus76.core.discount.RateDiscountPolicy@5471388b

모든 빈 출력하기 코드를 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.

ac.getBeanDefinitionNames()는 스프링에 등록된 모든 빈 이름을 조회하는 것이고, ac.getBean()는 빈 이름으로 빈 객체를 조회하는 것이다.

 

애플리케이션 빈 출력하기 코드를 실행하면 스프링이 내부에서 사용하는 빈은 제외하고 내가 등록한 빈만 출력할 수 있다.

스프링이 내부에서 사용하는 빈의 경우, getRole()로 구분할 수 있다.

ROLE_APPLICATION은 일반적으로 사용자가 정의한 빈이고, ROLE_INFRASTRUCTURE는 스프링이 내부에서 사용하는 빈을 나타낸다.

 

스프링 빈 조회 - 기본

구체 타입으로도 빈을 조회할 수는 있지만 유연성이 떨어지기 때문에 인터페이스로 조회하는 것이 조금 더 좋은 방법이다.

// ApplicationContextBasicFindTest.java
package ghdtjgus76.core.beanFind;

import ghdtjgus76.core.AppConfig;
import ghdtjgus76.core.member.MemberService;
import ghdtjgus76.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
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.AssertionsForClassTypes.assertThat;
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() {
    	MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByNameX() {
    	assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxx", MemberService.class));
    }
}

 

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

// ApplicationContextSameBeanFindTest.java
package ghdtjgus76.core.beanFind;

import ghdtjgus76.core.member.MemberRepository;
import ghdtjgus76.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.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 중복 오류가 발생한다.")
    void findByTypeDuplicate() {
    	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 타입으로 조회하면 모든 스프링 빈을 조회하는 것이다.

 

하위 타입으로 빈을 조회하는 것은 좋지 않다.

// ApplicationContextExtendsFindTest
package ghdtjgus76.core.beanFind;

import ghdtjgus76.core.discount.DiscountPolicy;
import ghdtjgus76.core.discount.FixDiscountPolicy;
import ghdtjgus76.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 java.util.Objects;

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

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

    @Test
    @DisplayName("부모 타입으로 조회 시, 자식이 둘 이상 있으면 중복 오류가 발생한다.")
    void findBeanByParentTypeDuplicate() {
        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();
        }
    }
}

 

BeanFactory와 ApplicationContext

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

스프링 빈을 관리하고 조회하는 역할을 담당한다.

그리고 getBean() 메서드를 제공하는데, 우리가 사용했던 대부분의 기능은 BeanFactory가 제공한다고 보면 된다.

 

ApplicationContext는 BeanFactory 기능을 모두 상속 받아서 제공한다.

빈을 관리하고 검색하는 기능은 BeanFactory가 제공하는데, 추가적으로 부가 기능이 들어간 것이다.

BeanFactory를 직접 사용할 일은 없고 부가 기능이 포함된 ApplicationContext를 주로 사용한다.

BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

 

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

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

 

GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다.

// 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="ghdtjgus76.core.member.MemberServiceImpl" >
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

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

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

    <bean id="discountPolicy" class="ghdtjgus76.core.discount.RateDiscountPolicy" />
</beans>
// XmlAppContext.java
package ghdtjgus76.core.xml;

import ghdtjgus76.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.AssertionsForClassTypes.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);
    }
}

 

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

BeanDefinition에도 추상화가 사용되었는데, 역할과 구현을 개념적으로 나누었다.

스프링 컨테이너는 자바 코드인지, XML인지 몰라도 되고, BeanDefinition만 알면 된다.

 

BeanDefinition을 빈 설정 메타 정보라고 한다.

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

스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다.

'Backend > Spring' 카테고리의 다른 글

컴포넌트 스캔  (0) 2023.08.02
싱글톤 컨테이너  (0) 2023.08.02
스프링 예제-2  (0) 2023.08.02
스프링 예제-1  (0) 2023.08.02
객체 지향 설계와 스프링  (0) 2023.08.02