Spring DI, IOC, Bean

Red Culture·2021년 7월 30일
0

DI란?

Dependency Injection이라고 하는 의존성 주입이라고 한다. 의존이란 한 클래스가 다른 클래스의 메서드를 실행할 때 의존이라고 한다. 예를들어 자동차가 본체, 엔진, 타이어 객체가 있어야 기능을 할 수 있고 본체와 엔진, 타이어의 속성과 메서드가 있어야 동작할 수 있기 때문에 의존 관계라고 할 수 있다.
어떤 객체(A 객체)가 사용하는 의존 객체(B, C 객체)를 직접 만들어서 사용하는 것이 아니라(new B(), new C()) 주입 받아서 사용하는 방법이다.

IOC란?

의존 관계에 있는 클래스를 직접 new를 사용하여 객체를 주입하는 것이 아니라 생성자를 사용하여 주입받는 것을 Inversion of Control (제어의 역전)이라고 한다. 즉, 메서드나 객체의 호출 작업을 사용하는 쪽(개발자)에서 결정하는 것이 아니라 외부에서 결정되는 것을 의미한다. 객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다.

  • 기존 (개발자가 직접 new를 써서 의존성을 만든다.)
    : 객체 생성 -> 클래스 내부에서 의존성 객체 생성 -> 의존성 객체 메서드 호출
  • 스프링 (외부에서 객체를 생성해서 생성한 객체를 주입한다.)
    : 객체 생성 -> 의존성 객체 주입 (사용자가 컨트롤해서 만드는 것이 아니라 스프링에게 위임하여 스프링이 만들어놓은 객체를 주입) -> 의존성 객체 메서드 호출

스프링 IOC 컨테이너

Bean 설정 소스로부터 Bean 정의를 읽어들이고, Bean을 구성하고 제공한다. Bean들의 의존 관계를 설정해주고 가장 중요한 인터페이스는 BeanFactory, ApplicationContext이다.

  • BeanFactory: 스프링 Bean 객체를 생성하고 관리하는 스프링 컨테이너의 최상위 인터페이스이다. BeanFactory 컨테이너는 구동될 때 Bean 객체를 생성하는 것이 아니라 클라이언트의 요청이 있을 때 객체를 생성한다.
  • ApplicationContext: BeanFactory를 상속받은 인터페이스이며, 구동되는 시점에 등록된 Bean 객체들을 스캔하여 객체화한다. 스프링 컨테이너라고 한다. 스프링 컨네이너는 @Configuration이 붙은 클래스를 구성 정보로 사용한다. 여기서 @Bean으로 적힌 메서드들을 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 bean이라 한다.

스프링이 모든 의존성 객체를 스프링이 실행될 때 다 만들어주고 필요한 곳에 주입시킴으로써 Bean들은 싱글톤 패턴의 특징을 가진다. @Bean이 붙은 메서드 마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
@Bean만 사용해도 스프링 빈으로 등록이 되지만, 싱글톤을 보장하지는 않는다. @Configuration을 사용해야 싱글톤을 보장한다.

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 참조값이 같은 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

	// isSameAs : == , 인스턴스 비교
        // isEqualTo : equals, 값 비교
        assertThat(memberService1).isSameAs(memberService2);
    }

Bean

스프링 IOC 컨테이너가 관리하는 객체이다. 스프링에서 프로젝트가 실행될 때 사용자가 Bean으로 관리하는 객체들의 생성과 소멸에 관련된 작업을 자동적으로 수행해주는데 객체가 생성되는 곳을 Bean 컨테이너라고 부른다. 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료되기 때문에 초기화 작업은 의존관계 주입이 모두 완료되고 난 이후에 호출해야 한다.

스프링 IOC 컨테이너에 등록된 Bean들은 의존성 관리가 수월해지고 싱글톤의 형태이다. 스프링 bean은 @Bean이 붙은 메서드의 명을 스프링 bean의 이름으로 사용한다. (@Bean(name="xxx") 로 bean 이름을 직접 부여할 수 있다.)
스프링 bean은 getBean() 메서드를 사용해서 찾을 수 있다. bean의 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면 다른 bean이 무시되거나 기존 bean을 덮어버리거나 설정에 따라 오류가 발생할 수 있다.
(하나의 Bean 정의에 대해서 컨테이너 내에 단 하나의 객체만 존재한다. 스프링 빈은 항상 무상태로 설계하자.)

class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        // getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            // getBean() : 빈 이름으로 빈 객체를 조회한다.
            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);

            // 스프링 내부에서 등록한 빈이 아니라 애플리케이션에서 개발하기 위해 등록한 빈 혹은 외부 라이브러리
            // 스프링이 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
            // ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
            // ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

}

스프링 Bean 조회하기

  • 기본 조회하기
class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        // System.out.println("memberService = " + memberService);
        // System.out.println("memberService.getClass() = " + memberService.getClass());
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

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

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

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        // MemberService xxxx = ac.getBean("xxxx", MemberService.class);
        // ac.getBean 실행 시 NoSuchBeanDefinitionException 예외 실행
        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxx", MemberService.class));
    }
}
  • 동일한 타입이 둘 이상일 때 조회하기
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();
        }
    }
}
  • 상속관계일 때 조회하기
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();
        }
    }


}

스프링에서 의존성을 주입받는 방법

1) 생성자를 통한 의존성 주입
UserController에 UserInfoRepository, OrderRepository 의존성을 생성자를 통해 주입하고 있다.

public UserController(UserInfoRepository userInfoService, OrderRepository orderService) {
		this.userName = userInfoService;
		this.orderNumber = orderService;
	}

2) 필드에서 주입받을 때
@Autowired: 필요한 의존 객체의 “타입"에 해당하는 빈을 찾아 주입한다. (생성자/setter/필드)

@Autowired
private UserInfoRepository userName; 
private OrderRepository orderNumber;
// UserInfoRepository, OrderRepository가 이미 Bean으로 등록된 상태에서, userName, orderNumber에게 의존성을 주입해 달라는 의미이다.
// 필드가 final 변수일 경우 주입받을 수 없다.

3) setter 메서드를 통해 주입받을 때

@Autowired
public void setUserName(UserInfoRepository userName){
	this.userName = userName;
}

@Autowired
public void setOrderNumber(OrderRepository orderNumber) {
	this.orderNumber = orderNumber;
}

3가지 중에서 1번 방식이 스프링 레퍼런스에서 권장하고 있는데 그 이유는 생성자의 인자로 필수 레퍼런스를 넣어줘야 하기 때문에 이것이 없으면 인스턴스를 만들지 못하도록 강제하는 역할을 하기 때문이다.

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

*참고 자료
https://devlog-wjdrbs96.tistory.com/165
https://velog.io/@gillog/Spring-DIDependency-Injection
https://chanhuiseok.github.io/posts/spring-5/
예제로 배우는 스프링 입문(백기선님)
스프링 핵심 원리 - 기본편(김영한님)

profile
자기 개발, 학습 정리를 위한 블로그

0개의 댓글