3 분 소요

DI(Dependency Injection)

제어 역전의 방법 중 하나로 우리가 사용할 객체를 직접 생성하는 것이 아니라 외부 컨테이너가 만든 객체를 “주입”받아 사용하는 방식을 뜻한다.

DI의 의미

  • 객체 지향에서 의존성은 2가지 타입, 강한 결합 vs 느슨한 결합이 존재한다.
    • 강한 결합이란? 하나의 클래스 내부에 다른 클래스 객체를 필드 변수로 갖는 구조로 서로 뗄 수 없는 상태를 뜻한다. 만약 A⊃B를 A⊃C로 변경한다면 소스코드를 새로 작성해야한다.(하드코딩이 필요)
    • 약한 결합이란? 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는 것을 의미한다. 이렇게 낮아진 결합도는 런타임시 의존관계가 결정되기에 보다 유연한 구조를 갖게된다. (만약 “인터페이스를 통해 넘겨받는 것”이 이해되지 않는다면 객체지향의 다형성에 대해 다시 학습하자)
  • 2가지 방식 중 하나의 방식(구조의 유연성을 위해서는 후자를 더 지향한다.) 통해 객체를 생성하고 이를 사용하기 위한 객체의 의존성 설정을 DI라고 한다.

DI의 3가지 유형

  1. 생성자(생성자 주입)
  2. 필드 객체 선언(필드 주입)
  3. setter(수정자 주입)
    • 자바에서는 3가지 방법으로 DI를 할 수 있다. 3가지 모두 의존성을 주입받는 것에는 문제가 없지만 한가지 방식을 골라야 한다면 단연코 1번! 생성자 방식이다. 2,3 번은 아예 사용하지 않아도 된 정도이다
  • 스프링 공식 문서에서는 생성자 방식을 지향한다. why? 따로 레퍼런스 객체 없이 객체를 초기화할 수 있기 때문이다. 생성자 주입은 생성자 호출 시점에 1회 호출된다. 때문에 주입받은 객체가 변하지 않거나 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다.
  • setter 방식은 객체 생성이후에 의존성이 설정된다. 의존 관계를 주입받지만 주입받는 객체가 변경될 가능성이 있는 경우에 주로 사용한다.(사실 그런 경우는 드물다) @Autowired로 주입할 대상이 없는 경우에는 오류가 발생한다.
  • 필드주입은 원하는 객체 변수에 @Autowired만 붙이면 된다. 코드가 간편해진다. 하지만 해당 객체는 외부에서 내부로 접근할 수가 없게 된다. 특히 테스트 상황에서 해당 객체의 의존성 객체레 대한 설정이 불가해지기에 TDD 측면에서 불리하다.

생성자 방식의 DI를 써야 하는 이유

  1. 객체의 불변성 확보
  2. 테스트 코드의 작성
  3. final 키워드 작성 및 Lombok과의 결합
  4. 스프링에 비침투적인 코드 작성
  5. 순환 참조 에러 방지

객체의 불변성 확보

  • 수정자 메서드로 주입을 한다면 의존성을 갖는 멤버 객체가 불변하지 않고 변경되거나 수정될 가능성이 생기게 된다(같은 타입이지만 내부 내용이 다른 객체로 바뀔 수도 있기 때문.) 이러한 변경 가능성은 개발 및 유지보수시에 하나의 변수가 될 수 있기에 수정자 메서드 방식이 지양되는 것이다.

테스트 코드의 작성

  • 위에서 잠깐 언급한 것처럼 테스트 코드 작성시에 필드 객체 주입은 문제가 될 수 있다. 예를 들어 MVC에서 컨트롤러 객체를 테스트하는데 내부 변수인 서비스 객체가 존재한다면 new로 생성한 컨트롤러 객체에 service 객체도 따로 설정해주어야 하는데 @Autowired로 되어있다면 테스트 코드에서 따로 설정할 방법이 없다.(@Autowired로 되었으니 컨테이너에서 객체를 가져올 테는 온전히 “컨트롤러”만 독립적으로 테스트 한다고 불수 없는 것(즉 단위, slice 테스트 의미에 어긋남). 컨트롤러 상태와 상관없이 컨테이너 문제가 있을 경우 독립된 테스가 아닌 것이다.) 또한 스프링 컨테이너를 거쳐서 작동하기에 그만큼의 리소스가 소모된다.

final 키워드 작성 및 Lombok과의 결합

  • 생성자 주입을 쓰면 필드 객체에 final 사용이 가능해진다.final로 선언된, 의존성을 갖는 필드 변수는 @RequiredArgsConstructor와 함께 사용되어 객체가 생성될 때 무조건 의존성이 자동으로 주입되기에 필수로 주입되어야 하는 객체를 빠짐없이 넣을 수 있게 된다.

스프링에 비침투적인 코드 작성

  • 테스트 코드 언급하며서 설명했듯이 원하는 객체의 작동만 독립적으로 테스트하느데 있어 의존성 주입을 위한 스프링 프레임워크의 역할을 배제할 수 있다.

순환 참조 에러 방지

  • 만약 컨트롤러와 서비스 객체가 서로를 내부 변수로 갖고 있고 주입자로 의존성을 생성한다면? 일단 객체를 생성하는 것까지는 문제가 없다. 의존성 주입은 이후에 일어나니까. 그리고 비즈니스 로직이 돌아갈 때, 하나의 객체가 서로를 계속해서 주입하는, 즉 순환 참조 에러가 발생할 것이다. 생성자 방식으로 만든다면 애초에 컨트롤러 객체를 만들 때부터 무한으로 서로가 서로를 필요로 하기에 컴파일 자체가 안된다. 즉 수정자 방식은 서버가 돌아가 죽기전에 미리 어플리케이션을 구동할 때 구조적 오류를 확인할 수 있다는 점에서 권장되는 것.

※ DI시 발생 가능한 순환참조

  • DI과정에서 발생가능한 StackOverFlowError는 2가지가 있다.
    1. 객체 생성시에 순환참조
    2. 생성된 객체를 가지고 발생하는 비즈니스 로직상의 순환참조 간단히 설명하자면 1은 new A_Service(new B_Service(new A_Service(new B_Service(new A_Service(…..))))로 인한 RuntimeError이고
***************************
APPLICATION FAILED TO START
***************************

Description:
The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  courseServiceImpl defined in file [/User/.../A_Service.class]
↑     ↓
|  studentServiceImpl defined in file [/User/.../B_Service.class]
└─────┘

2는 A→B→A→B→A→B→…..의 무한 호출에서 오는 순환 참조 에러이다. 2번은 호출시에 발생하는 에러로 서버는 잘 돌아가나 작동할 때 문제가 생겨 서버가 죽는 에러이므로 주의해야 한다.

출처:

  • 스프링부트 핵심 가이드(2022)
  • https://mangkyu.tistory.com/125
  • https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

댓글남기기