Coderoad

IoC와 DI

2023-01-18 at Spring category

본 문서는 인프런에서 수강할 수 있는 스프링 핵심 원리 - 기본편을 수강한 후, 공부한 내용을 정리한 문서입니다. 본 문서의 모든 저작권은 해당 강의의 저자이신 김영한 우아한형제들 기술이사님께 있습니다.

제어의 역전 IoC

제어의 역전(Inversion of Control)은 말 그대로 제어에 역전이 일어났다는 의미입니다. 제어에 역전이 일어났다니, 도대체 무슨 말일까요? 그동안 우리가 작성해왔던 프로그램들은 크게 신경쓰지 않았다면 자연스럽게 구현 객체가 프로그램의 흐름을 제어했습니다. 구현 객체 스스로 로직에 필요한 다른 구현 객체들을 생성하고(new 키워드), 연결하고, 로직에 맞춰 실행했습니다. 그러나 이런 방식은 결국 구현 객체에 의존해야(알아야)한다는 문제가 있습니다.

다시 말해서 SOLID 중 DIP(의존관계 역전 원칙)를 위반한다는 것입니다. 또한, 기존의 방식은 SRP와 OCP도 함께 위반하게 됩니다. 구현 객체가 객체의 생성, 연결, 실행까지 모두 담당하고 있어 SRP(단일 책임 원칙)에 위배되고, 새로운 구현 객체를 비즈니스에 적용하고자 코드를 수정해야할 때, 기존의 구현 객체에 의존하는 모든 코드들을 수정해야하기 때문에 OCP(개방-폐쇄 원칙)을 위반하게 됩니다.

우린 좋은 객체 지향 프로그래밍을 위해 여러 SOLID 원칙들을 위반하는 문제를 해결해야합니다. 먼저, DIP를 준수하기 위해선 추상화에만 의존해야합니다. 구현 객체들은 다른 구현 객체에 전혀 의존하지 않고, 자신들이 맡은 비즈니스 로직만 잘 수행하면 됩니다. 즉, 구현 객체 내부에서 다른 구현 객체들을 알고 있을 필요가 없습니다. 하지만 다른 구현 객체들과 협력하지 못한다면 SOLID를 지키기는 커녕 프로그램이 제대로 돌아갈리가 없습니다. 그렇다면 어떻게 추상화에만 의존하면서 객체들 간의 협력 관계를 부여할 수 있을까요?

방법은 간단합니다. 추상화에만 의존하고 있는 객체들에게 구현 객체를 전달해주는 구성자가 존재하면 해결됩니다. 모든 구현 객체들을 생성하고 필요한 곳에 연결해주는 등의 제어 권한을 가진 클래스를 만들어주면 DIP를 준수할 수 있습니다. 이 구성자의 등장으로 SRP와 OCP도 준수할 수 있습니다.

구성자는 앞서 설명했던대로 객체 생성, 연결이라는 책임을 구현 객체들로부터 가져왔기 때문에 구현 객체는 실행이라는 책임만 가지게 됐습니다. 또한, 구현 객체들이 추상화에만 의존하게 되었으므로 구현 객체를 교체할 때 수정해야하는 것은 구성자이지 구현 객체들이 아니기 때문에 OCP도 준수할 수 있습니다.

이렇게 외부의 구성자가 프로그램의 흐름을 관리하는 것을 제어의 역전(Inversion of Control)이라고 합니다. 흔히 프레임워크들이 제어의 역전을 통해 우리가 작성한 코드들을 제어하고 대신 실행합니다. 그에 반해 라이브러리는 우리의 코드를 제어하기 보단 라이브러리의 코드를 우리가 직접 활용하기 때문에 제어의 역전을 사용하지 않습니다.

의존관계 주입 DI

의존관계 주입(Dependency Injection)은 의존관계를 외부에서 주입해주는 것을 말합니다. 우리는 앞서 SOLID 위반을 피하기 위해 추상화에만 의존하고자 프로그램에 IoC 개념을 적용했습니다. 추상화에만 의존하게 된 구현 객체들은 자신이 의존하고 있는 인터페이스 외에는 알고 있는 것이 전혀 없습니다. 하지만 인터페이스는 역할이고 구현체가 아니기 때문에, 인터페이스만 알고 있어서는 프로그램이 올바르게 동작하지 않습니다. 제대로 프로그램이 동작하도록 프로그램의 제어권을 가져간 구성자가 의존관계를 설정해줘야 합니다.

