1. 개요

DIP(Dependency Inversion Principle)는 SOLID 로 널리 알려진 객체 지향 프로그래밍 원칙 모음의 일부를 구성합니다 .

본질적으로 DIP는 잘 구조화되고 고도로 분리되어 있으며 재사용 가능한 소프트웨어 구성 요소를 구현하는 데 사용할 수 있는 단순하면서도 강력한 프로그래밍 패러다임입니다 .

이 사용방법(예제)에서는 JPMS (Java Platform Module System)를  사용하여 Java 8과 Java 11에서 각각 하나씩 DIP를 구현하는 다양한 접근 방식을 살펴봅니다 .

2. 의존성 주입 및 제어 반전은 DIP 구현이 아닙니다.

무엇보다도 기본을 올바르게 하기 위해 근본적인 차이점을 만들어 보겠습니다. DIP는 DI(의존성 주입)도 IoC(제어 역전)도 아닙니다 . 그럼에도 불구하고 그들은 모두 함께 훌륭하게 작동합니다.

간단히 말해서, DI는 소프트웨어 구성 요소를 직접 획득하는 대신 API를 통해 의존성 또는 협력자를 명시적으로 선언하도록 만드는 것입니다.

DI가 없으면 소프트웨어 구성 요소가 서로 밀접하게 결합됩니다. 따라서 재사용, 교체, 모의 및 테스트가 어려우며 결과적으로 경직된 디자인이 됩니다.

DI를 사용하면 구성 요소 의존성을 제공하고 개체 그래프를 연결하는 책임이 구성 요소에서 기본 주입 프레임워크로 이전됩니다. 그런 관점에서 DI는 IoC를 달성하는 방법일 뿐입니다.

반면에 IoC는 애플리케이션 흐름의 제어가 역전되는 패턴입니다 . 기존의 프로그래밍 방법론에서는 Custom형 코드가 애플리케이션의 흐름을 제어합니다. 반대로 IoC를 사용하면 제어가 외부 프레임워크 또는 컨테이너로 전송됩니다 .

프레임워크는 우리 자신의 코드를 연결하기 위한 후크 포인트를 정의하는 확장 가능한 코드베이스입니다 .

차례로 프레임워크는 인터페이스의 구현을 사용하고 어노테이션을 통해 하나 이상의 특수 하위 클래스를 통해 코드를 다시 호출합니다. Spring 프레임워크는 이 마지막 접근 방식의 좋은 예입니다.

3. DIP의 기초

DIP의 배후에 있는 동기를 이해하기 위해 Robert C. Martin이 그의 저서 Agile Software Development: Principles, Patterns, and Practices 에서 제공한 공식적인 정의부터 시작하겠습니다 .

  1. 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
  2. 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 따라 달라집니다.

따라서 DIP의 핵심은 상위 수준 구성 요소와 하위 수준 구성 요소 사이의 상호 작용을 추상화하여 기존 의존성을 반전시키는 것입니다 .

전통적인 소프트웨어 개발에서 높은 수준의 구성 요소는 낮은 수준의 구성 요소에 의존합니다. 따라서 높은 수준의 구성 요소를 재사용하기가 어렵습니다.

3.1. 디자인 선택 및 DIP

StringReader 구성 요소를 사용하여 문자열 값을 가져오고 StringWriter 구성 요소를 사용하여 다른 곳에 쓰는 간단한 StringProcessor 클래스를 고려해 보겠습니다 .

public class StringProcessor {
    
    private final StringReader stringReader;
    private final StringWriter stringWriter;
    
    public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
        this.stringReader = stringReader;
        this.stringWriter = stringWriter;
    }

    public void printString() {
        stringWriter.write(stringReader.getValue());
    }
}

StringProcessor 클래스 의 구현은 기본이지만 여기에서 선택할 수 있는 몇 가지 디자인이 있습니다.

