본문 바로가기
프로그래밍/클린 아키텍처

[클린아키텍처] 3부. 설계 원칙 DIP

by 2KB 2021. 3. 2.

소프트웨어에서 좋은 아키텍처를 정의하는 원칙이 바로 SOLID이다.

SOLID의 원칙의 목적은 중간 수준(=모듈)의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

1. 변경에 유연하다.

2. 이해하기 쉽다.

3. 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.

** 모듈 : 독립적인 개발 단위. 라이브러리 또는 API 인터페이스 등. 단순히 함수와 데이터구조로 구성된 집합인 클래스를 의미하기도 함.


DIP: 의존성 역전 원칙


DIP (The Dependency Inversion Principle)
추상화에 의존해야 하며, 구체화에 의존하면 안 된다.
즉, 하위의 모듈이 상위 모듈에 정의한 추상 타입(인터페이스)에 의존


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

즉, 인터페이스나 추상 클래스와 같은 추상적인 선언을 참조하고, 구체적인 대상에는 절대로 의존하면 안된다는 이야기이다.

우리가 DIP를 통해 의존하지 않도록 피하고자 하는 것은 자주 변경될 수 밖에 없는 변동성이 큰(volatile) 구체적인 모듈이다.

 

안정된 추상화

 추상 인터페이스에 변경이 생기면, 이를 구체화한 구현체들도 따라서 일괄 수정이 필요하다.

 반대로, 구체적인 구현체에 변경이 생기더라도, 인터페이스는 대다수의 경우 변경될 필요가 없다.

 따라서 인터페이스는 구현체보다 변동성이 낮다.

즉, 안정된 소프트웨어 아키텍처는 변동성이 큰 구현체에 의존하는 일을 지향하고, 안정된 추상 인터페이스를 선호하는 아키텍처이다.

DIP는 다음의 매우 구체적인 코딩실천법으로 요약 가능하다.

 

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

   대신, 추상 인터페이스를 참조하라

 

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

 상속은 소스 코드에 존재하는 모든 관계 중에서 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다.

 상위 클래스에 의존성을 가진다는 사실에는 변함이 없으므로 신중에 신중을 거듭하는 게 가장 현명한 선택이다.

 

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

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

 

팩토리


 DIP 원칙을 지키기 위한 위의 코딩실천법을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 한다.

어떠한 객체를 생성하기 위해서는 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다.

따라서, 이러한 바람직하지 못한 의존성 처리를 회피하기 위해 추상 팩토리 사용을 하게 된다.

 

의존성 관리를 위한 추상 팩토리(Abstract Factory) 패턴

Application은 Service 인터페이스를 통해 ConcreateImpl을 사용하기 위해서 어떤식으로든 ConcreteImpl 클래스의 인스턴스를 생성해야 한다. 이러한 코드 의존성을 만들지 않기 위해, ServiceFactory 인터페이스를 만들어 makeSvc 메서드를 호출하게 되고, 이 메서드는 구체화 클래스인 ServiceFactoryImpl 클래스의 makeSvc를 통해 ConcreteImpl 인스턴스를 생성하여 전달하게 된다.

코드로는 다음과 같이 표현될 수 있다.

public class Application {

    private final ServiceFactory serviceFactory;

    public Application(final ServiceFactory serviceFactory) {
        this.serviceFactory = serviceFactory;
    }

    public void executeService() {
        final Service service = serviceFactory.makeSvc();
        service.executeService();
    }


    public interface ServiceFactory {
        Service makeSvc();
    }

    public class ServiceFactoryImpl implements ServiceFactory {

        @Override
        public Service makeSvc() {
            return new ConcreteImpl();
        }
    }

    public interface Service {
        void executeService();
    }

    public class ConcreteImpl implements Service {

        @Override
        public void executeService() {
            /* logic */
        }
    }
}

위의 그림에서 보이는 곡선은 구체적인 것과 추상적인 것들을 분리하는 경계가 된다.

또한 소스 코드의 의존성은 모두 추상적인 클래스를 향하게 되어 있는 반면 제어의 흐름은 구체화 클래스를 통해 제어가 되므로, 소스 코드 의존성과는 정 반대 방향을 가르키게 된다.

따라서 의존성과 제어흐름의 방향이 반대 방향을 띄게 되고, 이러한 이유로 이 원칙을 의존성 역전(Dependency Inversion)이라 부른다.

 

구체 컴포넌트

 물론, 구체 컴포넌트인 ServiceFactoryImpl 클래스에는 ConcreateImpl 클래스에 대한 구체적인 의존성이 잇고, 따라서 DIP에 위배될 수 있지만, 이는 일반적인 일이다. 모든 DIP 위배를 없앨 수는 없지만, DIP를 위배하는 클래스들을 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분을 분리할 수 있다는 것이 중요한 포인트이다.