당니의 개발자 스토리

IoC, DI, 그리고 컨테이너 본문

스프링/스프링 핵심 원리 - 기본편

IoC, DI, 그리고 컨테이너

clainy 2024. 1. 19. 17:32

IoC, DI, 그리고 컨테이너

이번 시간에는 IoC, DI, 그리고 컨테이너 라는 용어에 대해서 알아 보겠습니다.

먼저 스프링을 공부하면, 제어의 역전, IoC, 이런 단어를 많이 들어 보셨을텐데요.

이건 스프링에만 국한된 단어는 아닙니다. 보통은 개발자가 원하는 대로 객체를 생성하고 호출하고, 또 그 안에서 그 다음 걸 생성하고 호출하고, 이런 식으로 직접 다 컨트롤 하고 제어하는 스타일로 개발을 하는데,

제어의 역전(IoC)이라는 개념은 내가 직접 호출 하는 게 아니라, 프레임워크 같은 게 내 코드를 대신해서 호출 해주는 거예요.

이렇게 말 그대로 제어권이 뒤바뀐다고 해서 제어의 역전이라고 하고요. 굉장히 여러 곳에서 제어의 역전이 나옵니다.

먼저 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했습니다. 기존 코드에서 멤버 서비스와 관련된 구현체가 자기한테 필요한 메모리 멤버 리포지토리를 직접 생성했었죠. 한마디로 구현 객체가 프로그램의 제어흐름을 스스로 다 조종했어요. 개발자 입장에서는 되게 자연스럽죠.

내가 하다가 필요한 거 있으면 new 해서 생성을 하고, 또 호출을 하고 이게 자연스러운 흐름이죠.

AppConfig가 등장 했죠. 그 뒤로 구현 객체는 자신의 로직을 실행하는 역할만 담당을 해요. 그리고 프로그램의 제어 흐름은 이제 AppConfig가 가져가요.

예를 들어서 OrderServiceImpl은 필요한 인터페이스들을 호출하죠. 그런데 어떤 구현 객체들이 실행될지는 전혀 모르는 거에요.

심지어 OrderServiceImpl 조차도 AppConfig가 생성을 합니다.

그리고 AppConfig는 OrderServiceImpl이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있어요. OrderServiceImpl2든, OrderServiceImpl3든 다른 객체를 생성할 수도 있는거죠. 그래서 자기가 사용 안 될 수도 있다는 사실도 모른 채, OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐인 거죠.

이렇게 프로그램의 제어 흐름을 직접 제어하는 게 아니라, 외부에서 관리하는 것을 제어의 역전(IoC)이라고 합니다.


프레임워크라이브러리를 구분할 때 제어의 역전이 중요한데요.

프레임워크가 내가 작성한 코드를 제어하고 대신 실행하면 그것은 프레임워크가 맞아요. JUnit이 있었죠.

테스트 케이스를 보면,

나는 @Test 해서 join() 로직만 개발한 겁니다. 근데 이거에 대한 실행이랑 제어권을 누가 가져가냐면,

JUnit이랑 테스트 프레임워크가 이 테스트를 대신 실행해줘요.

그리고 그걸 또 그냥 실행하는 게 아니라, 자신만의 라이프 사이클이 있어요.

@BeforeEach를 먼저 실행하고, 그 다음에 테스트를 실행한다는 이런 프레임워크의 라이프 사이클 코드 속에서 내 것만 콜백식으로 불러지는 거죠.

이렇게 내가 제어권을 가지고 있는 게 아니고, 나는 그 프레임워크 안에서 필요한 부분만 딱 개발하면, 프레임워크가 알아서 적절한 타이밍에 호출하는 거, 이렇게 호출하는 제어권을 넘기는 걸 제어의 역전이라고 합니다.

근데 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당하면, 그거는 프레임워크가 아니고 라이브러리에요. 예를 들어서, 우리가 Java 객체를 XML로 바꾸거나 JSON으로 바꾸는 게 있어요. 그럼 그 라이브러리를 불러다가 내가 직접 호출하잖아요.

이렇듯 내가 직접 호출해서 내 코드가 제어의 흐름을 담당한다면, 그건 라이브러리라고 보시면 됩니다.


자 그래서 제어의 역전이랑 프레임워크 vs 라이브러리에 대해서 간단히 알아봤구요.

그 다음에 의존관계 주입에 대해서 말씀드리겠습니다.

OrderServiceImpl은 DiscountPolicy Interface에만 의존하는 거 아시죠. OrderServiceInput은 DiscountPolicy Interface에만 의존하도록 우리가 바꿨죠. 그래서 실제 어떤 구현 객체가 사용될지는 이제 몰라요.

그림을 보면, OrderServiceImpl 입장에서는 DiscountPolicy 인터페이스만 알고 있는 거예요. 그래서 실제 Fix DiscountPolicy 들어올지, RateDiscountPolicy가 들어올지는 전혀 모르는 거예요.

이제 중요한 얘기인데,

정적인 클래스 의존관계는 뭐냐면, 클래스가 사용하는 import 하는 코드만 보고 의존관계를 쉽게 판단할 수가 있어요. 내가 사용하는 클래스객체들만 보고 바로 판단할 수 있는 거예요. 그게 이제 클래스 코드만 열어봐도 판단할 수 있는 거.

OrderServiceImpl을 보면, 의존관계가 Member, MemberRepository, DiscountPolicy 등 이 정도 사용하죠.

이런 걸 정적인 의존관계라고 하고요. 정적인 의존관계는 애플리케이션을 실행하지 않고도 분석할 수 있어요. 그래서 tool 같은 걸로 분석이 가능해요.

Show Diagram 들어가서,

누르면,

의존관계를 한눈에 볼 수 있습니다. 정적으로 실제 실행하지 않고 판단할 수 있는 거죠.

