DIP(Dependency Inversion Principle)

Soobin Kim·2025년 2월 12일

공부

목록 보기
15/15

✏️    ""추상화에 의존해야지, 구체화에 의존하면 안 된다."
   최근, "도메인 주도 개발 시작하기"를 통해 DIP에 대한 개념을 접했고, 이에 대한 내용을 정리하고자 블로그 글을 작성한다.


1. 핵심 개념

DIP

  • DIP(Dependency Inversion Principle)는 객체 지향 프로그래밍에서 SOLID 원칙 중 하나로, 고수준 모듈이 저수준 모듈에 의존하는 대신, 추상화된 인터페이스에 의존하도록 만들어 시스템의 유연성과 확장성을 높이는 것을 목표로 한다.
  • 이 원칙은 구체적인 구현이 아닌 추상화(인터페이스나 추상 클래스를 통해)에 의존하도록 하여, 코드의 유연성을 극대화하고, 변경에 강한 구조를 제공한다. 이를 통해, 구현 세부 사항이 변경되더라도 시스템 전체에 미치는 영향을 최소화할 수 있다.

DIP의 핵심 규칙:

  • 고수준 모듈은 저수준 모듈에 의존하지 않는다.
  • 고수준과 저수준 모듈 모두 추상화(인터페이스)에 의존해야 한다.

고수준 모듈

  • 고수준 모듈은 실제 비즈니스 로직을 담당하는 모듈로, 예를 들어 CustomerService와 같이 비즈니스 로직을 처리한다. 이 모듈은 구체적인 데이터 접근 구현체에 의존하지 않고, 추상화된 인터페이스를 통해 작업을 처리한다.

저수준 모듈

  • 저수준 모듈은 데이터 접근과 같은 하위 기능을 실제로 구현한 모듈로, 예를 들어 CustomerRepository와 같은 RDBMS를 사용하는 데이터 접근 구현체를 의미한다. 고수준 모듈과의 의존성을 끊기 위해 인터페이스를 통해 연결된다.

2. DIP의 적용과 유연성

  • 고수준 모듈이 저수준 모듈을 직접 사용하게 되면, 구현 교체가 어려워진다. 예를 들어, 데이터베이스를 MySQL에서 PostgreSQL로 변경하려면 고수준 모듈도 함께 수정해야 할 수 있다.
  • DIP는 이를 해결한다. 고수준 모듈은 인터페이스에 의존하고, 저수준 모듈은 이 인터페이스를 구현한다. 이를 통해 구체적인 구현을 교체하더라도 고수준 모듈에는 영향을 주지 않으며, 시스템을 유연하게 유지할 수 있다.

3. 실제 구현 예시

// 1. 인터페이스 정의
public interface CustomerRepository {
    Customer findById(Long id);
    void save(Customer customer);
}

// 2. 저수준 모듈 구현 (JpaRepository)
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaCustomerRepository extends JpaRepository<Customer, Long> {
    // JpaRepository에서 기본적인 CRUD 메서드가 제공된다.
}

// 3. CustomerRepository 구현 (고수준 모듈이 의존하는 부분)
public class CustomerRepositoryImpl implements CustomerRepository {
    private final JpaCustomerRepository jpaCustomerRepository;

    public CustomerRepositoryImpl(JpaCustomerRepository jpaCustomerRepository) {
        this.jpaCustomerRepository = jpaCustomerRepository;
    }

    @Override
    public Customer findById(Long id) {
        return jpaCustomerRepository.findById(id)
                .orElseThrow(() -> new ApiBusinessException(CustomerError.NOT_EXIST));
    }

    @Override
    public void save(Customer customer) {
        jpaCustomerRepository.save(customer);
    }
}

// 4. 고수준 모듈 구현 (서비스 클래스)
public class CustomerService {
    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void registerCustomer(Customer customer) {
        customerRepository.save(customer);
    }

    public Customer getCustomer(Long id) {
        return customerRepository.findById(id);
    }
}