각 디자인 선택을 별도의 항목으로 나누어 각 항목이 전체 디자인에 어떤 영향을 미칠 수 있는지 명확하게 이해해 보겠습니다.

  1. 하위 수준 구성 요소인 StringReaderStringWriter 는 동일한 패키지에 있는 구체적인 클래스입니다. StringProcessor , 상위 수준 구성 요소는 다른 패키지에 배치됩니다. StringProcessor는 StringReader StringWriter 에 의존합니다. 의존성 반전이 없으므로 StringProcessor는 다른 컨텍스트에서 재사용할 수 없습니다.
  2. StringReaderStringWriter는 구현과 함께 동일한 패키지에 있는 인터페이스입니다 . StringProcessor는 이제 추상화에 의존하지만 하위 수준 구성 요소는 그렇지 않습니다. 우리는 아직 의존성 반전을 달성하지 못했습니다.
  3. StringReaderStringWriter는 StringProcessor 와 함께 동일한 패키지에 있는 인터페이스입니다 . 이제 StringProcessor는 추상화의 명시적 소유권을 갖습니다. StringProcessor,  StringReader StringWriter는 모두 추상화에 의존합니다. 우리는 구성 요소 간의 상호 작용을 추상화하여 위에서 아래로 의존성의 역전을 달성했습니다 . 이제 StringProcessor를 다른 컨텍스트에서 재사용할 수 있습니다.
  4. StringReaderStringWriter는 StringProcessor 와는 별도의 패키지에 있는 인터페이스입니다 . 의존성 반전을 달성했으며 StringReader StringWriter 구현을 교체하는 것도 더 쉽습니다. StringProcessor 는 다른 컨텍스트에서도 재사용할 수 있습니다.

위의 모든 시나리오 중에서 항목 3과 4만 DIP의 유효한 구현입니다.

3.2. 추상화 소유권 정의

항목 3 은 상위 수준 구성 요소와 추상화가 동일한 패키지에 배치되는 직접 DIP 구현입니다 . 따라서 상위 수준 구성 요소는 추상화를 소유합니다 . 이 구현에서 상위 수준 구성 요소는 하위 수준 구성 요소와 상호 작용하는 추상 프로토콜 정의를 담당합니다.

마찬가지로 항목 4는 더 분리된 DIP 구현입니다. 패턴의 이 변형에서 상위 수준 구성 요소와 하위 수준 구성 요소 모두 추상화의 소유권이 없습니다 .

추상화는 별도의 레이어에 배치되어 하위 수준 구성 요소를 쉽게 전환할 수 있습니다. 동시에 모든 구성 요소가 서로 격리되어 더 강력한 캡슐화가 가능합니다.

3.3. 올바른 추상화 수준 선택

대부분의 경우 상위 수준 구성 요소에서 사용할 추상화를 선택하는 것은 매우 간단해야 하지만 주목해야 할 한 가지 주의 사항이 있습니다. 바로 추상화 수준입니다.

위의 예에서는 DI를 사용하여 StringReader 유형을 StringProcessor 클래스 에 주입했습니다 . 이는 StringReader 의 추상화 수준이 StringProcessor 의 도메인에 가까운 한 효과적입니다 .

대조적으로, 예를 들어 StringReader가 파일에서 문자열 값을 읽는 File 객체 라면 DIP의 본질적인 이점을 놓치고 있는 것입니다 . 이 경우 StringReader 의 추상화 수준은 StringProcessor 도메인 수준보다 훨씬 낮습니다 .

간단히 말해서, 상위 수준 구성 요소가 하위 수준 구성 요소와 상호 운용하는 데 사용할 추상화 수준은 항상 전자의 도메인에 가까워야 합니다 .

4. 자바 8 구현

우리는 이미 DIP의 주요 개념을 자세히 살펴보았으므로 이제 Java 8에서 패턴의 몇 가지 실용적인 구현을 살펴보겠습니다.

4.1. 직접 DIP 구현

지속성 계층에서 일부 고객을 가져와 몇 가지 추가 방식으로 처리하는 데모 애플리케이션을 만들어 보겠습니다.

