의존 관계 주입(DI)과 제어의 역전(IoC)
이번 포스팅에서는 의존 관계 주입(Dependency Injection, DI)과 제어의 역전(Inversion of Control, IoC)이 무엇인지 살펴보고 이를 통해 프레임워크와 라이브러리의 차이점도 알아보겠습니다.
의존 관계 주입(DI)이란?
우선 의존 관계 주입을 이해하기 위해서는 앞선 관심사의 분리 포스팅의 내용을 이해해야합니다. 이전 포스팅에서 관심사의 분리를 설명하면서 “단일 책임 원리에 따라 역할에 너무 많은 책임을 주어선 안되고, 공연을 예시로 배우가 캐스팅까지 맡아선 안된다. 그러므로 캐스팅 담당자라는 역할을 따로 두어야한다.”라고 설명을 드렸던 기억이 납니다.
이 캐스팅 담당자라는 것은 저희가 구현하고자 하는 Application의 AppConfig(설정 및 구성을 관장하는 클래스에 대한 가칭)입니다. AppConfig에서는 역할에 구현체를 넘겨주기 위해 보통 생성자 주입 패턴을 활용하며, 프로그램이 실행되는 런타임 시에 구현 객체를 AppConfig에서 직접 생성해서 역할(인터페이스)에 구현체를 넣어주게 됩니다. 이렇게 외부에서 구현 객체를 넣어주는 것을 의존 관계 주입(Dependency Injection, DI)라고 합니다.
의존 관계는 크게 정적인 클래스 의존 관계와 동적인 객체 인스턴스 의존 관계로 나눌 수 있습니다. 이 부분이 말로만 들어서는 이해가 쉽지 않기 때문에 참고 자료를 가져왔습니다.
위 이미지는 클래스 다이어그램이고 인터페이스가 지금까지 계속 설명드린 역할입니다. 이 역할에 대해 인터페이스를 implements하고 있는 구현 객체들이 있습니다.
예를 들면 DiscountPolicy라는 인터페이스를 구현하는 FixDiscountPolicy와 RateDiscountPolicy가 있습니다. 이러한 구현 객체들에 대한 생성은 캐스팅 담당자 역할을 맡은 AppConfig에서 관리하게 되고 객체들의 생성은 동적으로 런타임 시에 이루어지게 됩니다. 따라서 인터페이스와 같은 역할에 의존 관계를 맺고 있으면 정적인 클래스 의존 관계라고 부르며 이는 컴파일 시에 이루어지기 때문에 별도로 코드를 실행시키지 않고도 의존 관계를 알 수 있습니다.
이와 다르게 동적인 객체 인스턴스 의존 관계는 런타임 시에 생성되는 구현 객체들에 의존하는 경우를 말하며 이는 코드를 실제로 실행해야 해당 구현 객체들이 생성되기 때문에 런타임 시에 이런 의존 관계가 맺어지게 됩니다.
제어의 역전(IoC)이란?
지금까지 Dependecy Injection에 대해 자세히 알아봤습니다. 그러면 제어의 역전이란 무엇일까요?
제어의 역전을 이해하기 위해서는 앞선 AppConfig에 주목해볼 필요가 있습니다. 우선 그 전에 앞서서 AppConfig와 같은 캐스팅 담당자의 역할이 나오기 전의 상황으로 돌아가보겠습니다. 다음은 관심사의 분리 포스팅에서 문제가 되었던 코드입니다.
1
2
3
public class UserService {
private UserRepository userRepository = new MemoryUserRepository();
}
위 코드를 보시면 UserService라는 클래스가 자신의 userRepository라는 필드의 값을 위해 MemoryUserRepository라는 구현 객체를 직접 생성하는 것을 볼 수 있습니다.
이렇듯, 원래 전통적인 프로그래밍 방식에서는 여러 가지 객체들이 있고 그 객체들을 사용하는 과정에서 필요하다면 그때그때 해당 객체에서 생성해서 불러 사용한다던지 하는 방식으로 코딩을 했었습니다.
하지만, 위처럼 코드를 짜면 단일 책임 원칙에 위배되며 추후에 변경하기 어려운 코드가 됩니다. 위 상황을 해결하기 위해 나온 것이 AppConfig와 같은 캐스팅 담당자 역할입니다.
AppConfig의 등장 이후로 UserService와 같은 클래스들은 생성에 전혀 관여하지 않고 외부에서 런타임 시에 Dependency Injection 해주는 대로 그대로 받아 사용하게 됩니다. 즉, 원래 직접 필요한 객체 인스턴스들을 생성해서 만들어 사용했던 UserService가 이제는 외부에서 생성해주는 객체를 받아 사용하기만 한다는 것입니다. 이렇게 제어 권한이 외부로 넘어가 버리는 상황을 제어의 역전이라고 합니다.
바로 이 제어의 역전을 통해 많이들 혼동하시는 프레임워크와 라이브러리를 구분할 수 있습니다. 마치 AppConfig의 역할처럼 프레임워크는 내가 작성한 코드에 대한 제어권을 가질 수 있고, 이를 통해 대신 실행한다면 그것은 프레임워크입니다.
단적인 예로 JUnit을 들 수 있습니다. JUnit을 활용해서 Unit test code를 작성할 때 저희는 테스트 로직에 대한 부분만을 작성하게 됩니다. 이때 해당 로직을 실행하면 JUnit 자체의 라이프 사이클에 맞춰 제가 짠 test code가 호출되어 알아서 실행됩니다. 이렇듯 실행에 대한 제어를 완전히 넘겨줄 수 있기 때문에 JUnit은 프레임워크라고 할 수 있습니다.
반대로 라이브러리는 제어권을 가질 수도 없고 따라서 대신 실행시켜줄 수도 없습니다. 예를 들어 객체를 JSON 형식으로 바꿔주는 라이브러리를 생각해봅시다. 객체를 JSON 포맷으로 변경이 필요할 때 개발자가 직접 호출하여 제어권을 넘겨주지 않고 내 흐름에 끼워 맞춰 사용합니다. 따라서 이는 라이브러리에 속합니다.
IoC 컨테이너, DI 컨테이너란?
IoC와 DI에 대한 개념을 배웠으니 IoC 컨테이너와 DI 컨테이너 개념에 대해 알아보겠습니다. 지금까지 살펴본 AppConfig와 같이 객체에 대한 생성을 담당하고 의존 관계를 연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 부릅니다.
원래 초기에는 제어의 역전 관점에 좀 더 초점을 맞춰서 IoC 컨테이너라고 주로 불렀지만 앞서 JUnit 예제에서 살펴봤듯이 프로그램의 실행 권한 등이 넘어가는 것도 IoC라고 부르기 때문에 너무 범용적이라서 혼란을 초래할 수 있기 때문에 요즘은 주로 의존 관계 주입에 포인트를 둬서 DI 컨테이너라고 더 많이 부른다고 합니다.
즉, 오해하지 말아야 될 것이 제어의 역전이 가능하면 그게 IoC 컨테이너나 DI 컨테이너가 아니고 AppConfig와 같이 객체에 대한 생성 전반을 관장하고 의존 관계를 맺어주는 것이 IoC 혹은 DI 컨테이너입니다. 일부 사람들은 이를 어셈블러, 오브젝트 팩토리 등으로 부르기도 합니다.
마무리
이번 포스팅에서는 객체 지향 설계를 공부한다면 반드시 짚고 넘어가야할 의존 관계 주입과 제어의 역전에 대해 살펴봤습니다. 이를 통해 제어의 역전이 적용되는지 안되는지에 따라 프레임워크와 라이브러리를 구분하는 방법까지 알아봤습니다. 지금까지 OOP, SOLID, SoC, IoC, DI 등 객체 지향 프로그래밍 관련 전반적인 핵심 키워드를 공부하다보니, “스프링 프레임워크는 결국 더 객체 지향적으로 잘 코드를 짤 수 있게 해주기 위해 나왔구나”라는 생각이 문득 듭니다. 이번 공부를 계기로 한층 더 객체 지향적으로 질 좋은 코드를 작성하는 개발자가 되었으면 좋겠습니다.
References
- 인프런 내 김영한 강사님의 스프링 핵심 원리 - 기본편