// 5. 사용 예시
public class Main {
    public static void main(String[] args) {
        JpaCustomerRepository jpaCustomerRepository = new JpaCustomerRepository(); // 실제 구현체는 DI를 통해 주입받는다.
        CustomerRepository customerRepository = new CustomerRepositoryImpl(jpaCustomerRepository);
        CustomerService customerService = new CustomerService(customerRepository);

        Customer newCustomer = new Customer("John Doe");
        customerService.registerCustomer(newCustomer);

        Customer retrievedCustomer = customerService.getCustomer(newCustomer.getId());
        System.out.println("고객 이름: " + retrievedCustomer.getName());
    }
}

설명:

  • CustomerRepository 인터페이스는 여전히 고수준 모듈인 CustomerService에 의해 의존되고 있으며, 실제 구현체인 CustomerRepositoryImpl에서는 JpaCustomerRepository를 통해 데이터베이스를 처리한다.
  • CustomerRepositoryImpl은 데이터 접근에 필요한 예외 처리나 추가적인 로직을 처리할 수 있다.
  • 고수준 모듈인 CustomerServiceCustomerRepository 인터페이스에만 의존하므로, 구현체 변경 시 CustomerService는 수정하지 않고도 다른 데이터 접근 구현체로 쉽게 교체할 수 있다. 예를 들어, JpaCustomerRepositoryMongoCustomerRepository로 변경해도 CustomerService는 영향을 받지 않는다.

4. 테스트 용이성

  • DIP를 적용하면, 실제 구현체 없이 테스트를 수행할 수 있어 단위 테스트가 용이해진다.
  • Mock 객체나 테스트용 구현체를 사용해 빠르고 간단한 테스트가 가능하다. 예를 들어, 아래와 같이 테스트용 구현체를 만들어 단위 테스트를 할 수 있다.
// 테스트용 Mock 구현체
public class MockCustomerRepository implements CustomerRepository {
    private Map<Long, Customer> database = new HashMap<>();

    @Override
    public Customer findById(Long id) {
        return database.get(id);
    }

    @Override
    public void save(Customer customer) {
        database.put(customer.getId(), customer);
    }
}

// 단위 테스트
public class CustomerServiceTest {
    @Test
    public void testRegisterCustomer() {
        CustomerRepository mockRepository = new MockCustomerRepository();
        CustomerService service = new CustomerService(mockRepository);

        Customer newCustomer = new Customer("Jane Doe");
        service.registerCustomer(newCustomer);

        Customer retrievedCustomer = service.getCustomer(newCustomer.getId());
        assertEquals("Jane Doe", retrievedCustomer.getName());
    }
}
  • 위 예시에서는 MockCustomerRepository라는 테스트용 구현체를 사용하여 CustomerService의 기능을 테스트한다. 실제 데이터베이스 연결 없이 테스트를 수행할 수 있으므로, 단위 테스트가 훨씬 간단하고 빠르게 이루어진다.

5. 아키텍처 관점의 DIP

  • DIP는 인프라스트럭처, 응용, 도메인 영역 간의 의존 관계에서도 유용하다. 예를 들어, 이메일 알림 기능(EmailNotifier)이나 주문 처리 기능(OrderService)에서 각 영역을 인터페이스를 통해 분리하는 것이 좋은 예이다.
  • 이러한 아키텍처에서는 기능 변경이나 확장 시 다른 영역에 미치는 영향을 최소화할 수 있다.

6. 주의사항

  • DIP는 항상 최선의 선택이 아닐 수 있다. 시스템의 크기나 복잡도에 따라 적절히 적용해야 한다. 작은 프로젝트나 단순한 시스템에서는 DIP 적용이 오히려 복잡도를 증가시킬 수 있다.
  • 과도한 추상화는 오히려 시스템을 복잡하게 만들 수 있으므로, 상황에 맞는 판단이 필요하다.

✏️    DIP의 적용을 통해 코드의 유연성과 테스트 용이성을 높일 수 있으며, 시스템의 결합도를 낮추고 응집도를 높이는 데 기여할 수 있다. 이 원칙을 잘 이해하고 활용하면, 더 견고하고 유지보수가 용이한 소프트웨어를 개발할 수 있을 것이다.

0개의 댓글