Post

의존성 주입(DI, Dependency Injection)이란?

의존성이란?

“의존성 주입”이 무엇인지 얘기하기 전에 “의존성”이 무엇인지부터 명확히 해야 한다.

우리가 의존성 의존성 말은 많이 하는데, 의존성이라는 단어가 등장할때면 어김없이 추상적인 개념이 하나 둘 튀어나오다 보니 코드에서 정확히 뭘 의미하는건지 감을 못잡는 경우가 많다.

1
2
3
4
public class SimpleMovieLister {
    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;
}

이게 의존성이다.
별게 아님.
그냥 한 객체에서 다른 객체 갖다 쓰면 의존성이다.

그렇다면 의존성 주입은 무엇이냐?

1
2
3
4
5
6
7
public class SimpleMovieLister {
    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = Objects.requreNonNull(movieFinder);
    }
}
  • 이렇게 생성자를 통해서 객체(의존성)을 외부에서 넣어주면 그게 의존성 주입이다. “필요한 자원을 외부에서 생성자를 통해 넣어준다.” 를 어렵게 표현하면 의존성 주입.
    • SimpleMovieLister 내부에서 new MovieFinder()를 해서 직접 객체를 만들어버리는게 의존성 주입의 반대 라고도 할 수 있을 것 같다.
  • 의존성 주입은 이처럼 생성자를 통해서도 가능하고, setter를 통해서도 당연히 가능하다.
    • Constructor-based DI
    • Setter-based DI

이 개념이 왜 중요할까? : 인터페이스 참조를 통한 의존성 끊기

만약 MovieFinder가 인터페이스고, MovieFinder를 implement한 HorrorMovieFinder 클래스가 있다고 가정하자.
그러면 SimpleMovideFinder 안에서 MovideFinder 변수를 resolve하려면

1
2
3
public class SimpleMovieLister {
    ... this.movieFinder = new HorrorMovieFinder()
}

요렇게 될 것이다.
근데 이렇게 SimpleMovieLister 안에서 구체적인 타입이 결정되어 버리면, SimpleMovieLister는 MovieFinder라는 인터페이스 자체가 아니라 HorrorMovieFinder와 의존성을 갖게 된다.

그래서 이걸 해결해 주는 것이 바로? => DI

내부에서 new로 구체적인 객체를 생성해서 변수를 resolve하는게 아니라, 외부에서 구체적인 타입(HorrorMovieFinder)을 결정하고 그걸 SimpleMovieLister에 주입 해 주겠다는 것이다.

DI를 사용하면 SimpleMovieLister는 MovieFinder의 실제 구현체가 무엇인지와 관계 없이 그냥 MovieFinder로 모든 것을 처리한다.
즉, MovieFinder와의 의존성만 존재하는 것이다. 어떤 구현체든지 그냥 외부에서 만들어서 넘기기만 하면 되니 재활용성이 높아진다.

그러나 DI를 사용하지 않고, SimpleMovieLister 안에서 new HorrorMovieFinder()를 만들게 되면 다른 MovieFinder 구현체를 사용하기 위해서는 코드를 수정해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {
    1... HorrorMovieFinder movieFinder = new HorrorMovieFinder();
    // 제일 별로. SimpleMovieLister 안에서 HorrorMovieFinder에 종속적인 필드,메서드를 사용하면서 의존성이 강해짐.

    2... MovieFinder movieFinder = new HorrorMovieFinder();
    // 이 정도만 해도... 다른 MovieFinder가 필요하다면 선언부만 변경해주면 되니까 변경에 대한 부담이 크진 않은.
    // 하지만 다른 MovieFinder를 사용해 테스트를 하기 위해서는? 여기까지 와서 Horror..를 다른 것으로 변경해주어야 한다.

    3... MovieFinder movieFinder;
    // DI를 사용하면 아예 의존성 그래프 자체가 HorrorMovieFinder와 직접 연결되지 않음.
    // 다른 MovieFinder를 사용해 SimpleMovieLister를 테스트하고 싶다면, 그냥 다른MovieFinder를 DI 해주면 되므로 테스트 용이.
}

언제 써야 하나?

DI 개념은 그냥 객체를 외부에서 만들어서 넣어주면 되는 것이기 때문에, 프레임워크와 무관하게 언제든 사용할 수 있다.

그러나 의존성이 많아지면 그 만큼 의존성을 관리하는 코드도 복잡해진다.

그래서 Spring, Dagger같은 DI 프레임워크를 사용하는 것이고, 이를 사용하면 설정한 대로 프레임워크가 알아서 DI 해준다.

DI의 장점?

어떤 객체 참조를 static 변수 또는 내부 멤버 변수로 두는 것 대비,
interface 참조하여 DI 받는 것은 다음과 같은 장점이 있다.

  • 직접 참조해서 쓰는 것 보다, DI로 넘겨받는 것은 상대적으로 더 느슨한 결합을 가지게 된다.
    • 재사용성에 도움이 된다. 넘겨받는 bean만 다른 것으로 바꿔주면 되므로.
    • 다형성을 사용할 수 있으니 프로그램이 유연해진다.
  • DI는 mocking이 쉬워진다. 다른 mock 객체를 DI해주면 되니까. 그래서 테스트가 용이하다.
    • 테스트 대상 클래스가 Constructor로 의존성을 받는다면 바깥에서 Mock객체 만들어서 DI해주면 되나, 클래스 내부에서 Util 클래스를 직접 불러 사용한다면? 이건 Mocking이 불가능하다.
    • Constructor에 직접 Mock 객체 넣어주는 방법도 있고, Spring을 사용한다면 @Primary 를 통해서 자동으로 Mock객체가 먼저 들어가게끔 할 수도 있다.
  • 별도 Configuration이 필요한 경우, 설정이 적용된 Bean을 각 서비스에서 DI 받을 수 있음.
    • e.g. RestTemplate, ObjectMapper, …
  • DI 주체인 외부 컨테이너에서 Singleton lifecycle을 관리해 줄 수 있다.
    • @Scope 애너테이션 참조
    • Spring Container가 Bean lifecycle에 맞게 @Destroy 같은 hook을 호출 해준다.


반면 단점도 있는데, 어떤 컴포넌트에서 framework의 bean을 DI 받기 위해서는 그 컴포넌트도 bean이어야 한다. 따라서 시스템 전역적으로 대부분의 컴포넌트가 bean이 된다.

프레임워크 의존성 없이 관리하고 싶은 항목들(e.g., CoroutineScope, Client 등) 조차 다른 컴포넌트를 사용하기 위해서 bean으로 만들어야 하는 경우가 생긴다.

This post is licensed under CC BY 4.0 by the author.