이제 SOLID의 마지막 원칙인 의존관계 역전 원칙Dependency Inversion Principle에 대해 알아볼 차례이다. 이전 원칙들을 설명하는 과정에서 재사용성이나 유연성, 강건성 등을 반복적으로 다뤄 보았기에(특히 예제와 함께) 내용이 좀 더 쉽게 와닿지 않을까 기대한다.

의존관계 역전 원칙이란?

“A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.”

“B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.”

Robert Martin. ISP: The Dependency Inversion Principle

상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안되며, 두 모듈 모두 추상화에 의존해야 한다. 또한 추상화는 세부 내용에 의존해서는 안되고, 세부 내용은 추상화에 의존해야 한다. 이렇게 하면 유연성flexibility, 내구성durability, 유동성mobility이 높아진다.

SOLID의 앞선 원칙들을 이해하고 있다면 이 설명이 그리 어렵지 않을 것이다. 하지만 LSP와 OCP와의 구분이 다소 모호하고, 왜 역전inversion이라는 용어로 DIP를 설명하고 있는지 궁금하다. 이 질문들에 답하기에 앞서 원칙을 제대로 이해하고 있는지 예제를 통해 먼저 확인해 보도록 하자.

예제를 통한 이해

four

그림 1. 복사 프로그램 (출처: https://goo.gl/BVZajj)

위 그림은 Copy라는 모듈이 ReadKeyboardWritePrinter라는 모듈에 의존하고 있음을 나타낸다. CopyReadKeyboard의 반환 값을 WriterPrinter에게 넘겨 주어 키보드로 읽은 내용을 프린터로 출력한다. 코드는 다음과 같다.

public class Copy {

   private ReadKeyboard keyboard;
   private WritePrinter printer;

   public void copy() {
       printer.write(keyboard.read());
   }
}

무슨 문제가 있는 걸까? 요구사항을 잘 만족시키는 이 코드가 지금 당장 아무런 문제 없어 보일지 모르지만 2가지 잠재적 문제를 가지고 있다.

  • Copy가 다루는 협력(또는 행위)을 다른 문맥context에 재사용 할 수 없다.
  • 입력이나 출력에 관한 새로운 행위가 추가되거나 제거될 때 Copy가 함께 변경된다.

먼저, Copy가 다루는 협력을 다른 문맥에 재사용 할 수 없다는 문제부터 이야기 해보자. 여기서 오해 하지 말아야 할 것은 ReadKeyboardWritePrinter는 충분히 재사용 가능하다는 것이다. ReadKeyboard는 키보드 입력에 관한 것을 캡슐화 해 두었기 때문에 이러한 입력이 필요한 곳에 이 객체를 다시 사용할 수 있다. WritePrinter도 마찬가지이다. 다만, 여기서 재사용이 어려운 것은 Copy가 다루는 협력이다. Copy는 다른 모듈로부터 입력을 받고 이를 출력하는 모듈로 넘긴다. 여기서 ‘입력 모듈의 결과를 출력 모듈로 넘긴다’라는 이 협력은 오직 ReadKeyboardWritePrinter에게만 적용 가능하다. 만약 이러한 입출력 처리가 ReadMouseWriteFax 등의 객체에서도 발생한다면, 기존 Copy 대신 MouseFaxCopy라는 새로운 객체를 만들어야 할지도 모른다.

다음으로, 입출력에 관한 행위가 추가되거나 제거될 때 Copy가 함께 변경되는 문제를 살펴보자. 만약 키보드 입력의 결과를 파일에도 출력해야 한다면 어떻게 해야 할까? CopyWritePrinter에 여전히 의존하고 있는 경우라면, 다음과 같이 코드를 작성할 수 있다.

public class Copy {

   private ReadKeyboard keyboard;
   private WritePrinter printer;
   private WriteFile file;

   public void copy(boolean isPrinterWrite) {
       if (isPrinterWrite) {
           printer.write(keyboard.read());
       } else {
           file.write(keyboard.read());
       }
   }
}

이 상태에서 새로운 출력 객체가 또 다시 추가되야 한다면? 새로운 입력 객체가 생긴다면? 기존 PrinterWrite가 없어진다면? 위 코드에서는 이러한 변화가 일어날 때 마다 Copy가 함께 수정되어야 한다. 수정이 한 곳에서만 일어난다면 그나마 다행이다. 함께 변경되야 하는 부분이 언제 어디에 추가 될지는 예측하기 어렵다.

왜 이런 문제가 발생했을까? Copy가 너무 세부적인 것들에 의존하기 때문이다. Copy를 아래 그림처럼 추상화 된 것에 의존하도록 변경해 보자.

four

그림2. 추상화된 것에 의존하는 Copy 모듈 (출처: https://goo.gl/BVZajj)

SOLID에 대한 앞선 글들을 읽어 보았다면 개선된 코드를 충분히 예상할 수 있을 것이다. 코드는 생략하고 무엇이 좋아졌는지를 이야기 해보자. 우선, Copy의 행위를 다른 문맥, 즉 다른 객체들과의 관계에도 적용할 수 있다. 예를 들어, Copy의 행위를 KeyboardReaderPrinterWriter 대신 VoiceReader(Reader 구현체)와 DisplayWriter(Writer 구현체)에도 그대로 적용할 수 있다. 입력과 출력의 협력을 다른 객체들에 재사용할 수 있는 것이다. 또한 CopyReaderWriter라는 추상화에 의존하므로, 이들의 구현체(세부 사항)가 추가되거나 변경될 때 더 이상 직접적인 영향을 받지 않게 된다.

LSP, OCP와의 관계

DIP를 보고 나면 ‘LSP, OCP와 무엇이 다른가’라는 의문이 생길 수 있다. 따라서 이들과의 관계를 정리해 볼 필요가 있다. 우선 OCP와 LSP의 정의를 떠올려 보자.

  • OCP: 확장에 열려 있고 수정에 닫혀 있어야 한다.
  • LSP: 기반 클래스의 사용자는 파생 클래스의 존재를 몰라도 기반 클래스를 사용할 수 있어야 한다.

우선, OCP는 DIP가 가져오는 효과라고 할 수 있다. 추상화에 의존하는 DIP를 적용함으로써, 확장에는 열려 있고 수정에는 닫혀 있는 OCP를 이룰 수 있기 때문이다. LSP와 OCP의 관계도 유사하다. LSP가 깨지면 OCP도 깨진다. 마지막으로 DIP와 LSP의 관계는 어떠한가? 이를 정의하는 것이 쉽지는 않지만 개인적으로 이렇게 생각한다. DIP는 기존의 의존 관계를 뒤집으라는 메시지 전달 의도가 강하고, LSP는 이러한 구조에서 저지르기 쉬운 오류를 짚어 주는 느낌이다. 또한 DIP가 LSP에 비해 좀 더 포괄적이고 일반화 된 관계로 보이기도 한다. 참고로, 로버트 마틴은 LSP, OCP를 적용하여 얻어진 구조를 일반화 할 수 있고 이를 DIP라고 부른다고 했다.

The structure that results from rigorous use of these principles can be generalized into a principle all by itself. I call it “The Dependency Inversion Principle” (DIP).

Robert Martin. ISP: The Dependency Inversion Principle

왜 역전인가?

DIP를 이해하고 난 후에도, 왜 역전inversion이라는 용어가 사용 되었는지는 여전히 의문이다. 이에 대해 로버트 마틴은 이렇게 이야기한다.

One might question why I use the word “inversion”. Frankly, it is because more traditional software development methods, such as Structured Analysis and Design, tend to create software structures in which high level modules depend upon low level modules, and in which abstractions depend upon details.

Robert Martin. ISP: The Dependency Inversion Principle

요약하면, 상위 수준의 모듈이 하위 수준의 모듈에 의존하는 경향이 기존의 전통적 개발 방식에서 두드러지기 때문이라고 한다. 이러한 구조를 뒤집으라는 의도에서 역전inversion이라는 용어가 사용된 것이다. 하지만 처음부터 객체 지향 언어와 설계를 경험한 사람들에게 이 용어가 과연 이해를 돕는가에 대해서는 의문이다. 제어의 역전Inversion of Control이라는 용어에 익숙하다면 얘기가 다르겠지만 말이다.

마무리 하며

그 동안 다소 부족한 글이긴 했지만 SOLID의 다섯 가지 원칙 모두를 알아보았다. 지금까지 SOLID가 무엇인지 그 자체에 집중 했었다면, 이제는 다시 SOLID가 해결하려는 문제가 뭐였는지를 되돌아 볼 필요가 있다. 이는 결국 OOP의 의존성 관리에 관한 것이었고 재사용성reusability, 강건함robustness, 유연함flexibility을 위한 노력이었다. 하지만 소프트웨어 개발에 있어 이 가치가 절대적인 것은 아니며, 이 가치를 위한 노력이 SOLID만 존재하는 것도 아니다. 우리가 지금 해결하려는 문제가 무엇인지 생각해야 하고 문제를 해결하기 위한 수단의 한 가지로 SOLID를 이해해야 한다. 적어도 SOLID를 통해 고민했던 내용들이 코드를 바라보는 힘을 길러주고 문제 해결을 위한 아이디어가 될 것임은 분명하다.

참고 문헌

[1] Robert Martin. DIP: The Dependency Inversion Principle