계층의 기본 저장소는 일반적으로 데이터베이스이지만 코드를 단순하게 유지하기 위해 여기서는 일반 Map 을 사용합니다 .

상위 수준 구성 요소를 정의하여 시작하겠습니다 .

public class CustomerService {

    private final CustomerDao customerDao;

    // standard constructor / getter

    public Optional<Customer> findById(int id) {
        return customerDao.findById(id);
    }

    public List<Customer> findAll() {
        return customerDao.findAll();
    }
}

보시다시피 CustomerService 클래스는 간단한 DAO 구현을 사용하여 지속성 계층에서 고객을 가져오는 findById()findAll() 메서드를 구현합니다 . 물론 클래스에 더 많은 기능을 캡슐화할 수 있었지만 단순성을 위해 이와 같이 유지하겠습니다.

이 경우 CustomerDao 유형은 CustomerService가 하위 수준 구성 요소를 소비하는 데 사용하는 추상화 입니다 .

이것은 직접 DIP 구현이므로 동일한 CustomerService 패키지의 인터페이스로 추상화를 정의해 보겠습니다 .

public interface CustomerDao {

    Optional<Customer> findById(int id);

    List<Customer> findAll();

}

상위 수준 구성 요소의 동일한 패키지에 추상화를 배치하여 구성 요소가 추상화 소유를 담당하도록 합니다. 이 구현 세부 사항은 상위 수준 구성 요소와 하위 수준 구성 요소 간의 의존성을 실제로 반전시키는 것 입니다 .

또한 CustomerDao 의 추상화 수준은 CustomerService 의 추상화 수준에 가깝고 좋은 DIP 구현에도 필요합니다.

이제 다른 패키지에 하위 수준 구성 요소를 만들어 보겠습니다. 이 경우 기본 CustomerDao 구현일 뿐입니다.

public class SimpleCustomerDao implements CustomerDao {

    // standard constructor / getter

    @Override
    public Optional<Customer> findById(int id) {
        return Optional.ofNullable(customers.get(id));
    }

    @Override
    public List<Customer> findAll() {
        return new ArrayList<>(customers.values());
    }
}

마지막으로 CustomerService 클래스의 기능을 확인하는 단위 테스트를 만들어 보겠습니다 .

@Before
public void setUpCustomerServiceInstance() {
    var customers = new HashMap<Integer, Customer>();
    customers.put(1, new Customer("John"));
    customers.put(2, new Customer("Susan"));
    customerService = new CustomerService(new SimpleCustomerDao(customers));
}

