Study/객체지향

DIP: 의존성 역전 원칙

voider 2021. 7. 31. 23:29

의존성 역전 원칙에서 말하는 '유연성이 극대화된 시스템'이란 소스 코드 의존성이 추상abstraction에 의존하며 구체concretion에는 의존하지 않는 시스템이다.

자바와 같은 정적 타입 언어에서 이 말은 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻이다. 구체적인 대상에 절대로 의존하지 말아야 한다.

Ruby나 Python 같은 동적 타입 언어에도 이 규칙이 동일하게 적용된다. 소스 코드 의존 관계에서 구체 모듈은 참조하면 안 된다. 하지만 이들 언어의 경우 구체 모듈이 무엇인지 정의하기 다소 어렵다. 호출할 함수가 구현된 모듈이라면 참조하지 않기가 특히 어렵다.

규칙으로서 DIP는 비현실적이다. 소프트웨어 시스템이라면 구체적은 많은 장치에 반드시 의존할 수밖에 없기 때문이다. 예를 들어 자바에서 String은 구체 클래스이며, 이를 애써 추상 클래스로 만들려는 시도는 현실성이 없다. java.lang.String 구체 클래스에 대한 소스 코드 의존성은 벗어날 수 없고 벗어나서도 안 된다.

String 클래스는 매우 안정적이다. String 클래스가 변경되는 일은 거의 없으며, 있더라도 엄격하게 통제된다. 프로그래머는 String 클래스가 변경될까봐 두려움에 떨 필요가 없다.

이런 이유로 DIP를 말할 때, 운영체제나 플랫폼 등 안정성이 보장되는 환경은 무시한다. 즉 DIP에서 말하는 구체적인 것에 의존하지 말라는 뜻은, '변할 가능성이 높은' 구체적인 것에 의존하지 말라는 의미로 해석할 수 있다.

안정된 추상화

추상 인터페이스에 변경이 생기면 이를 구체화한 구현체도 따라서 수정해야 한다. 반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 항상, 좀 더 정확히 말하면 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.

실제로 뛰어난 소프트웨어 설계자라면 인터페이스의 변동성을 낮추기 위해 애쓴다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력한다. 이것이 소프트웨어 설계의 기본이다.

즉, 안정된 소프트웨어 아키텍처란, 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호해야 한다. 이 원칙에서 전달하려는 내용을 구체적인 코딩 실천법으로 요약할 수 있다.

DIP를 위한 코딩 꿀팁

  • 변동성이 큰 구체 클래스를 참조하지 않기
  • 대신 추상 인터페이스를 참조하면 된다. 이 규칙은 언어가 정적 타입이든, 동적 타입이든 간 관계없이, 모두 적용 가능하다. 또한 이 규칙은 객체 생성 방식을 강하게 제약하고, 일반적으로 추상 팩토리를 사용하도록 강제한다.
  • 변동성이 큰 구체 클래스로부터 파생하지 않기
  • 이 규칙은 이전 규칙을 따르는 규칙이다. 정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 상속은 아주 신중하게 사용해야 한다. 동적 타입 언어라면 문제가 덜 되지만, 의존성을 가진다는 사실에는 변함없다. 따라서 신중에 신중을 거듭하는 게 가장 현명한 선택이다.
  • 구체 함수를 오버라이드 하지 않기
  • 대체로 구체함수는 소스 코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다. 이런 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들이 각자의 용도에 맞게 오버라이딩해야 한다.
  • 구체적이며 변동이 크다면 절대로 그 이름을 언급하지 말아야 한다

팩토리

이 규칙을 준수하려면 변동이 큰 구체적인 객체는 특별히 주의해서 생성해야 한다. 사실상 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다.

자바 등 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용한다.

아래 사진은 추상 팩토리를 사용한 구조다. Application 은 Service 인터페이스를 통해 ConcreteImpl 을 사용하지만, Application 은 어떤 식으로든 ConcreteImpl 의 인스턴스를 생성해야 한다. ConcreteImpl 에 대해 소스 코드 의존성을 만들지 않으면서 이 목적을 이루기 위해 Application 은 ServiceFactory 인터페이스의 makeSvc 메서드를 호출한다. 이 메서드는 ServiceFactory 로부터 파생된 ServiceFactoryImpl 에서 구현된다. 그리고 ServiceFactoryImpl 구현체가 ConcreteImpl 의 인스턴스를 생성한 후 Service 타입으로 반환한다.

사진의 곡선은 아키텍처 경계를 뜻한다. 이 곡선은 구체적인 것들로부터 추상적인 것들을 분리한다. 소스 코드 의존성은 해당 곡선과 교차할 때 모두 한 방향, 즉 추상적인 쪽으로 향한다.

곡선은 시스템을 두 가지 컴포넌트로 분리한다.

  • 추상 컴포넌트
  • 구체 컴포넌트

추상 컴포넌트는 애플리케이션의 모든 고수준 업무 규칙을 포함한다. 구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.

제어흐름은 소스 코드 의존성과 정반대 방향으로 곡선을 가로지른다. 말하자면 소스 코드 의존성은 제어흐름과는 반대 방향으로 역전된다. 이런 이유로 이 원칙을 Dependency Inversion이라고 부른다.

구체 컴포넌트

위 사진의 구체 컴포넌트에는 구체적인 의존성이 하나(ServiceFactoryImpl ⇒ ConcreteImpl ) 있고, 따라서 DIP를 위배한다. 이것은 일반적이다. 모든 DIP 위배를 없앨 수 없다. 하지만 DIP를 위배하는 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고,이를 통해 시스템의 나머지 부분과는 분리할 수 있다.

대다수의 시스템은 이러한 구체 컴포넌트를 최소한 하나는 포함한다. 흔히 이 컴포넌트를 메인이라고 부르는데, main 함수를 포함하기 때문이다. 위 사진 같은 경우라면 main 함수는 ServiceFactoryImpl 의 인스턴스를 생성한 후, 이 인스턴스를 ServiceFactory 타입으로 전역 변수에 저장할 것이다. 그런 다음, Application 은 이 전역 변수를 이용하여 ServiceFactoryImpl 의 인스턴스에 접근할 것이다.

결론

DIP는 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙이다. 의존성은 사진1에서 보듯, 곡선을 경계로 더 추상적인 엔티티가 있는 쪽으로만 향한다.

'Study > 객체지향' 카테고리의 다른 글

ISP: 인터페이스 분리 원칙  (0) 2021.07.31
LSP: 리스코프 치환 원칙  (0) 2021.07.13
OCP: 개방 폐쇄 원칙  (0) 2021.07.10
SRP: 단일 책임 원칙  (0) 2021.07.07