주요 내용으로 건너뛰기

프레임워크의 핵심, 의존성 주입과 제어의 역전

프레임워크라는 다소 복잡할 수 있는 개념이 왜 생겼고, 왜 이것들이 필요한 것인가?

Laravel's Dependency Injection Container in Depth 라는 문서를 번역한 후 의외로 많은 분들이 IoC와 DI 개념에 대한 이해도 부족에 시달린다는 사실을 알았습니다. 사실 프로그래밍은 언어학과 유사한 면이 많습니다. 체득과 망각의 과정을 통하여 온전한 자신의 것으로 만드는데 오랜 시간과 경험이 필요하죠. 이것은 프로그래밍이 하나의 절대적인 것을 탐구하거나 하는 학문이 아닌 프로그래밍이 먼저 필요에 의해 탄생하고, 그것을 다듬어서 하나의 법칙 또는 이론으로 만들었기 때문입니다. 일본어에는 "라이먼의 법칙(ライマンの法則)"이라는 것이 있는데, 이 또한 실재하는 현상에 대해 정리한 것에 불과하죠. 이런 이론들은 근본적으로 경험이 없다면 체득이 어려운 것들입니다. 그러니 앞으로도, 마치 하나의 언어를 공부하듯 경험을 쌓아가시길 바라겠습니다. 체득과 망각을 기억하시면서요.


자 우선, 의존성 주입이라는 개념을 알고 가셔야 합니다. 클래스가 인스턴스화를 할 때 우리는 보통 

와 같이 new 키워드를 사용합니다. 대다수의 경우 직접 클래스를 인스턴스화 시키는 것에 문제는 없습니다. 그러나 규모가 큰 프로젝트나 단위 테스팅 등 여러 환경에서는 이렇게 직접 클래스를 인스턴스화 시키는 것이 바람직한 방법은 아닙니다. SOLID에서 D를 뭐라고 불렀는지 기억이 나십니까? "Dependency inversion principle"입니다.

좋은 객체 지향 프로그래밍은 추상적인 것에서 구체적인 방향으로 흐르는 코드를 작성하는 것입니다. 직접 클래스를 인스턴스화 시키는 코드는 무척이나 구체적이죠. 프로그래머가, 컴퓨터에게, 지시하는 것입니다. 그리고 무척이나 의존적입니다. 어느 날 SomeClass의 구조가 바뀐다면 어떤 일이 발생할까요? 조금, 아니 상당히 곤란하겠죠. 갑자기 SomeClass가 아닌 다른 클래스를 사용해야 한다면 어떨까요? 아 정말 귀찮을 것입니다. 생산성은 바닥을 치고, 의욕도 바닥을 치고, 어느 날 갑자기 사표를 쓰고 있는 자신을 발견하겠죠. 이미 이 순간 OCP도 위반했지만, 그건 넘기도록 하죠 

이럴 때 우린 Interface를 찾게 됩니다. 인터페이스는 명세서입니다. 근본적 개념은 마치 계약과 같아서 "이것을 구현하지 않는 클래스는 날 구현시킬 자격이 없다" 뭐 이런 느낌입니다. 자, 조금 생각을 해보죠. 개발자는 사실 라이브러리가 정확하게 어떻게 동작해야 하는지에 대한 정보를 알 필요가 없습니다. 알면 좋지만, 그걸 적어도 코드가 알고 있어야 하는 건 아니죠. 프로그램은 내 의도대로 동작만 하면 그만입니다. 인터페이스는 바로 이럴 때 쓰이는 것입니다. "왜 프로그램이 어떤 클래스인지 꼭 알아야 해? 그냥 특정 기능을 동작시킨다는 보장만 있다면 그만이잖아?"

이것이 바로 DIP에서 말하는 "구체적인 것에 의존하지 말고 추상적인 것에 의존해라" 라는 것입니다. Interface 는 추상적입니다. 구현을 담당하지 않고 명세만을 제공하죠. 그렇다면 이 개념을 어떻게 Dependency Injection에 녹여낼 수 있을까요? 

자 위 코드처럼, "마실 수 있는 것"을 인수로 지정한다면, 여러분은 이제 "drink 인자는 마실 수(act)있다"라는 사실을 알고, 이 행위를 실행할 수 있습니다. 내가 만약 콜라를 마시고 싶다? 저 코드를 불러올 때 콜라를 넣어주면 되는 것입니다. 

