DI (Dependency Injection, 의존성 주입)

손효재·2022년 1월 10일
0

Spring

목록 보기
3/3

DI란?

Dependency Injection 으로 의존성을 주입한다는 뜻이다.
여기서 말하는 의존이란 “객체간의 의존”으로, 변경에 의해 영향을 받는 관계를 의미한다.

객체를 직접 생성하는 것이 아니라 외부에서 생성한 객체를 동적으로 주입시켜주는 방식이다.
추상화를 해치지 않고 의존성을 인수로 넘겨주는 방법이다.

DI의 장점

DI 장점은 구체적인 구현을 몰라도 추상화된 인터페이스를 가지고 사용할 수 있다”

  1. 의존성이 줄어들면서 변경에 유연해진다.

DI를 사용하지않으면 기존에 의존하던 객체에 변경이 생기면, 해당 객체를 의존하던 모든 객체가 변경되어야 한다.
DI를 통해 의존성을 주입받는다면, 주입받는 대상이 변경 되더라도 내부의 구현 자체를 변경할 일이 줄어든다.

  1. 재사용성이 높아진다.

의존성을 바꿔서 다른 클래스에서도 사용할 수 있으므로 재사용성이 높아진다.

  1. 테스트에 용이해진다.

테스트를 위한 객체를 사용하거나, 객체를 생성하는 비용이 큰데 테스트를 위해 생성한다면 비효율적일 수 있다.

“의존성을 주입할 때, 왜 상속이 아닌 인터페이스여야 하지? 상속한 부모 클래스로 DI를 할 수는 없을까?”

* 주입받는 의존 관계가 인터페이스라면, 객체가 바뀌더라도 모든 코드를 재사용할 수 있다.

→ 인터페이스의 규약에 맞게 정의된 메서드로 인해, 인터페이스에 어떤 구현체가 오더라도 해당 인터페이스의 메서드는 변하지 않아 모든 코드가 재사용가능하다. 하지만, 상속한 클래스는 다른 메서드를 가질 수 있기 때문에, 모든 코드를 재사용할 수 없다.

또한, 상속한 클래스의 세부타입에 의존하기 때문에 OCP 원칙을 지킬 수 없게 된다.

<OCP 원칙 예시>

OCP 원칙의 가장 잘 따르는 예시가 바로 자바의 데이터베이스 인터페이스인 JDBC이다.
만약 사용하고 있는 데이터베이스를 MySQL에서 Oracle로 바꾸고 싶다면, 데이터베이스를 사용하는 곳에서 복잡한 수정없이 그냥 connection 객체 부분만 교체해주면 된다.

즉, 어플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀(closed) 있는 것이다.
반대로 데이터베이스를 손쉽게 교체하는 것은 자신의 확장에는 열려(opend) 있는 것이다.

“그렇다면 인터페이스 vs 추상 클래스는 어떨까?”

인터페이스 vs 추상 클래스

DI 단점

  • DI에 대한 이해도가 필요하다.
  • 추상화되어 코드 추적이 어렵고 가독성이 떨어진다. 그로인해 협업간에 어려움이 생길 수 있을 것 같다.
    그래서 컨벤션을 잘 정해서 DI의 깊이를 정하고 가독성을 높이기 위해 고민할 필요가 있다.

"언제 DI를 사용해야할까? DI가 꼭 필요할까?"

사용하는 자원에 따라 동작이 달라지는 클래스에 DI를 사용하는 것이 좋다.

시스템에 변경이 없고, 유지보수가 필요없는 상황이라면 필요없을 것 같다.
하지만, 언제 시스템의 요구사항이 변경되고 유지보수가 필요할지 모르니 적용하는게 좋을 것 같다.

객체지향적인 측면에서 DI의 이점은 무엇인가?

  1. OCP를 지킬 수 있다.

역할과 구현을 분리하여 유연함과 변경에 용이해진다.
다형성으로 역할을 가지는 인터페이스와 인터페이스를 구현한 클래스, 구현 객체를 실행시점에 주입하여 유연하게 변경할 수 있다.

  1. DIP를 지킬 수 있다.

