728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저번 시간에 스프링 컨테이너를 만들어보았다.

 

스프링 컨테이너의 생성 과정에 대해 알아보자.

저번 시간에는 구성 정보로 AppConfig.class 를 넘겨주었다.

 

그러면 이 class 정보로 스프링 빈들을 등록한다.

AppConfig가 이렇게 구성이 되어 있었으니

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


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

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

스프링 빈 저장소에는

빈 이름 빈 객체
customerService CustomerServiceImpl....
orderService OrderServiceImpl....
customerRepository MemoryCustomerRepository....
discountPolicy RateDiscountPolicy....

로 스프링 빈이 등록이 된다.

빈 이름은 직접 등록할 수 있으며, 빈 이름끼리 겹치면 안된다.

 

스프링 빈들을 등록하고 나면 생성자들을 보고 의존관계를 파악한다.

이렇게 의존관계를 주입할 때 단순히 자바코드를 호출하는 것이 아닌 싱글콘으로 주입을 한다.

 

그러면 이렇게 만든 스프링 빈들을 조회해보자.

 

테스트에 패키지로 beanfind 패키지를 만들고

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 applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

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

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

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition beanDefinition = applicationContext.getBeanDefinition(beanDefinitionName);

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

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findNotApplicationBean(){
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition beanDefinition = applicationContext.getBeanDefinition(beanDefinitionName);

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

하나씩 동작시켜보면 등록된 빈들이 출력되는 것을 볼 수 있다.

getBeanDefinitionNames()를 사용하면 스프링에 등록된 모든 빈 이름을 조회한다. 문자열 배열로 반환이 된다.

getBean(String)은 빈 이름으로 빈 객체를 조회한다.

 

이 getBean이 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 방법인데

getBean(빈 이름, 타입)

getBean(타입)

이렇게 2가지 방법이 있다.

 

빈 이름을 조회하는 예제를 만들어보자.

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
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;

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름, 타입으로 조회")
    void findBeanByName(){
        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("타입으로만 조회")
    void findBeanByType(){
        CustomerService customerService = applicationContext.getBean(CustomerService.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2(){
        CustomerServiceImpl customerService = applicationContext.getBean("customerService", CustomerServiceImpl.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("빈 이름으로 조회 실패")
    void findBeanByNameFail(){
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () -> applicationContext.getBean("abcd", CustomerServiceImpl.class));
    }
}

이렇게 조회할 수 있고, 보통은 interface를 조회를 하는데 구체 타입으로 조회하면 변경시 유연성이 떨어지기 때문이다.

 

위의 2가지 방법을 이용해 조회할 수 있도록 하자.

 

만약 동일한 타입이 둘 이상이라면 어떻게 될까?

package hello.core.beanfind;

import hello.core.customer.CustomerRepository;
import hello.core.customer.MemoryCustomerRepository;
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 org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

public class ApplicationContextSameBeanFindTest {

    @Configuration
    static class SameBeanConfig{

        @Bean
        public CustomerRepository customerRepository1(){
            return new MemoryCustomerRepository();
        }

        @Bean
        public CustomerRepository customerRepository2(){
            return new MemoryCustomerRepository();
        }
    }

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 오류가 발생한다.")
    void findBeanByTypeDuplicate(){
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () -> applicationContext.getBean(CustomerRepository.class));
    }

    @Test
    @DisplayName("그렇기 때문에 같은 타입이 둘 이상 있으면, 빈 이름을 지정해야 한다.")
    void findBeanByName(){
        CustomerRepository customerRepository = applicationContext.getBean("customerRepository1", CustomerRepository.class);

        Assertions.assertInstanceOf(CustomerRepository.class, customerRepository);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType(){
        Map<String, CustomerRepository> beansOfType = applicationContext.getBeansOfType(CustomerRepository.class);
        for(String key : beansOfType.keySet()){
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
        }

        System.out.println("beansOfType = " + beansOfType);
    }
}

당연히 같은 타입이 둘 이상 있으면, 오류가 발생하고 이름을 지정해주어야 한다.

만약 해당 타입에 있는 모든 빈을 조회하려면 getBeansOfType(타입)을 사용해야 한다.

 

스프링 빈을 조회할 때, 부모 타입으로 조회하면 해당 타입의 자식 타입까지 모두 조회한다.

package hello.core.beanfind;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import org.junit.jupiter.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;

public class ApplicationContextExtendsFindTest {

    @Configuration
    static class TestConfig{
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestConfig.class);

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

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

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType(){
        Map<String, DiscountPolicy> beansOfType = applicationContext.getBeansOfType(DiscountPolicy.class);
        for(String key : beansOfType.keySet()){
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType(){
        Map<String, Object> beansOfType = applicationContext.getBeansOfType(Object.class);
        for(String key : beansOfType.keySet()){
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }
}

그렇기에 자식 타입이 둘 이상이라면 빈 이름을 사용해서 조회해야 한다.

 

스프링 컨테이너는 자바 코드 말고도 다양한 형식의 설정 정보를 받아드릴 수 있게 설계되어 있다.

지금까지 사용한 방법이 가장 왼쪽의 자바 코드를 사용하는 방법이었고 이외에도 많이 사용하지는 않지만 XML을 사용하는 방법 등도 있다.

 

GenericXmlApplicationContext를 사용하면서 parameter로 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 = "customerService" class="hello.core.customer.CustomerServiceImpl">
        <constructor-arg name="customerRepository" ref="customerRepository"/>
    </bean>
    
    <bean id = "customerRepository" class="hello.core.customer.MemoryCustomerRepository"/>
    
    <bean id = "orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="customerRepository" ref="customerRepository"/>
        <constructor-arg name="discountPolicy" ref="discountPolicy"/>
    </bean>
    
    <bean id="discountPolicy" class="hello.core.Discount.RateDiscountPolicy"/>
</beans>

보면 AppConfig 클래스와 비슷한 것을 볼 수 있다.

 

이 xml 파일을

package hello.core.xml;

import hello.core.customer.CustomerService;
import org.junit.jupiter.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 applicationContext = new GenericXmlApplicationContext("appConfig.xml");

        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Assertions.assertInstanceOf(CustomerService.class, customerService);
    }
}

이렇게 GenericXmlApplicationContext()로 넘겨주면 저번과 같이 동일하게 사용할 수 있다.

 

이렇게 다양한 형식을 지원하는 방법은 BeanDefinition 인터페이스를 사용하기 때문인데

xml을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만든다.

스프링 컨테이너는 어떻게 읽어왔는지 몰라도 BeanDefinition만 알면 된다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03

+ Recent posts