Programming/Clean Architecture

11장. DIP: 의존성 역전 원칙

JKROH 2023. 12. 11. 15:38
반응형

의존성 역전 원칙

  • 의존성 역전 원칙에서 말하는 '유연성이 극대화된 시스템'은 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템이다.
  • 자바와 같은 정적 타입 언어에서 이는 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻이다.
  • 그러나 이를 무조건적 규칙으로 따르기는 어렵다. 예를 들어 String과 같은 구체 클래스에 의존하지 않고 이를 억지로 추상화 하는 것은 현실성이 없다.
  • 따라서 의존성 역전 원칙을 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편이다.
  • 우리가 의존하지 않고자 하는 것은 변동성이 큰 구체적인 요소다. 이 구체적인 요소는 우리가 개발을 하는 동안 자주 변경될 수밖에 없는 모듈들이다.

안정된 추상화

  • 추상 인터페이스에 변경이 생기면 이를 구체화 한 구현체들도 따라서 수정해야한다.
  • 그러나 구체 클래스에 변경이 생기더라도 추상 인터페이스가 변경될 가능성은 낮다. 따라서 인터페이스는 구현체보다 변동성이 낮다.
  • 즉, 안정된 소프트웨어 아키텍처란 변동성이 큰 구현제에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻이다.

- 변동성이 큰 구체 클래스를 참조하지 말라.

  • 추상 인터페이스를 참조하자.
  • 이 규칙은 객체 생성 방식을 강하게제약하며, 일반적으로 추상 팩토리를 사용하도록 강제한다.

- 변동성이 큰 구체 클래스로부터 파생하지 말라.

  • 정적 타입 언어에서 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다.
  • 따라서 상속은 아주 조심해서 사용해야한다.

- 구체 함수를 오버라이드 하지 말라.

  • 대체로 구체 함수는 소스코드 의존성을 필요로 한다. 따라서 구체 함수를 오버라이드 하면 이러한 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다.
  • 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.

- 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

  • 위의 말들을 종합하면 이런 결론이 나온다.

팩토리

  • 이 규칙들을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야한다.
  • 사실상 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스코드 의존성이 발생한다.
  • 대다수 객체지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 이용하곤 한다.

  • Application은 Service에 의존하지만, 어쨋든 기능을 사용하려면 구체 클래스인 ConcreteImpl을 만들기는 해야한다.
  • ConcreteImpl에 의존하지 않으면서, ConcreteImpl을 만들기 위해 새롭게 ServiceFactory라는 인터페이스를 만들어 Application이 이에 의존하게 만든다.
  • ServiceFactory를 구현한 ServiceFactoryImpl은 makeSvc() 메서드를 통해 ConcreteImpl 클래스의 인스턴스를 만들고 이를 Service 타입으로 반환한다.
  • 결과적으로 Application은 추상 인터페이스에만 의존하면서 구체 클래스를 만들어낼 수 있다.
  • 소스코드 의존성은 Application이 추상 컴포넌트에 의존한다.
    • Appliation 클래스는 ServiceFactory와 Service에 의존한다. 코드로도 이렇게 표현될 것이다.
  • 그러나 제어 흐름을 보면 사실 Application클래스는 구체 컴포넌트에 의존한다.
    • 결과적으로 ConcreteImpl과 ServiceFactoryImpl의 기능에 의존한다.
  • 이러한 이유로 이 원칙을 의존성 역전이라 부른다.

구체 컴포넌트

  • 위 그림에는 ServiceFacotryImpl이 ConcreteImpl이라는 구체 클래스에 의존하고 있다. 이는 DIP를 위배한다. 그러나, 모든 DIP 위배를 없앤다는 것은 불가능하다.
    • 뭐가 됐든, 어딘가에서는 ConcreteImpl 구체 클래스를 만들어서 Application이 의존하는 Service에 넣어줘야한다.
    • 결국, 어짜피 누군가는 구체 클래스에 의존해야한다.
    • 그렇다면 Application이 직접 Service service = new ConcreteImpl()로 사용하는 것보다는 팩토리에서 대신 만들어주는 것이 훨씬 낫다. 복잡한 모듈인 Application은 추상 클래스에 의존하고, 구체 클래스는 구체 클래스끼리 의존하게만 만들어줄 수 있기 때문이다.
  • 대다수 시스템은 이런 구체 컴포넌트를 메인 함수에서 만들어서 전역으로 설정한다.
  • Spring이라면 Spring Context를 생각하면 될 것이다. main 함수를 담은 Application 클래스를 실행시키면 구체 클래스인 Bean들을 만들어서 Spring Context에 저장하고 이를 이용해 의존성 주입을 해준다.
반응형