BACK/SPRING

[Spring] DI / IoC, 컨테이너, 의존성 주입 방법

연듀 2023. 1. 9. 14:07

 

DI (Dependency Injection)

 

DI(Dependency Injection)란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로,
객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.

DI(의존성 주입)를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.

 

 

의존 관계

 

먼저 의존 관계가 무엇인지 알아보자. 

 UML 모델에서는 두 클래스의 의존관계를 다음과 같이 점선으로 된 화살표로 표현한다.

 

 

A가 B에 의존하고 있음을 나타낸다.

 

 

B의 기능이 추가하거나 변경되면  A에 영향을 미친다.

의존 관계에는 방향성이 있다. 

A가 B에 의존하지만, 반대로 B는 A에 의존하지 않는다. 

 

 

 

의존성 주입

첫번째 방법은 A객체가 B와 C객체를 New 생성자를 통해서 직접 생성하는 방법이고,

두번째 방법은 외부에서 생성된 객체를 사용하는 방식으로 이것이 의존성 주입이다.

A객체에서 B,C 객체를 사용(의존)할 때 A객체에서 직접 생성하는 것이 아니라 외부(IoC 컨테이너)에서 생성된 B, C 객체를

주입시켜 setter혹은 생성자를 통해 사용하는 것이다. 

 

 

 

스프링에서는 외부의 대상이 IoC 컨테이너가 되어, 빈을 알아서 주입해준다. 

 

 

Ioc(Inversion of Control)

 

IoC란 '제어의 역전'이라는 의미로, 프로그램의 제어 흐름을 개발자가 직접 제어하는 것이 아니라 외부에서 관리되는 것을 의미한다. 프로그램의 제어 흐름에 대한 권한은 모두 스프링 컨테이너가 가지고 있다. 

코드의 최종 호출은 개발자가 제어하는 것이 아닌 프레임워크의 내부에서 결정된 대로 이루어진다.

객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있다. 

 

기존에는 다음과 같은 순서로 객체가 만들어지고 실행되었다.

 

1. 객체 생성

2. 의존성 객체 생성(클래스 내부에서 생성)

3. 의존성 객체 메소드 호출

 

하지만, 스프링에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.

 

1. 객체 생성

2. 의존성 객체 주입

  (스스로가 만드는 것이 아니라 제어권을 스프링에게 위임하여 스프링이 만들어놓은 객체를 주입한다.)

3. 의존성 객체 메소드 호출 

 

스프링이 모든 의존성 객체를 스프링이 실행될 때 다 만들어주고 필요한 곳에 주입시켜줌으로써 빈들은 싱글턴 패턴의 특징을 가지며, 제어의 흐름을 사용자가 컨트롤하는 것이 아니라 스프링에게 맡겨 작업을 처리하게 된다. 

 

 

스프링 컨테이너

 

스프링에서는 IoC를 담당하는 컨테이너를 빈 팩토리, DI 컨테이너, 애플리케이션 컨텍스트라고 부른다.

오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 보면, 컨테이너를 빈 팩토리 또는 DI 컨테이너라고 부른다.

그러나 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하는데, DI를 위한 빈 팩토리에 여러가지 기능을 추가한 것을 애플리케이션 컨텍스트라고 한다. 

 

빈 팩토리와 애플리케이션 컨텍스트 관계는 다음과 같다.

빈 팩토리는 스프링 컨테이너의 최상위 인터페이스로, 스프링 빈을 관리하고 조회하는 역할을 담당한다.

애플리케이션 컨텍스트는 빈 팩토리 기능을 모두 상속받아서 제공한다. 빈 팩토리 인터페이스의 서브 인터페이스들을 상속받아 빈 팩토리에게 없는 추가 기능까지도 제공한다. 

 

 

스프링 컨테이너는 @Configuration 이 붙은 클래스를 설정 정보로 사용한다.

여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 

이렇게 스프링 컨테이너에 등록된 객체를 Bean 이라고 부른다. 

기존에는 개발자가 직접 모두 자바 코드를 짰다면, 스프링 컨테이너에 객체를 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용할 수 있는 것이다. 

 

 

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

@Configuration // 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야 함 
public class AppConfig {

        @Bean // 클래스를 빈으로 등록할 때 사용 
        public MemberService memberService() {
                return new MemberServiceImpl(memberRepository());
        }
}

 

 

의존성 주입 3가지 방식

 

3가지 방식에는 빈을 주입하는 순서가 다르다. 

 

1. 필드 주입(Field Injection) 

@Autowired
private BService Bservice;

 

1. 주입받은 빈의 생성자를 호출하여 빈을 찾거나 빈 팩토리에 등록

2. 생성자 인자에 사용하는 빈을 찾거나 생성

3. 필드에 주입

 

2. 수정자 주입, 세터 주입(Setter based Injection)

private BService bService;
 
    @Autowired
    public void setBService(BService bService) {
        this.bService = bService;
    }

 

1. 주입받은 빈의 생성자를 호출하여 빈을 찾거나 빈 팩토리에 등록

2. 생성자 인자에 사용하는 빈을 찾거나 생성

3. 주입하려는 빈 객체의 수정자를 호출하여 주입

 

 

위의 두가지 방식은 런타임에서 의존성을 주입하기 때문에 의존성을 주입하지 않아도 객체가 생성될 수 있다.

 

 

 

3. 생성자 주입(Constructor based Injection)

@Controller
public class BController {
 
      private final BService bService;
 
      @Autowired
      public BController(BService bService) {
          this.bService = bService;
      }
}

 

1. 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 생성

2. 찾은 인자 빈으로 주입하려는 생성자를 호출

 

 

스프링에서는 현재 생성자 주입 방식을 권고하고 있다.

그 이유는 아래와 같다.

 

 

1) 필드에 final 키워드 사용이 가능하다.

필드 주입시 final 키워드를 사용할 수 없지만, 생성자 주입시 final 키워드를 사용해 불변하게 사용할 수 있다. 

 

 

2) 순환 참조를 방지할 수 있다.

어떤 클래스가 A를 참조하고, B가 다시 A를 참조하는 경우를 순환 참조라고 말한다.

필드 주입, 세터 주입은 빈이 생성된 후 참조를 하기에 애플리케이션은 아무런 오류나 경고 없이 구동 되어

실제 코드가 호출되기 전까지는 문제를 알 수 없다.

반면, 생성자 주입을 통해 실행하면 BeanCurrentlyInCreationException이 발생한다.

순환 참조 뿐만 아니라 의존 관계에 내용을 외부로 노출시킴으로써 애플리케이션을 실행하는 시점에 오류를 체크할 수 있다. 

 

 

3. 테스트 작성의 편의성

테스트 작성을 하는 클래스가 필드 주입을 사용하면 외부에서 빈을 주입해 줄 수 없다. 그렇기에 해당 필드는 null이 된다. 

따라서 스프링 및 모든 설정을 가져와서 실행해야 테스트를 할 수 있다.

생성자 주입의 경우 테스트 코드 자체에서 필요한 의존 관계만 만들어서 테스트가 가능하다. 

 

 

https://steady-coding.tistory.com/600

https://minsoolog.tistory.com/52

https://velog.io/@gillog/Spring-DIDependency-Injection