구현 클래스에 의존하지 말고, 인터페이스(추상화)에 의존하여 역할에 의존하게 해야한다.

스프링의 의존관계 주입 방법

  1. 생성자 주입

생성자를 통해 의존관계를 주입받는 방법으로 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
그로인해, 불변성이 보장되는 의존관계가 된다.
* 스프링 빈의 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다.
생성자에 너무 많은 의존성이 추가될 경우 리팩토링 시점을 판단할 수 있는 것도 하나의 장점이 될 수 있다.

  1. 메서드 주입 (수정자(setter) 주입, 일반 메서드 주입)

주입받는 객체가 변경될 가능성이 있을 때 사용한다.
하지만, setter 주입을 사용하려면 setter 메서드를 public으로 지정해야하고, 불필요하게 수정의 가능성을 열어두는 것은 권장하지 않는 방법이다.
일반 메서드를 통해 주입하면 한번에 여러 필드를 주입받을 수 있다.

  1. 필드 주입

코드가 간결하지만 외부에서 변경이 불가능하여 테스트하기 어렵다는 치명적인 단점이 있다.
어플리케이션 코드와 관계없는 테스트 코드, 스프링 설정 목적의 @Configuration 에서만 특별하게 사용하자

“생성자 주입을 권장하는 이유가 무엇인가?”

  1. 의존관계의 불변

대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없을 뿐만 아니라, 변하면 안된다.
생성자 주입은 객체 생성시 1번만 호출되므로 불변하게 설계할 수 있다.

  1. final 키워드 사용 가능

필드에 final 키워드를 사용할 수 있게 되어 생성자에서 값을 주입하지 않는 오류를 컴파일 시점에 막을 수 있다.
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, final 키워드를 사용할 수 없다.

“순환 참조"란?

생성자 주입 방식을 선택하여 A클래스의 Bean을 만드는데 B 클래스의 Bean을 주입할 때, B 클래스의 Bean을 만드는데 A 클래스의 Bean이 필요하여 클래스가 서로 주입이 필요한 Bean을 생성하기 위해 어떠한 Bean도 생성할 수 없고, 어플리케이션 로딩 시점에 예외가 발생한다.

→ 순환참조 설계를 개선해야 하며, 불가피하면 다른 주입방식을 선택한다.

* 메서드, 필드 주입 방식을 선택했을 때는 어플리케이션 실행과정에서는 예외가 발생하지 않지만, 서로 순환참조하고 있는 메서드를 호출하는 시점에 순환 호출 문제가 발생한다.

생성자주입은 빌드할때 객체를 만들기 위해 생성하는 시점에 발생한다. 따라서,애초에 순환참조를 방지할 수 있다.
메서드, 필드 주입은 해당 인스턴스를 사용하는 시점에 발생한다. 따라서 사용시점에 순환참조가 발생할 수 있다.

* 스프링부트 2.6까지는 컴파일 시점부터 오류를 발생해서 순환 참조 오류를 사전에 알 수 있다.
2.6 이후부터는 메서드나 필드 주입 시에도 순환 참조를 사전에 알 수 있다.

Autowired의 동작원리

Autowired는 같은 타입(Type)의 Bean을 찾아서 의존관계를 자동으로 주입해준다.
리플렉션을 통해서 내부의 Bean Factory를 탐색하여 적절한 객체를 대체하여 의존성을 주입해준다.

Autowired 시에 Bean이 여러개일 때 어떻게 동작할까? - Autowired의 다형성

Autowired는 같은 타입(Type)의 Bean을 찾아서 의존관계를 자동으로 주입해준다.

  1. @Primary
    Autowired 대상이 되는 클래스 중에서 Primary 어노테이션을 가진 클래스가 우선순위를 가진다.

  2. @Qualifier
    주입하고 싶은 Bean의 이름을 Qualifier로 지정해준다. Primary 보다 우선순위가 높다.

0개의 댓글