우리가 만들고 있는 소프트웨어가 점점 살을 붙이며 복잡해지면 흔히 마주하는 문제?고민중 하나는 아마 다음과 같지 않을까 싶다:
의존성
class Foo(val bar: Bar)
단어나 위 코드에서 유추 할 수 있듯이 그냥 심플하게 생각하면 A가 B에게 의존한다
. 그 이상 그 이하의 의미도 없다. 조금 더 쉽게 실생활에서의 예를 들어보자면
아이들은 모르는게 있으면 엄마한테 물어봐야한다
자동차는 기름이 있어야 움직인다
이 서류에 김부장님 서명이 있어야 해당 물건을 발주 할 수 있다.
라고 생각하면 될 것 같다. 즉 이렇게 일반 생활에서 물체와 물체와의 의존성 쉽게 발견 할 수 있다.
그리고 이러한 의존성이 딱히 문제가 되어보이지는 않는다. 글을 쓰는 순간에도 문제가 되었던점이 딱히 생각이 나지 않는다.
그럼에도 불구하고 의존성
이 문제가 되는 경우는 언제가 있을까?
결국에는 코딩을 할때도 의식적이든 무의식적이든 사용하고, 실생활에서도 자주 마주 할 수있는 이 의존성의 가지고 있는 숨겨진 면모는 무엇일까를 자세히 생각해보면 답답함
이라고 말하는게 나한테는 이해하기 쉽게 다가온다.
무슨 말이냐면, 위에 들었던 예시를 조금 비판적으로 바라보면
아이들은 모르는게 있으면 엄마한테 물어봐야한다
-> 아빠는 안되나요? 삼촌은요?
자동차는 기름이 있어야 움직인다
-> 전기차는요?
이 서류에 김부장님 서명이 있어야 해당 물건을 발주 할 수 있다.
-> 지금 김부장님 휴가라서 일주일 뒤에 오는데, 물건은 당장 내일 발주가 나가야하는데??
결국 이러한 예시에서 유추 할 수 있는 결론은 의존성의 숨겨진 모습은
상황에 유연하게 대처하기가 힘들어 질 수 있다.
로 결론 내릴 수 있을거같다. 그리고 또 하나 내릴수 있는 결론은
1. 상황에 따라 유연하게 대처를 해야 할 때 특정 대상에 대한 의존성을 줄이자.
2. 딱히 유연하게 할 이유가 없거나, 무조건 의존성을 지정해야 한다면 노빠꾸로 하자.
위 세개의 예시중에 자동차를 코드를 통해 들여다보면 아래와 같다.
//코틀린의 경우 아래 코드처럼 생성자 및 매개변수를 바로 선언 가능합니다.
class HyundaiGrandeur(val oil: Oil) {
...
}
해당 클래스(현대그랜져가)가 전기도 동력원으로 사용한다면
//코틀린의 경우 아래 코드처럼 생성자 및 매개변수를 바로 선언 가능합니다.
class HyundaiGrandeur(val source: PowerSource) {
...
}
interface PowerSource {
...
}
class Oil: PowerSource {
...
}
class Electric: PowerSource {
...
}
위 예시를 통해 확인 할 수 있는 알 수있는 점은
1. 현대그랜져 클래스는 더 이상 기름(휘발유)만으로 옵션이 아니다
2. 이제는 전기로도 갈 수 있다.
4. 그러기 위해서는 interface를 통해 추상화 + 다형성을 활용해야 한다.
그러면 이제 현대 그랜져는 코드상으로 더 이상 휘발유라는 동력원에 의존하지 않는다. 그리고 동력원이 어떤 것이든지 상관이 없어진다.
fun main() {
val normalVehicle = HyundaiGrandeur(Oil())
val electricVehicle = HyundaiGrandeur(Electric())
}
처음에 언급했던 세가지 경우를 유연함을 적용한다는 가정하에 다시 바꿔보자면
아이들은 모르는게 있으면 <연장자>한테 물어봐야한다
자동차는 <동력원>이 있어야 움직인다
이 서류에 <부장이상의 상급자> 서명이 있어야 해당 물건을 발주 할 수 있다.
조금은 더 상황에 맞게 대처가 가능해진느낌이다. 그리고 그 유연성을 주기 위해 interface를 통해 휘발유와 기름의 공통적인 성질인 동력원
을 일반화
하여 사용 하였다는 것을 명심해야한다.
하지만 여전히 상황에 맞게 유연한 대처가 가능하다는 것이지 의존을 한다는 사실은 변하지 않는다.
바로 전에 언급한 자동차와 동력원의 관계를 살펴보자.
//코틀린의 경우 아래 코드처럼 생성자 및 매개변수를 바로 선언 가능합니다.
class HyundaiGrandeur(val source: PowerSource) {
...
}
...
fun main() {
val normalVehicle = HyundaiGrandeur(Oil())
val electricVehicle = HyundaiGrandeur(Electric())
}
우리는 자동차의 개체를 생성
할때 해당 개체가 어떤 동력원을 사용할지 명시한다.
더 쉽게 얘기하면 어떤 클래스에 포함된 멤버변수를 언제 세팅하니?
에 대한 여러가지 방법이 DI, 즉 의존성 주입이라고 생각하면 된다.
접근은 크게 두가지로 나뉜다고 생각하는데
1. 개체의 생성시(생성자를 통해) 주입
2. 메서드(setter)를 통한 주입
이 두가지로 나뉘는데
OOP의 개체는 생성시부터 유효한 상태(멤버 변수의 유의미한 값)를 가져야한다
라는 관점에서 보았을때는 생성자를 통한 주입이 옳은 방법이라고 생각한다. 그리고 사실 많은 개발자분이 생성자를 통해 구현한다고 들었다.
하지만 해당 의존성이 생성시부터 유효한 상태(멤버 변수의 유의미한 값)를 가지지 않아도 된다고 생각한다면 setter 메서드를 통해서 구현하여도 상관없다고 생각한다. 상황에 맞게 잘쓰면 좋지 않을까?
뭘까 얘는??? 미안하지만 이미 봤다. 내가 갑자기 이상한 추상화니 다형성이니 뭐니 하면서 은근슬쩍 interface로 바꾼 부분을 주목한다면 말이다.
현대그랜져 관련 코드를 다시 보면
//코틀린의 경우 아래 코드처럼 생성자 및 매개변수를 바로 선언 가능합니다.
class HyundaiGrandeur(val source: PowerSource) {
...
}
interface PowerSource {
...
}
class Oil: PowerSource {
...
}
class Electric: PowerSource {
...
}
fun main() {
val normalVehicle = HyundaiGrandeur(Oil())
val electricVehicle = HyundaiGrandeur(Electric())
}
더 이상 현대그랜져는 휘발유에만 의존하게 코드를 작성하지 않고 휘발유를 추상화한 PowerSource
를 참조하고 휘발유(Oil)과, 전기(Electric)이 PowerSource
를 상속(inherit)받아 처리(Dependency Inversion)하였다.
- 고차원 모듈(현대 그랜저)은 저차원 모듈(휘발유, 전기)에 의존하면 안된다. 이 모듈 모두 다른 추상화된 것(동력원)에 의존해야 한다.
- 추상화 된 것(동력원)은 구체적인 것(휘발유, 전기)에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다
DI는 여기서 더 나아가 추상화(동력원)를 구현한 구현체(implementation, concrete class, 휘발유, 전기)를 현대그랜저 개체를 생성할때 같이 넣어준것 뿐이다.
control은 우리나라 말로 제어
라고 한다. inversion은 전에서도 보았듯이 역전? 역이라는 의미를 지니는데 도대체 뭐에 대한 컨트롤을 말하는 걸까?
개발을 하면서 오픈소스 라이브러리라든지, 프레임워크를 필연적으로 사용하게 되는데 이 둘을 어떻게 사용하는지를 잘 살펴보면
라이브러리: 우리가 해당 라이브러리의 기능을 사용한다(호출한다)
프레임워크: 프레임워크가 나의 코드를 호출한다
그럼 코드의 호출
의 관점에서 보면 다음과 같다
라이브러리: 내가 코드의 호출 흐름을 제어한다
프레임워크: 프레임워크가 내 코드의 호출 흐름을 제어한다
DI가 적용된 현대그랜저 개체 생성부분을 보자.
fun main() {
val normalVehicle = HyundaiGrandeur(Oil())
val electricVehicle = HyundaiGrandeur(Electric())
}
물론 동력원의 구현체를 작성하고 넣어준건 나
이다. 현대그랜저는 아무것도 하지 않는다. 즉 그랜저 개체를 생성하는 부분(호출자, 또는 클라이언트)에서 현대 그랜저 개체 생성시 들어갈 동력원의 구현체 지정을 컨트롤 하고 있다.
이것이 IoC, 제어의 역전이다.
그리고 이러한 설정의 위임을 맡고있는것이 IoC Container이다.
결국 DI도 IoC의 구현 방법중 하나이다.
다른예시로는 callback도 IoC의 한 예가 될 수 있을것같다.
이런저런 이야기를 했고 결국 소프트웨어의 유연한 설계를 위해 하였다고 하지만, 코드가 조금은 복잡해질수도있다. 유연성을 위해 가독성을 조금은 포기한 경우이기도 하다.
가끔 글들을 읽어보면 무조건 개발단계부터 유연성을, 마치 나중에 분명히 이렇게 될거같으니 초기단계부터 재활용성을 끌어올려서 작성한 글들을 읽은듯한 경우가 있는것같은데, 필요하면 그때 적용하는게 맞지않을까? 라는 생각이 든다.
그리고 마지막으로 시간적 여유가 있으신분들은 마지막 링크 제어의 역전이란?
글을 읽어보시길 강력추천합니다. 물론 제가 이해가 잘되서 그런것도 있지만 제가 이해가 잘되면 다른분들도 이해가 잘 될 것이라 확신합니다.
Dependency Injection Container
Link2
Link3
제어의 역전(Inversion of Control, IoC) 이란?