마실 수 있는 것은 추상적인 것입니다. 마실 수 있는 것이니, 내가 그것을 받아 필요할 때 마시면 그만이겠지만 꼭 그게 특정한 마실 것일 이유는 없죠. 아니 뭐 내 입에는 콜라만 들어가나? 물론 대다수 상황에서 제 입으로 들어가는 것은 콜라이지만, 꼭 그러라는 법은 없죠. 마찬가지입니다.


그럼 이걸 효과적으로 써먹을 수 있는 방법을 모색해야겠죠. 이 때 나온 개념이 Inversion ofcontrol 입니다. 하나의 중앙관리 시스템이 있고, 이 시스템이 인스턴스를 생성해주고 관리해준다 보시면 됩니다.제어의 역전이라 부르는 이유가 바로 여기에 있습니다. 지금까지는 사용자(개발자)가 인스턴스를 관리했지만, 그건 이제 개발자의 몫이 아닌 거죠. 

의존성을 주입받을 때, 수많은 의존성을 해소시키기 위해서는 그만큼의 코드를 직접 짜고, 인스턴스를 관리해야 합니다. 세상에. 편하자고 한 짓거리가 이런 스노우볼이 되어 굴러오다니요. 그런데, 이것을 "잘" 만든다면 아예 인스턴스 관리를 위임시킬 수 있을 것 같습니다. 특정한 규칙만 지킨다면 무언가 중앙 관리 시스템을 만들어서 이 모든 코드를 올려두고, 거기서 필요할 때 특정 코드를 실행시킬 수 있을 것만 같습니다.

바로 이것이 프레임워크의 핵심입니다. 라이브러리와 프레임워크의 근본적 차이는 프레임워크는 내가 짠 코드를 관리하고 실행시킵니다. 그 코드가 필요하다 판단되었을 때 말이죠. 라이브러리가 이런 역할을 할 수 있을까요? 라이브러리는 소비자가 될 수 없습니다. 프레임워크는 코드의 소비자로서 동작합니다. 이 모든 인스턴스를 관리해주기 위해 존재하는 것이 DI Container입니다. 


왜 Kotlin 이나 Java가 없죠..? 하다 못해 Typescript라도..

자, 이 코드 어디에도 new 가 존재하지 않습니다. 그런데 이 코드는 잘 작동하고, 정체모를 음료도 마실 수 있습니다. 왜냐면, 저기 Container라는 친구가 인스턴스를 다 알아서 관리해줬기 때문입니다. 생성자에서 Drinkable을필요로 하고 있죠? 이 컨테이너라는 친구는 무척 똑똑해서, Drinkable한 인스턴스가 어떤 것인지만 알고 있다면 알아서 이런 의존성을 해소시켜 줍니다. 

Spring Framework 에서는 @Autowired도 많이 사용하실텐데, 이건 좋은 방법은 아니니 주의하시기 바랍니다. 간략한 것보다 명확한 것이 좋은 법입니다.

이런 의존성 해소에는 프레임워크마다 특정한 규칙이 있고, 이것은 프레임워크마다 다 다릅니다. Spring 은 거의 이쪽 계의 거성이라 좀 깡패같이 복잡하죠. 어떻게 IoC 를 구현하고 있는가, 에 대해서는 각 프레임워크별 책을 구매하시거나, 강좌를 따로 읽어보심이 좋습니다.


모든 DI가 IoC를 필요로 하는 것은 아닙니다 Pure DI라는 컨셉도 있으며, 충분히 좋은 개념입니다. https://martinfowler.com/articles/injection.html 에서 더 자세한 내용을 확인해 보실 수 있습니다. 그리고 모든 시스템이 의존성 주입을 요구하는 것도 아니죠. (React: 난 라이브러리야.)

자 이제 여러분은 몇 가지 원칙을 배웠습니다. 그리고 SOLID에 대해 조금 더 이해도가 깊어지셨겠죠. 하지만 아직도 갈 길이 많이 남았습니다. Functional Programming에선 쓸모 없는 이야기로 전락해버린 이 개념들, OOP 에서 더 좋은 구조를 만드는 법, 테스트하기 편한 코드.. 배워야 할 것은 산처럼 쌓여있으니 너무 걱정하지 마세요!

다음에는 코드의 복잡도를 알아보는 법과 안정된 의존 관계란 무엇이고 어떻게 좋은 구조의 코드를 생산하는가에 대해 조금 써보겠습니다. 어째 코딩보다 이런게 더 재밌네



Backend developer in Tokyo, Japan

Kazuma Itou 님의 창작활동을 응원하고 싶으세요?

댓글

SNS 계정으로 간편하게 로그인하고 댓글을 남겨주세요.