티스토리 뷰

IT/SOLID

DIP - 의존성 역전 원칙 (Dependency Inversion Principle)

Stv 2025. 4. 29. 19:32

목차


    1. DIP란?

    DIP (Dependency Inversion Principle)
    "상위 모듈은 하위 모듈에 의존하면 안 된다"
    모두가 추상화(인터페이스)에 의존해야 한다"

     

    📌 DIP의 핵심

    • 고수준 모듈(비즈니스 로직)과 저수준 모듈(구현 세부사항)이 서로 구체적인 것에 의존하지 않고, 공통된 추상화에 의존해야 한다는 원칙이다.

    2. DIP가 필요한 이유

    ❌ DIP를 지키지 않으면

    • 고수준 로직이 저수준 세부 구현에 강하게 결합
    • 구현이 바뀌면 고수준 로직도 수정해야 해서 유지보수성 급격히 저하
    • 코드 재사용과 확장이 어려움

    ✅ DIP를 지키면

    • 고수준 모듈이 구현 변화에 영향받지 않음
    • 새로운 기능 추가/변경 시 기존 로직에 영향 최소화
    • 의존성 관리가 명확해지고 테스트가 쉬워짐

    3. DIP 위반 예시

    public class FileLogger {
        public void write(String message) {
            // 파일에 로그 쓰기
        }
    }
    
    public class OrderService {
        private final FileLogger logger = new FileLogger(); // 직접 생성 및 의존
    
        public void createOrder() {
            logger.write("주문 생성됨");
        }
    }

     

    📉 문제점

    • OrderService가 FileLogger 구체 클래스에 직접 의존
    • FileLogger를 변경하거나 다른 Logger로 바꾸려면 OrderService를 수정해야 함 -> OCP도 위반
    • 테스트 시 Mock 주입이 불가능

    4. DIP를 만족시키는 설계

     

    📌 해결 방법 : 추상화 도입 + 의존성 주입

    public interface Logger {
        void write(String message);
    }
    public class FileLogger implements Logger {
        public void write(String message) {
            // 파일에 로그 쓰기
        }
    }
    
    public class ConsoleLogger implements Logger {
        public void write(String message) {
            // 콘솔에 출력
        }
    }
    public class OrderService {
        private final Logger logger;
    
        public OrderService(Logger logger) { // 생성자 주입
            this.logger = logger;
        }
    
        public void createOrder() {
            logger.write("주문 생성됨");
        }
    }

     

    [OrderService] ---> [Logger 인터페이스] <--- [FileLogger]
                                             <--- [ConsoleLogger]
                                             <--- [MockLogger]
    • 이제 OrderService는 Logger라는 추상화에만 의존
    • FileLogger, ConsoleLogger 구현체가 바뀌어도 OrderService는 변경 없음 -> DIP 만족 ✅
    • 테스트 시 MockLogger로 교체 가능

    5. 실무에서 자주 보는 DIP 위반 사례

    위반 사례 문제점
    Controller에서 직접 Repository 생성 (new) 테스트/확장 어려움
    Service가 직접 외부 API Client를 생성 구현 변경이 어렵고 테스트 불가능
    Manager 클래스가 특정 구현체에 직접 의존 OCP, DIP 모두 위반

     

    6. DIP를 지키는 방법

     

    ✔️ 인터페이스 기반 설계

    • 항상 역할(추상화)에 의존하고, 구현체는 DI(Dependency Injection)로 주입

    ✔️ 의존성 주입(DI) 활용

    • 생성자 주입
    • Setter 주입
    • 인터페이스 주입 등

    📌 Spring Framework는 기본적으로 DI 기반으로 동작 -> DIP를 자연스럽게 지킴

    @Service
    public class OrderService {
        private final PaymentGateway gateway;
    
        @Autowired
        public OrderService(PaymentGateway gateway) {
            this.gateway = gateway;
        }
    }

     

    7. Spring과 DIP

    구조 설명
    의존성 주입 (DI Container) Bean 등록 및 주입으로 DIP 실현
    인터페이스 + 구현체 분리 Service, Repository 등 모두 인터페이스로 추상화
    Profile, Qualifier 사용 상황에 따라 구현체 다르게 주입

     

    • Spring에서는 DIP를 지키는 것이 "선택"이 아니라 "기본"이다.

    8. Spring에서 DIP가 실현되는 구조 흐름도

     

    ✅ Spring Framework의 구조 요약

    [Controller / Service / Repository] 
            |
            |  (DI - 의존성 주입)
            v
    [인터페이스(역할)] <--- 여러 구현체(구현)

     

    • 예시 흐름
      1. 개발자는 인터페이스만 설계
      2. 구현체는 @Component, @Service, @Repository 등으로 등록
      3. Spring Container가 주입 시 자동으로 인터페이스에 맞는 구현체를 DI
      4. 실행 시점에 어떤 구현체를 주입할지는 외부 설정 or @Qulifier 사용

    🎯 정리 요약

    항목 DIP 미적용 DIP 적용 + Spring
    의존성 구현체 직접 의존 인터페이스에 의존
    변경 유연성 낮음 (수정 필수) 높음 (DI로 주입)
    테스트 용이성 어려움  Mock 주입 쉬움
    설계 유연성 구현 변경 시 영향 큼 구조 변경에 강함
    Spring 적용 불가 DI 컨테이너가 자동 관리

     

    ✅ 마무리 요약

    • DIP는 "상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다"는 원칙
    • DIP를 지키지 않으면 구현 변경에 고수준 로직이 휘둘리게 됨
    • 인터페이스 기반 설계 -> 의존성 주입(DI)으로 DIP를 실현할 수 있다.
    • Spring은 기본적으로 DIP 철학을 실현하는 프레임워크이다.

    🔜 다음 포스팅에서는 SOLID 정리편 - SOLID 5대 원칙 통합 요약 & 실무 적용 사례 정리라는 주제를 다뤄보려고 한다.