📖 [11장] DIP: 의존성 역전 원칙

📘 클린 아키텍처 북스터디 정리입니다

📚 도서: 로버트 C. 마틴 《Clean Architecture》
🧑‍💻 목적: 올바른 설계에 대한 감각과 습관을 익히기 위해
🗓️ 진행 기간: 2025년 7월 ~ 매주 2장


✅ 핵심 요약 (Key Takeaways)

이 장의 핵심 문장은?

우리가 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰 구체적인 요소다

저자가 전달하고자 하는 메세지 요약

  • 안정된 소프트웨어 아키텍처를 설계 하기 위해서 변동성이 큰 구현체에 대한 의존은 지양하고, 안정된 추상 인터페이스에 의존해야 함
  • 변동성이 큰 구체적인 객체를 생성할 때는 주의해야 함
  • 의존성 관리를 위해 추상 팩토리 패턴 사용 가능
  • 추상 팩토리 사용 시, 제어 흐름이 소스 코드 의존성과 정반대 방향으로 역전됨

💡 내용 정리

서론

유연성이 극대화된 시스템: 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템

의존의 대상

  • 변동성이 큰 구체적인 요소에 의존하지 않도록 피해야 함

안정된 추상화

인터페이스의 안정성

  • 인터페이스는 구현체보다 변동성이 낮음

안정된 소프트웨어 아키텍처

  • 변동성이 큰 구현체에 의존하는 일은 지양
  • 안정된 추상 인터페이스를 선호하는 아키텍처

안정된 추상화를 위한 실천법

  • 변동성이 큰 구체 클래스 참조 대신 추상 인터페이스 참조
    - 언어와 무관하게 적용되는 규칙
    • 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제함
  • 변동성이 큰 구체 클래스로부터 파생하지 말 것
  • 구체 함수를 오버라이드 하지 말 것
    - 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현
  • 구체적이고 변동성이 크다면 이름 언급하지 말 것

팩토리

  • 변동성이 큰 구체적 객체는 특별히 주의해서 생성해야 함
  • 하지만 사실상 모든 언어에서 객체 생성 시, 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생함
  • 따라서 자바 등 대다수 객체 지향 언어에서는 추상 팩토리를 사용해서 바람직하지 못한 의존성 처리

추상 팩토리를 통한 시스템 분리

  • 추상 컴포넌트: 앱의 고수준 정책(규칙, 흐름) 포함
  • 구체 컴포넌트: 업무 규칙을 다루기 위해 필요한 모든 세부사항(API 호출, DB 연동 등) 포함

추상 팩토리를 통한 의존성 역전

  • 추상 팩토리 사용 시, 소스 코드 의존성이 제어 흐름과 반대방향으로 역전됨

구체 컴포넌트

  • 현실적인 소프트웨어에서는 객체 생성, 외부 API 연동, DB 접근 등에서 구체적인 클래스나 프레임워크를 사용할 수밖에 없으므로, 사실상 DIP 위반을 모두 없앨 수는 없음
  • 예: main 함수를 포함하는 Main 컴포넌트
  • 하지만, DIP 위배 클래스들은 적은 수의 구체 컴포넌트 내부로 모을 수 있고, 이를 통해 시스템의 나머지 부분과 분리 가능
  • 전체 아키텍처가 영향을 받지 않도록 DIP 위반 범위를 좁혀야 함

결론

  • 아키텍처 경계: 추상 컴포넌트와 구체 컴포넌트를 분리
  • 의존성 규칙: 의존성은 아키텍처 경계를 기준으로 더 추상적인 엔티티가 있는 쪽으로 향함

용어 정리

추상 팩토리(Abstract Factory)

  • 객체 생성을 담당하는 인터페이스 계층을 의미
  • 구체 클래스 생성을 클라이언트에서 분리하여, DIP 위반을 방지

DIP 위반 케이스

  • BillingService가 MySQL이라는 구체 기술에 종속됨
public class BillingService {
    private final MySQLBillingRepository repository = new MySQLBillingRepository(); // 구체 의존
}

추상 팩토리를 사용한 리팩토링

  • 고수준 모듈(BillingService)이 구체 모듈(MySQLBillingRepository)에 의존하는 대신, 추상 인터페이스(BillingRepository)에 의존하도록 설계 변경
    → 의존성의 방향을 구체 → 추상으로 역전시킴
구조
  • 고수준 정책: 비즈니스 로직 담당 - BillingService
  • 추상화 계층: 인터페이스 정의 - BillingRepository, BillingRepositoryFactory
  • 저수준 구현: 실제 구현체 - MySQLBillingRepository, MySQLBillingRepositoryFactory