구성자가 관리해야할 의존관계에 대해 자세히 알아보기 전에 의존관계의 두 가지 분류를 살펴보겠습니다. 먼저, import 구문을 통해 명시적으로 작성하는 의존관계인 정적 클래스 의존관계가 있습니다. 이 의존관계는 import 구문을 읽으면 어떤 객체에 의존하고 있는지 프로그램을 실행하지 않아도 알 수 있습니다.

//이 코드는 MemberRepository 객체에 의존하고 있습니다.
import hello.core.member.MemberRepository;

그러나, 우리는 프로그램을 오로지 추상화에만 의존하도록 제어의 역전을 일으켜야 SOLID를 준수할 수 있다는 것을 알았습니다. 위의 코드와 같은 정적 클래스 의존관계는 의존하고 있는 추상화가 무엇인지는 알 수 있지만 실제 구현 객체는 전혀 알 수 없습니다.

이때, IoC와 구성자의 역할이 중요한 이유가 나타납니다. 프로그램의 제어권을 가져간 구성자는 프로그램의 실행 시점(런타임)에 실제 구현 객체 인스턴스를 직접 생성하고 해당 객체를 필요로하는 코드들에 의존관계를 연결합니다. 이것을 의존관계 주입(Dependency Injection)이라고 합니다. DI를 사용하면 클라이언트 코드를 전혀 변경하지 않고, 클라이언트가 호출하는 대상의 인스턴스 타입을 변경할 수 있습니다. 즉, OCP 원칙이 준수된다는 의미입니다.

컨테이너의 등장

IoC를 통해 프로그램의 제어권을 온전히 가지게 된 구성자는 앞서 살펴본대로 객체를 생성하고, 관리하면서, 의존관계를 연결(DI)해주는 역할을 수행하고 있습니다. 이 구성자를 바로 컨테이너(Container)라고 합니다. IoC 컨테이너, DI 컨테이너 등, 많은 이름이 있지만 최근에는 DI에 중점을 두고 주로 DI 컨테이너라고 합니다. 물론 Spring에서는 Spring 컨테이너라고 합니다. 이름은 차이가 있어도 맡고 있는 역할은 전혀 다를게 없으니 Spring을 공부하는 입장에선 Spring 컨테이너라고 하는 것이 좋을 것 같습니다.

다시 한번 강조하지만, 컨테이너 외부에서는 애플리케이션의 모든 객체는 추상화에만 의존해야합니다. 컨테이너 외부에서 추상화가 아닌 구현체에 의존하게 되면 Spring을 사용하는 의미도 없을 뿐더러 SOLID에 위배되어 좋은 객체 지향 프로그래밍도 아니기 때문입니다. 물론 테스트 코드를 작성할 때는 필요에 따라 new 키워드로 구현 객체를 생성해도 상관 없습니다. 단위 테스트를 위해 간단한 더미 데이터가 필요한데 실제 서비스 코드를 수정하는게 훨씬 비효율적이기 때문입니다.

자, 이제 우리는 IoC를 통해 만들어진 구성자가 컨테이너라는 것도 알았습니다. 좋은 객체 지향 프로그래밍을 위해서 직접 컨테이너를 만들어 구현 객체를 생성하고, 관리하면서, 의존관계도 주입해주면 됩니다! Spring에서는 기본적으로 ApplicationContext라는 인터페이스를 통해 컨테이너를 제공하고 있습니다. 즉, 우린 Spring 컨테이너에 대해 공부하면 지금까지 알아봤던 여러 개념들을 간단하게 적용할 수 있는 것입니다! 이제 Spring의 컨테이너에 대해 더 자세하게 공부해, Spring의 장점을 극대화해봅시다.

hangillee

Personal blog by hangillee.

Road to good developer.