@Test
public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() {
    assertThat(customerService.findById(1)).isInstanceOf(Optional.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() {
    assertThat(customerService.findAll()).isInstanceOf(List.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() {
    var customers = new HashMap<Integer, Customer>();
    customers.put(1, null);
    customerService = new CustomerService(new SimpleCustomerDao(customers));
    Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer"));
    assertThat(customer.getName()).isEqualTo("Non-existing customer");
}

단위 테스트는 CustomerService API를 실행합니다. 또한 상위 수준 구성 요소에 추상화를 수동으로 주입하는 방법도 보여줍니다. 대부분의 경우 이를 수행하기 위해 일종의 DI 컨테이너 또는 프레임워크를 사용합니다.

또한 다음 다이어그램은 상위 수준에서 하위 수준 패키지 관점까지 데모 애플리케이션의 구조를 보여줍니다.

다이렉트 딥

4.2. 대체 DIP 구현

이전에 논의한 것처럼 다른 패키지에 상위 수준 구성 요소, 추상화 및 하위 수준 구성 요소를 배치하는 대체 DIP 구현을 사용할 수 있습니다.

명백한 이유 때문에 이 변형은 더 유연하고 구성 요소의 더 나은 캡슐화를 제공하며 하위 수준 구성 요소를 쉽게 교체할 수 있습니다.

물론 이 패턴의 변형을 구현하는 것은 CustomerService , MapCustomerDaoCustomerDao 를 별도의 패키지에 배치하는 것으로 귀결됩니다.

따라서 이 구현으로 각 구성 요소가 어떻게 배치되는지 보여주는 다이어그램이면 충분합니다.

대체 딥

5. Java 11 모듈식 구현

데모 애플리케이션을 모듈식 애플리케이션으로 리팩터링하는 것은 상당히 쉽습니다.

이것은 JPMS가 DIP를 통한 강력한 캡슐화, 추상화 및 구성 요소 재사용을 포함하여 최상의 프로그래밍 관행을 시행하는 방법을 보여주는 정말 좋은 방법입니다.

샘플 구성 요소를 처음부터 다시 구현할 필요가 없습니다. 따라서 샘플 애플리케이션을 모듈화하는 것은 각 구성 요소 파일을 해당 모듈 설명자와 함께 별도의 모듈에 배치하는 문제입니다 .

모듈식 프로젝트 구조는 다음과 같습니다.

project base directory (could be anything, like dipmodular)
|- com.baeldung.dip.services
   module-info.java
     |- com
       |- baeldung
         |- dip
           |- services
             CustomerService.java
|- com.baeldung.dip.daos
   module-info.java
     |- com
       |- baeldung
         |- dip
           |- daos
             CustomerDao.java
|- com.baeldung.dip.daoimplementations 
    module-info.java 
      |- com 
        |- baeldung 
          |- dip 
            |- daoimplementations 
              SimpleCustomerDao.java  
|- com.baeldung.dip.entities
    module-info.java
      |- com
        |- baeldung
          |- dip
            |- entities
              Customer.java
|- com.baeldung.dip.mainapp 
    module-info.java 
      |- com 
        |- baeldung 
          |- dip 
            |- mainapp
              MainApplication.java

5.1. 고수준 컴포넌트 모듈

자체 모듈에 CustomerService 클래스를 배치하여 시작하겠습니다 .

루트 디렉토리 com.baeldung.dip.services 에 이 모듈을 생성 하고 모듈 설명자 module-info.java를 추가합니다 .

module com.baeldung.dip.services {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    uses com.baeldung.dip.daos.CustomerDao;
    exports com.baeldung.dip.services;
}

분명한 이유로 JPMS가 작동하는 방식에 대해서는 자세히 설명하지 않겠습니다. 그럼에도 불구하고, require 지시문을 보는 것만으로도 모듈 의존성을 확인하는 것이 분명합니다 .

여기서 주목할 가치가 있는 가장 관련된 세부 사항은 uses 지시문입니다. 모듈이 CustomerDao 인터페이스  의 구현을 사용하는 클라이언트 모듈임을 나타냅니다 .

물론 여전히 이 모듈에 높은 수준의 구성 요소인 CustomerService 클래스를 배치해야 합니다. 따라서 루트 디렉토리 com.baeldung.dip.services 내에서 com/baeldung/dip/services 라는 패키지와 유사한 디렉토리 구조를 생성해 보겠습니다 .

마지막으로 CustomerService.java 파일을 해당 디렉터리에 배치합니다 .

5.2. 추상화 모듈

마찬가지로 자체 모듈에 CustomerDao 인터페이스를 배치해야 합니다 . 따라서 com.baeldung.dip.daos 루트 디렉터리에 모듈을 만들고 모듈 설명자를 추가해 보겠습니다.

module com.baeldung.dip.daos {
    requires com.baeldung.dip.entities;
    exports com.baeldung.dip.daos;
}

이제 com.baeldung.dip.daos 디렉토리로 이동하여 com/baeldung/dip/daos 디렉토리 구조를 생성해 보겠습니다 . CustomerDao.java 파일을 해당 디렉터리에 넣겠습니다 .

5.3. 저수준 컴포넌트 모듈

논리적으로 우리는 하위 수준 구성 요소인 SimpleCustomerDao도 별도의 모듈에 넣어야 합니다. 예상대로 프로세스는 방금 다른 모듈에서 수행한 것과 매우 유사합니다.

루트 디렉터리 com.baeldung.dip.daoimplementations 에 새 모듈을 만들고 모듈 설명자를 포함해 보겠습니다.

module com.baeldung.dip.daoimplementations {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao;
    exports com.baeldung.dip.daoimplementations;
}

JPMS 컨텍스트에서 이는 제공with 지시문을 선언하므로 서비스 제공자 모듈 입니다 .

이 경우 모듈은 SimpleCustomerDao 구현을 통해 하나 이상의 소비자 모듈에서 CustomerDao 서비스를 사용할 수 있도록 합니다 .

우리의 소비자 모듈인 com.baeldung.dip.services 는 uses 지시문을 통해 이 서비스를 사용한다는 점을 명심하십시오 .

이는 다양한 모듈에서 소비자, 서비스 제공자 및 추상화를 정의함으로써 JPMS로 직접 DIP를 구현하는 것이 얼마나 간단한지 명확하게 보여줍니다 .

마찬가지로 이 새 모듈에 SimpleCustomerDao.java 파일을 배치해야 합니다 . com.baeldung.dip.daoimplementations 디렉토리 로 이동하여 com/baeldung/dip/daoimplementations 라는 이름으로 새로운 패키지와 유사한 디렉토리 구조를 생성해 보겠습니다 .

마지막으로 SimpleCustomerDao.java 파일을 디렉터리에 배치합니다 .

5.4. 엔티티 모듈

또한 Customer.java 클래스를 배치할 수 있는 다른 모듈을 만들어야 합니다 . 이전과 마찬가지로 com.baeldung.dip.entities 루트 디렉터리를 만들고 모듈 설명자를 포함합니다.

module com.baeldung.dip.entities {
    exports com.baeldung.dip.entities;
}

패키지의 루트 디렉터리에서 com/baeldung/dip/entities 디렉터리를 만들고 다음 Customer.java 파일을 추가합니다.

public class Customer {

    private final String name;

    // standard constructor / getter / toString
    
}

5.5. 주요 애플리케이션 모듈

다음으로 데모 애플리케이션의 진입점을 정의할 수 있는 추가 모듈을 생성해야 합니다. 따라서 다른 루트 디렉토리 com.baeldung.dip.mainapp를 만들고 그 안에 모듈 설명자를 배치합니다.

module com.baeldung.dip.mainapp {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    requires com.baeldung.dip.daoimplementations;
    requires com.baeldung.dip.services;
    exports com.baeldung.dip.mainapp;
}

이제 모듈의 루트 디렉터리로 이동하여 com/baeldung/dip/mainapp 디렉터리 구조를 생성해 보겠습니다. 해당 디렉터리에서 단순히 main() 메서드를 구현하는 MainApplication.java 파일을 추가해 보겠습니다 .

public class MainApplication {

    public static void main(String args[]) {
        var customers = new HashMap<Integer, Customer>();
        customers.put(1, new Customer("John"));
        customers.put(2, new Customer("Susan"));
        CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers));
        customerService.findAll().forEach(System.out::println);
    }
}

마지막으로 IDE 내에서 또는 명령 콘솔에서 데모 애플리케이션을 컴파일하고 실행해 보겠습니다.

예상대로 애플리케이션이 시작될 때 콘솔에 출력되는 Customer 개체 List을 볼 수 있습니다 .

Customer{name=John}
Customer{name=Susan}

또한 다음 다이어그램은 애플리케이션의 각 모듈 의존성을 보여줍니다.

모듈 의존성 1

6. 결론

이 예제에서는 DIP의 주요 개념에 대해 자세히 살펴보고 JPMS를 사용하는 Java 8 및 Java 11에서 패턴의 다양한 구현을 보여주었습니다 .

Java 8 DIP 구현  및 Java 11 구현에 대한 모든 예제는 GitHub에서 사용할 수 있습니다.

Generic footer banner