// 1. 인터페이스 정의
public interface BillingRepository {
    void save(Invoice invoice);
}

// 2. 구체 클래스
public class MySQLBillingRepository implements BillingRepository {
    public void save(Invoice invoice) {
        // 실제 MySQL DB 저장
    }
}

// 3. 추상 팩토리
// BillingRepositoryFactory는 BillingService가 어떤 구현체를 사용하는지 몰라도 되게 해줌
public interface BillingRepositoryFactory {
    BillingRepository create();
}

// 4. 구체 팩토리
public class MySQLBillingRepositoryFactory implements BillingRepositoryFactory {
    public BillingRepository create() {
        return new MySQLBillingRepository();
    }
}


// 5. 클라이언트
public class BillingService {
    private final BillingRepository repository;

    public BillingService(BillingRepositoryFactory factory) {
        this.repository = factory.create();  // 어떤 구현체인지 모름
    }

    public void bill(Invoice invoice) {
        repository.save(invoice);
    }
}

추상팩토리의 한계점

  • 추상 팩토리 패턴은 복잡도가 증가할 수 있다는 단점이 있음
  • 규모가 작거나 DI 프레임워크를 사용하는 환경에서는 팩토리 없이 단순한 생성자 주입 방식이 더 적합할 수 있음

Spring DI

  • 스프링은 자체의 의존성 주입(Dependency Injection, DI) 기능을 통해 추상 팩토리 없이도 DIP 실현 가능

추상 팩토리와 Spring의 DI

항목추상 팩토리 패턴Spring DI (@Autowired, @Bean 등)
객체 생성 책임개발자가 팩토리 클래스 직접 작성Spring이 객체 생성 및 주입 관리
구성 방식명시적으로 Factory.create() 호출설정 클래스를 통해 주입 방식 정의
사용 예시객체 생성 위치를 코드로 분리설정 파일(@Configuration) 또는 자동 주입
복잡도복잡도 ↑ (팩토리 계층 필요)간단함, 생산성 ↑

Spring DI를 통한 DIP 실현

// 1. 인터페이스 정의
public interface BillingRepository {
    void save(Invoice invoice);
}

// 2. 구체 구현체
public class MySQLBillingRepository implements BillingRepository {
    public void save(Invoice invoice) {
        System.out.println("Saving invoice to MySQL");
    }
}

public class OracleBillingRepository implements BillingRepository {
    public void save(Invoice invoice) {
        System.out.println("Saving invoice to Oracle");
    }
}

// 3. 비즈니스 로직 클래스
@Service
public class BillingService {
    private final BillingRepository billingRepository;

    public BillingService(BillingRepository billingRepository) {
        this.billingRepository = billingRepository;
    }

    public void bill(Invoice invoice) {
        billingRepository.save(invoice);
    }
}

// 4. 명시적인 객체 생성을 담당하는 구성 클래스
@Configuration
public class BillingConfiguration {
    @Bean
    public BillingRepository mySQLBillingRepository() {
        return new MySQLBillingRepository();  // 추상 → 구체 주입
    }
    
    @Bean
    public BillingRepository oracleBillingRepository() {
        return new OracleBillingRepository();
    }

    @Bean
    public BillingService billingService(
        @Qualifier("mySQLBillingRepository") BillingRepository repository
    ) {
        return new BillingService(repository);
    }
}

💡 인상 깊었던 문장 & 나의 인사이트

책에서 가장 기억에 남는 문장

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

인상 깊었던 문장과 관련된 인사이트

이 문장은 의존성 역전 원칙(DIP)의 핵심 정신을 잘 담고 있다.

단순히 "구현체에 의존하지 말자"는 메시지를 넘어서,
좋은 소프트웨어 설계를 위해 지향해야 할 방향성을 제시한다.

특히 책에서 설명한 추상 팩토리 패턴을 통한 DIP 실현 예시는
어떻게 의존성의 방향을 '구체 → 추상'으로 역전시킬 수 있는지를 구체적으로 보여준다.

이번 장을 읽으며,
내가 현재 진행 중인 프로젝트에서 사용 중인 Spring 프레임워크에서도
@Bean, @Configuration, @Autowired와 같은 의존성 주입 기법을 통해
DIP 원칙이 자연스럽게 적용되고 있다는 점을 실제 코드와 연관지어 확인할 수 있었다.

앞으로는 클래스 간 의존 구조를 설계할 때,
의존성의 방향성과 추상화 수준을 보다 의식적으로 고려해야겠다는 생각이 들었다.


🛠 실무 적용 아이디어 (To Action)

✅ 오늘부터 실천할 작은 실천

  • 고수준 정책이 인터페이스가 아닌 구현체에 의존하지 않는 지 체크

0개의 댓글