그래서 이렇게 정적인 클래스 의존관계실행시점에 결정되는 동적인 객체 의존관계를 분리해서 생각해야 합니다.

정적인 의존관계는 애플리케이션을 실행하지 않아도 분석을 할 수 있습니다. 클래스 다이어그램을 봅시다.

우리가 OrderServiceImpl 이라는 코드를 보면 뭘 알 수 있습니까?

상위에 OrderService가 있구나 알 수 있죠.

그리고 OrderServiceImpl은 MemberRepository 인터페이스와 DiscountPolicy 인터페이스를 참조하고 있죠.

그리고 DiscountPolicy는 아무것도 의존하지 않지만, FixDiscountPolicyRateDiscountPolicyDiscountPolicy에 의존하고 있죠.

그림의 화살표는 내가 뭔가를 의존하고 있다는 의미에요. 상속관계에서도 화살표가 나오는 이유가 그거예요. 상속이든, Interface 구현이든 화살표 방향으로 의존하고 있다는 뜻이에요.

아무튼 OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있습니다. 그런데 이러한 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl에 주입 될지는 알 수가 없죠.

OrderServiceImpl의 코드만 보고는 뭐가 올지 분석이 불가능해요. 이거는 실제 실행을 시켜봐야 아는 거란 말이에요.

그래서 이런 거를 동적인 객체(인스턴스) 의존 관계라고 합니다.

이제 객체 다이어그램을 보면,

클라이언트는 주문 서비스 구현체를 호출하겠죠. 그런데 우리가 AppConfig에서 어떻게 했나요?

orderService를 생성할 때 뭘 넣냐면,

MemoryMemberRepository랑 RateDiscountPolicy가 결과적으로 들어갑니다.

그렇게 되면, 메모리 회원 저장소와 정률 할인 정책이 들어가겠죠. 그림은 정액 할인 정책이긴 한데.

객체 인스턴스 다이어그램은 언제 결정되냐면, 정적인 게 아니에요. 애플리케이션 실행할 때마다 동적으로 바뀌는 거예요.

그래서 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달을 해서, 클라이언트(ex. OrderServiceImpl)와 서버(ex. RateDiscountPolicy)의 실제 의존관계가 연결되는 것을 Dependency Injection이라고 해요. 의존관계 주입이라고 합니다.

객체 인스턴스를 생성하고, 그 참조 값을 연결하는 거죠. 자바는 레퍼런스, 그러니까 참조로 객체들이 다 연결되잖아요.

그래서 여기에 참조 값이 들어가있는 거 아시죠.

의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다.

그리고 이게 중요합니다. 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 할 수 있다.

우리가 의존관계 주입을 사용했더니 이 그림이 전혀 안 변했죠. 정적인 그림을 전혀 손댈 필요 없이,

이 동적인 그림만 바꿀 수 있는 거예요.

그러니까 정적인 다이어그램을 손대지 않는다는 말은 애플리케이션 코드를 손대지 않는다는 뜻이에요. 얘네들의 의존관계를 전혀 손대지 않고 구현체만 바꿀 수 있다는 거에요. 그게 의존관계 주입의 장점이라고 볼 수 있습니다.


이제 IoC 컨테이너, DI 컨테이너에 대해서 설명해드릴게요.

IoC 컨테이너, DI 컨테이너라는 용어가 나오는데요. 스프링의 DI 컨테이너라는 게 AppConfig의 역할을 해주는 거예요.

단순하게 IoC(제어의 역전)를 해주는 컨테이너, Dependency Injection을 해주는 컨테이너라고 보시면 되고요. 같은 거를 다르게 부르는 거예요.

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너, 또는 DI 컨테이너라고 해요.

OrderServiceImpl 자체를 생성해주는 것도 AppConfig가 가져가고, OrderServiceImpl에 어떤 게 주입이 돼서 전체 애플리케이션이 어떤 흐름으로 제어하고 돌아갈지 누가 결정한다고요? AppConfig가 결정을 해버린단 말이에요. 의존관계의 역전이 일어난 거죠. 그래서 의존관계의 역전을 일으킨다고 해서, IoC 컨테이너, 또는 DI 컨테이너라고 합니다.

그러니까 제어권이 넘어간다는 의미의 IoC는 굉장히 여러 군데서 일어나는 거거든요. 아까 Junit 같은 것도 다 IoC라고 했잖아요. 그래서 이건 너무 범용적이다. 라고 해서 후대에 IoC는 Dependency Injection을 잘 해주는 애라고 해서, DI Container로 이름을 바꿨어요.

지금 AppConfig를 보면,

AppConfig가 Dependency Injection(의존관계 주입)을 대신 해주죠.

그래서 AppConfigDI 컨테이너 라고 합니다.

요즘에는 의존관계 주입에 초점을 맞춰서, IoC 컨테이너라는 단어를 잘 안쓰고 주로 DI 컨테이너라고 얘기를 합니다.

그리고 스프링이 DI 컨테이너 역할을 하죠. 근데 꼭 스프링만 DI 컨테이너 역할을 하는게 아니에요. 여러 DI 컨테이너의 오픈소스 굉장히 많아요.

그리고 AppConfig 같은 애에 대해서 용어가 굉장히 많아요. 마치 AppConfig가 애플리케이션 전체에 대한 구성을 하나하나 조립한다고 해서 Assembler(조립자) 라고 부르기도 합니다. 또는 AppConfig가 오브젝트(객체)를 만들어낸다고 해서 ObjectFactory 등으로 부르기도 합니다.

여러분은 그냥 '아 얘가 DI 컨테이너구나' 라고 생각을 해주시면 됩니다.


드디어 다음시간부터는 이제 스프링으로 아주 간단하게 바꿔볼 거에요.