ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] IoC, DI 그리고 컨테이너
    Backend Dev/Spring Framework 2022. 3. 8. 19:16
    728x90

     

    제어의 역전 IoC(Inversion of Control)

     

     

    기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고 실행하였다. 즉, 클라이언트가 프로그램의 제어 흐름을 조종했다.

     

    AppConfig가 등장한 이후로 클라이언트 구현 객체는 자신의 로직을 실행하는 역할만 담당하고 제어 흐름은 AppConfig가 담당한다.

     


    결국 프로그램 제어 흐름에 대한 권한은 AppConfig가 가지고 있으며, OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모르고 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있다.

     

     

     


    어떤 인터페이스의 구현체가 실행될지도 모른체 OrderServiceImpl은 자신의 로직 "실행"에만 초점을 맞춘다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 게 아닌 외부에서 관리하는 걸 "제어의 역전"이라 한다.


    이를 통해 프레임워크와 라이브러리를 비교했을 때 자신이 작성한 코드를 제어하고, 대신 실행하면 프레임워크이고, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리이다.

     

     


    의존관계 주입 DI(Dependency Injection)

     


    OrderServiceImpl은 인터페이스에만 의존하고 실제 어떤 구현 객체가 사용될지는 모른다. 이 의존관계는 "정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체 의존 관계"로 분리해서 생각한다.

     


    "정적인 클래스 의존관계"

     


    클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있으며 정적인 의존 관계는 앱을 실행하지 않아도 분석이 가능하지만, 이러한 의존관계만으로 실제 어떤 객체가 주입될지 알 수 없는데 이는 동적인 객체 의존 관계로 파악할 수 있다.

     


    "동적인 객체 의존 관계"


    다음은 애플리케이션 런타임에 실제 생성된 객체의 참조가 연결된 의존 관계이다.


    "실행 시점(런타임)"외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 "의존관계 주입"이라 한다.

     


    의존관계 주입을 사용할 시 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다. 이처럼 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 의존관계를 쉽게 변경할 수 있다.

     

     

    IoC컨테이너, DI 컨테이너

     

    AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 하는데, 의존관계 주입에 초점을 맞춰 주로 DI 컨테이너라 불린다.

     

     

     

    이제까지 자바 코드만으로 DIP, OCP와 같은 원칙들을 지키려고 노력하였지만 스프링 프레임워크에서 지원하는 스프링 컨테이너는 이를 더욱 간단하게 처리할 수 있도록 도움을 준다.

     

     

    스프링 컨테이너

     

    먼저 컨테이너는 인스턴스의 생명주기를 관리하고, 생성된 인스턴스들에게 추가적인 기능을 제공하는 역할을 한다.

     

     

    ApplicationContext(인터페이스)를 스프링 컨테이너라 하며, 기존에는 AppConfig를 사용해 직접 객체를 생성하고 DI을 했으나 이제는 스프링 컨테이너를 통해서 사용한다. 이때 애노테이션 기반의 자바 설정 클래스가 사용된다.


    스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용하고, @Bean이 붙은 메서드를 모두 호출해서 반환한 객체를 스프링 컨테이너에 등록한다. 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

     

    package hello.core;
    
    import hello.core.discount.DiscountPolicy;
    import hello.core.discount.RateDiscountPolicy;
    import hello.core.member.MemberRepository;
    import hello.core.member.MemberService;
    import hello.core.member.MemberServiceImpl;
    import hello.core.member.MemoryMemberRepository;
    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 MemberService memberService() {
        	return new MemberServiceImpl(memberRepository());
        }
        @Bean
        public OrderService orderService() {
            return new OrderServiceImpl(
            memberRepository(),
            discountPolicy());
        }
        
        @Bean
        public MemberRepository memberRepository() {
        	return new MemoryMemberRepository();
        }
        @Bean
        public DiscountPolicy discountPolicy() {
        	return new RateDiscountPolicy();
        }
    }


    @Bean이 붙은 메서드의 이름은 스프링 빈의 name으로 사용된다. 이전에는 필요한 객체를 AppConfig를 사용해서 직접 조회했으나 스프링에서는 스프링 컨테이너의 getBean() 메서드를 사용해 찾을 수 있다.

     

    기존에는 개발자가 직접 자바 코드로 모든 것을 했다면 이제 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아 사용하도록 변경되었다.

     


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

     

    위와 같이 애노테이션 기반의 자바 설정 클래스로 컨테이너를 생성할 수도 있고, XML 기반으로 만들 수도 있으나 AppConfig 클래스 파일에서는 애노테이션 기반이니 이를 기준으로 생성 과정을 알아보려 한다.

     

    //스프링 컨테이너 생성
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

     

     

    1. 스프링 컨테이너 생성


    new AnnotationConfigApplicationContext(AppConfig.class)로 애노테이션 기반 자바 설정의 컨테이너가 생성되고, 구성 정보를 인자에 지정해주어야 한다.

     


    2. 스프링 빈 등록

     

    스프링 컨테이너는 인자로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록하고, 빈 이름은 메서드 이름을 사용한다. 또한, @Bean(name="memberService") 처럼 빈 이름을 직접 부여할 수 있다. 이때 빈 이름은 항상 달라야한다.

     


    3. 스프링 빈 의존관계 설정 "준비" 및 "완료"

     

    스프링 컨테이너는 설정 정보를 참고해 생성된 빈 객체 간 의존관계를 주입한다.

     

     

    스프링은 이처럼 빈을 등록하고 의존관계를 주입하는 단계가 나뉘어져 있다. 이는 AppConfig로 순수 DI 컨테이너를 생성해 객체를 생성하며 연결해주는 자바 코드 호출과 비슷한 거 같지만 다음 포스팅에 설명하게 될 싱글톤 컨테이너를 통해 스프링 컨테이너를 사용했을 때와의 차이를 알 수 있다.

     


    스프링 빈 조회 

     

    스프링 컨테이너가 생성되어 구성 영역에 있는 설정을 기반으로 빈이 등록되고, 의존 관계까지 설정된 이후 본 컨테이너에 있는 데이터(빈)를 조회하는 경우는 다음 세 가지로 나뉜다. 참고로 스프링 컨테이너의 getBean()은 빈 객체를 조회할 때 사용되는 메소드이며 getBeanDefinitionNames()는 스프링에 등록된 모든 빈 이름을 조회한다.

     

    기본 

     

    - 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
    - ac.getBean(빈 이름, 타입), ac.getBean(타입)
    - 조회 대상 스프링 빈이 없으면 NoSuchBeanDefinitionException 예외가 발생

     

    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("빈 이름으로 조회X")
        void findBeanByNameX() {
            Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxxx", MemberService.class));
        }
    }



    동일한 타입 두 개 이상


    - 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생하는데, 이때 빈 이름을 지정
    - ac.getBeansOfType()을 사용 시 해당 타입의 모든 빈 조회 가능

     

    class ApplicationContextSameBeanFindTest {
        AnnotationConfigApplicationContext ac = new
        AnnotationConfigApplicationContext(SameBeanConfig.class);
        
        @Test
        @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
        void findBeanByTypeDuplicate() {
            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();
            }
        }
    }

     

    임의적으로 SameBeanConfig라는 자바 설정 정보를 작성해 동일한 타입이 복수 개인 경우 예외가 발생하는 것을 확인할 수 있다.

     


    상속관계


    - 부모 타입으로 조회하면, 자식 타입도 함께 조회
    - 모든 자바 객체의 최고 부모인 "Object" 타입으로 조회시 모든 스프링 빈을 조회하게 된다.

     

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

     

    TestConfig라는 설정 정보에 대한 클래스를 만들어 스프링 컨테이너 생성시 해당 설정 정보가 적용되고 자식 타입 조회 여부를 확인하기 위해 따로 작성한 것이다. 

     


    BeanFactory와 ApplicationContext


    BeanFactory는 스프링 컨테이너의 최상위 인터페이스이며, 스프링 빈을 관리하고 조회하는 역할을 담당한다. 직접 사용할 일은 거의 없으며 부가기능이 포함된 ApplicationContext를 사용하지만, 둘다 스프링 컨테이너라 불린다.

    ApplicationContext의 경우 BaenFactory 기능을 모두 상속받아서 제공한다. 앱을 개발할 때는 빈을 관리하고 조회하는 기능뿐만 아니라 부가적인 기능이 필요한데 이 기능이 있다는 것이 BeanFactory와의 차이이다. 다음은 부가적인 기능에 어떤 것들이 있는지를 보여준다. 

    메시지소스를 활용한 국제화 기능(MessageSource)
     - 나라에 따른 언어로 출력

     

    환경변수(EnvironmentCapable)
     - 로컬, 개발, 운영등을 구분해서 처리

    애플리케이션 이벤트(ApplicationEventPublisher)
     - 이벤트를 발행하고 구독하는 모델을 편리하게 지원

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

     

     

     

    본 내용은 인프런-스프링 핵심원리 기본편 김영한님의 강의를 참고하였습니다.

     

     

    728x90

    댓글

Designed by Tistory.