컴퓨터공학

의존성 역전 원칙의 진짜 의미

nyambu 2025. 5. 21. 12:00

의존성 역전 원칙의 진짜 의미
의존성 역전 원칙의 진짜 의미

1. 의존성 역전 원칙(DIP)이란 무엇인가?

1-1. 정의와 원칙의 배경

 의존성 역전 원칙(Dependency Inversion Principle, DIP)은 로버트 C. 마틴(Robert C. Martin)이 제시한 SOLID 원칙의 마지막 다섯 번째 규칙이다. DIP는 단순한 코드 스타일 가이드가 아니라, 소프트웨어 구조 전체의 설계 방향을 근본적으로 바꾸는 패러다임이다.

 

 DIP는 다음 두 가지 핵심 문장으로 요약된다

  1. 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
  2. 추상화는 세부 구현에 의존하지 않아야 하며, 세부 구현이 추상화에 의존해야 한다.

 여기서 고수준 모듈은 시스템의 비즈니스 규칙을 담은 핵심 로직이고, 저수준 모듈은 DB, 메시지 브로커, 외부 API, 프레임워크 등 구현 기반이다. DIP는 정책이 구현을 지배해야 한다는 철학을 실현하는 수단이다.

이 원칙이 없으면 시스템은 기술 스택, 프레임워크, 외부 API에 종속된 구조가 되고 만다. 그 결과, 시스템이 기술 변화나 외부 환경 변화에 쉽게 흔들리며, 장기적인 유지보수가 어렵고 테스트도 불가능한 구조로 고착된다.

1-2. 의존성 '역전'의 정확한 의미

 "역전(Inversion)"이라는 표현은 많은 오해를 낳는다. DIP에서의 역전은 제어 흐름의 반전이 아니라, 의존성의 방향 전환이다. 기존에는 고수준 모듈이 저수준 모듈을 직접 호출했다. 즉, 비즈니스 로직이 JpaRepository, HttpClient, KafkaProducer 같은 구현체를 직접 사용하면서 기술에 종속되었다.

 

 하지만 DIP에서는 이러한 구조를 인터페이스 기반의 추상화 계층으로 뒤집는다. 이제 고수준 모듈은 특정 구현체가 아니라 인터페이스(Port) 에만 의존하게 된다. 구현체는 이 인터페이스를 구현하는 어댑터(Adapter) 형태로 설계된다. 의존성의 방향이 구체 → 추상으로 전환된 것이다.

 

 예시로, UserService가 UserRepository라는 인터페이스만 의존하고, JpaUserRepository는 이 인터페이스의 구현체로 바 인딩된다면, 기술 교체 없이 도메인 로직을 유지할 수 있다. 이것이 DIP의 핵심이며, 진정한 의미의 “역전”이다.

1-3. 객체지향 설계에서의 전략적 위치

 DIP는 객체지향 설계의 중심에 있다. 특히 SRP(단일 책임 원칙)과 OCP(개방-폐쇄 원칙)과 유기적으로 연결된다. 시스템이 커질수록 비즈니스 정책과 기술 구현은 명확히 분리되어야 하며, 이러한 분리는 DIP 없이는 불가능하다.

  • SRP를 통해 책임이 분리되었다 하더라도, 실제 구현체에 의존한다면 여전히 결합은 존재한다.
  • OCP는 기존 코드를 수정하지 않고도 확장을 가능하게 하려면, 반드시 DIP 기반의 추상화가 필요하다.

 결국 DIP는 객체 지향의 3대 축 중 하나이며, 시스템을 구조적으로 유연하게 만들고 유지보수 비용을 획기적으로 줄여주는 설계 철학이다.

1-4. DIP가 지켜지지 않는 현실의 문제점

 많은 실무 프로젝트에서 DIP는 간과되거나 무시된다. 그 결과 다음과 같은 문제가 자주 발생한다:

  • 비즈니스 로직이 기술 구현과 섞여 테스트 불가 구조가 된다.
  • Service 클래스에서 HttpClient, RestTemplate, KafkaTemplate 등이 직접 호출되면서, 테스트 시 네트워크/DB 환경 없이 실행이 불가능하다.
  • 기술 변경이 시스템 전체의 변경으로 확산된다.
  • 예를 들어, MongoDB를 PostgreSQL로 교체해야 할 때, 수많은 Service 코드까지 수정을 요하게 된다.
  • 로직의 재사용이 불가능하고, 코드 중복이 많아진다.
  • 기술 종속이 심한 코드는 다른 프로젝트에서 재활용이 불가능하며, 기능이 조금만 달라져도 처음부터 다시 작성해야 한다.

 이러한 문제들은 단순히 코드 스타일 문제가 아니다. 시스템 전체의 유연성과 확장성을 갉아먹는 구조적 결함이다. 따라서 DIP는 선택이 아닌 필수 원칙이다.


2. DIP 적용 구조와 설계 방식

2-1. 고수준 ↔ 저수준 모듈 구조 분리

 DIP의 실질적 적용을 위해서는 구조 자체를 바꿔야 한다. DIP 구조의 핵심은 다음과 같다:

  • 고수준 모듈(도메인, UseCase)은 구현체가 아닌 인터페이스(Port)에 의존한다.
  • 저수준 모듈(DB, 외부 API)는 그 인터페이스를 구현한다.
  • 고수준 ↔ 저수준은 직접 연결되지 않고, 의존성 주입(DI) 을 통해 연결된다.

 이를 통해 로직 중심 설계가 가능해지고, 외부 의존 없이도 도메인을 테스트할 수 있다.

2-2. 실전 설계 예시 – Repository, External API

(1) Repository

public interface UserRepositoryPort {
    User findById(Long id);
}
public class JpaUserRepositoryAdapter implements UserRepositoryPort {
    // 실제 JPA 로직 구현
}

UseCase에서는 UserRepositoryPort만 알고 있으며, JpaUserRepositoryAdapter는 바인딩만 된다. JPA를 MyBatis, QueryDSL, 혹은 Redis로 바꾸더라도 UseCase는 그대로 유지된다.

 

(2) External API

public interface NotificationSenderPort {
    void sendWelcomeEmail(User user);
}
public class SmtpNotificationAdapter implements NotificationSenderPort { ... }

public class SlackNotificationAdapter implements NotificationSenderPort { ... }

비즈니스 로직은 “무엇을 해야 한다”만 알고, “어떻게 해야 하는지”는 알지 못한다. 이 철저한 추상화가 DIP 설계의 핵심이다.

2-3. 의존성 주입(DI)과 구조적 응집력

 Spring이나 NestJS 같은 프레임워크는 DIP를 쉽게 실현할 수 있도록 다양한 DI 방식을 제공한다:

  • 생성자 주입 (Constructor Injection) – 가장 선호되는 방식
  • Setter 주입 – 선택적 의존성에 적합
  • 필드 주입 – 간단하지만 테스트 어려움

 이러한 주입 방식은 구조적 응집력을 높이며, 컴포넌트 간 의존 방향을 명확하게 만든다. 또한 테스트 시 Mock 객체를 바인딩하는 것도 매우 수월해진다.

2-4. Adapter 패턴과의 결합

 DIP는 Adapter 패턴과 결합되었을 때 실전에서 가장 큰 효과를 발휘한다. 다음과 같은 구조가 이상적이다:

  • Port (Interface): 고수준 정책의 계약
  • Adapter (Impl): 저수준 구현의 세부 내용

이 방식은 확장성과 교체 용이성 면에서 가장 효율적이다.

 

예:

  • PaymentProcessorPort → KakaoPayAdapter, TossAdapter
  • UserRepositoryPort → JpaAdapter, RedisAdapter

 결론적으로 DIP를 실현한 구조는 시스템 전체가 느슨하게 결합된 상태를 유지하면서도, 높은 응집력으로 인해 변경에 매우 강한 특성을 가진다. 이는 대규모 프로젝트, 장기 운영 서비스, 팀 협업 환경 모두에서 가장 강력한 설계 방식으로 자리매김하고 있다.


3. DIP와 테스트 전략의 연계

3-1. DIP가 테스트 구조를 어떻게 바꾸는가?

 의존성 역전 원칙(DIP)이 가장 실질적인 효과를 발휘하는 영역 중 하나는 테스트 구조다. DIP를 적용한 시스템은 의존성이 ‘구현체’가 아닌 ‘인터페이스’로 전환되어 있기 때문에, 실제 구현 없이도 테스트가 가능해진다. 즉, 테스트할 대상이 외부 시스템과 직접 연결되지 않고 포트를 통해 추상화되어 있다면, 테스트 환경에서는 해당 포트를 목(mock)이나 스텁(stub)으로 대체해 로직 검증이 가능하다. 이를 통해 단위 테스트(Unit Test), 통합 테스트(Integration Test), 인수 테스트(E2E Test)를 명확히 분리할 수 있게 된다.

 

3-2. 포트를 중심으로 한 테스트 설계

 DIP 기반 아키텍처에서는 UseCase 계층이 Output Port에 의존하고, 어댑터 계층이 이를 구현하게 된다. 이 구조에서는 다음과 같은 테스트 방식이 가능하다:

  • 도메인 로직 테스트: 외부 의존 없이 순수한 단위 테스트가 가능하다.
  • UseCase 테스트: Port 인터페이스를 Mock 객체로 대체하고, 흐름과 조건을 검증한다.
  • Adapter 테스트: 실제 구현체에 대한 기능 검증(예: JpaAdapter가 잘 작동하는지 확인)

 예를 들어 RegisterUserUseCase는 UserRepositoryPort, NotificationSenderPort에 의존할 수 있다. 테스트 시에는 이 두 포트를 가짜 구현(Mock 객체)으로 대체해 다음을 검증할 수 있다:

  • 올바른 파라미터로 Port가 호출되었는가?
  • 예외 상황 시 적절한 처리가 되었는가?
  • 포트를 통해 발생한 Side Effect 없이 도메인 상태가 바르게 변경되었는가?

 이러한 방식은 ‘로직 중심의 테스트’를 가능하게 하며, 통합 테스트 없이도 핵심 시나리오 검증이 가능하다는 큰 이점을 제공한다.

3-3. 테스트 커버리지와 CI/CD 환경에서의 장점

 DIP 구조는 CI/CD 환경에서 테스트 자동화를 적용하는 데에도 매우 유리하다. 다음과 같은 방식으로 테스트 계층을 구성할 수 있다:

  • 유닛 테스트: 도메인 모델과 UseCase 검증 (빠르고 독립적)
  • 통합 테스트: 어댑터 구현체와 외부 시스템 간 연결 검증
  • 계약 테스트(Contract Test): Port와 Adapter 간 계약이 올바르게 유지되는지 검증

 각 계층의 책임이 명확히 나뉘어 있기 때문에 테스트 코드도 각 계층별로 나누어 작성할 수 있으며, CI 서버에서 병렬로 실행하거나 변경 범위에 따라 필요한 테스트만 선택적으로 실행할 수 있다. 또한 DIP는 테스트 유지보수를 단순하게 만든다. 예를 들어, 메일 전송 기능에서 SMTP 기반 어댑터를 SendGrid API로 바꾼다 해도, UseCase 테스트는 그대로 유지될 수 있다. 이는 테스트 코드의 안정성과 재사용성을 획기적으로 끌어올린다.

3-4. 테스트 전략 수립 시 고려사항

 DIP 기반 테스트 전략을 실무에서 적용할 때는 다음 사항을 고려해야 한다:

  1. 포트 설계의 단일 책임: 한 포트가 너무 많은 기능을 담당하지 않도록 쪼개야 Mock 구성도 쉽다.
  2. 테스트 대역의 명확한 분리: 도메인 로직 검증, 어댑터 구현 검증, 통합 테스트는 철저히 나누어야 한다.
  3. 테스트 명명 규칙: RegisterUserUseCaseTest, JpaUserRepositoryTest, SendMailAdapterContractTest와 같이 계층별 명명 규칙을 정해 구조적 테스트 체계를 유지해야 한다.
  4. 테스트 가이드 문서화: 신규 인원도 쉽게 테스트 구조를 이해할 수 있도록 각 계층의 테스트 목적과 방식에 대한 문서화가 필요하다.

 이처럼 DIP는 단순히 아키텍처를 위한 설계 원칙이 아니라, 테스트 전략 수립과 실행을 위한 기반 설계라고 해도 과언이 아니다. 구조적으로 테스트 가능한 시스템을 만들기 위한 가장 강력한 무기가 바로 DIP이다.


4. 실무에서의 DIP 적용 전략

4-1. 완벽한 DIP보다는 현실적인 DIP부터

 DIP는 매우 이상적인 원칙이지만, 모든 프로젝트에서 100% 완벽하게 적용하는 것은 현실적으로 어려울 수 있다. 특히 이미 운영 중인 시스템이나 급하게 구축해야 하는 MVP에서는 DIP의 도입이 오히려 복잡성과 오버엔지니어링으로 작용할 수 있다. 따라서 실무에서는 “DIP를 지향하는 구조”부터 적용하는 것이 중요하다. 핵심 도메인 영역부터 인터페이스 기반으로 설계하고, 그 외 기능은 점진적으로 추상화하는 전략이 바람직하다. 예를 들어 ‘회원 등록’, ‘결제’, ‘주문’과 같이 비즈니스에서 중요한 핵심 로직부터 인터페이스 기반 포트를 정의하고, 그 외 영역은 점차 리팩토링하는 방식이다.

4-2. 레거시 시스템에서의 전환 전략

 이미 DIP가 적용되지 않은 레거시 구조에서도 DIP를 점진적으로 도입할 수 있다. 다음은 효과적인 단계별 접근 방법이다:

  1. 서비스 클래스에서 외부 시스템 호출을 인터페이스로 추상화
  2. 해당 인터페이스를 port.out 디렉토리에 위치시킴
  3. 어댑터 구현체를 adapter.out에 정의하고 기존 구현을 옮김
  4. 테스트 시에는 실제 어댑터 대신 목 구현을 주입하여 검증
  5. UseCase나 도메인 로직에서 어댑터가 아닌 포트만 참조하도록 리팩토링

 이 방식은 구조의 대대적 변경 없이도 DIP 원칙을 적용해 시스템을 유연하게 만들 수 있다. 실제로 많은 대기업이나 레거시 시스템이 이와 같은 방식으로 구조 개선을 점진적으로 수행하고 있다.

4-3. 조직 규모에 따른 도입 전략 차이

 DIP의 도입 전략은 팀 규모에 따라 달라져야 한다. 소규모 팀에서는 복잡한 추상화보다는 적절한 선에서 포트만 정의하고 구현을 바로 붙여 쓰는 방식이 낫다. 반면 중대형 조직에서는 다음과 같은 방식이 권장된다:

  • 공통 포트 정의 → 기능별 어댑터 구현 → 테스트 인터페이스 정의
  • 포트 설계 기준, 네이밍 규칙, 테스트 규칙 문서화
  • 신규 기능은 무조건 포트를 먼저 설계 후 구현
  • 아키텍처 템플릿 기반 구조 설계

 특히 많은 인원이 동시에 협업하는 프로젝트에서는 DIP가 적용된 구조가 협업 기준점이 되어 코드 품질을 유지하는 데 매우 유리하게 작용한다.

4-4. DIP 실패 사례와 방지법

 DIP 적용 실패는 대부분 “의지만 있고 기준이 없는 경우”에 발생한다. 다음과 같은 실패 사례가 많다:

  • 포트를 만들었지만 구현체에서 비즈니스 로직까지 처리해버림 → 도메인 침범
  • 포트를 너무 세분화해 인터페이스 폭발 현상 발생 → 유지보수 불가
  • 어댑터 없이 포트와 구현을 같은 클래스에서 처리 → 구조적 분리 실패
  • DI 설정이 불안정하여 런타임 오류 빈번

 이를 방지하기 위해 다음 기준을 준수해야 한다:

  • UseCase가 반드시 포트를 먼저 정의하고 주입받도록 강제
  • 어댑터는 항상 포트를 implements하며, 외부 기능만 처리
  • 포트는 기능 단위로 구분하되, 지나치게 세분화하지 않기
  • Spring의 @Configuration을 통한 명시적 DI 사용으로 주입 명확화

 이러한 기준을 지키면서 DIP를 적용하면, 실무에서도 현실적으로 유연하고 강력한 구조를 갖춘 시스템을 만들 수 있다.


5. DIP 적용 시 주의할 점과 오해

5-1. DIP는 단순한 인터페이스 사용이 아니다

 실무에서는 종종 인터페이스를 사용하면 DIP를 적용한 것이라 착각하는 경우가 많다. 그러나 DIP의 핵심은 단순히 인터페이스를 사용하는 것이 아니라, 추상화의 방향성과 의존 관계의 설계 구조에 있다.

 

 예를 들어, UserService가 UserRepository라는 인터페이스를 참조하더라도, 해당 인터페이스가 구체적인 JPA 엔티티나 DB 쿼리 방식에 종속되어 있다면 이는 DIP가 아니다. 오히려 구현에 가까운 인터페이스는 오히려 결합도를 높이고, 유지보수성과 테스트 가능성을 떨어뜨리는 결과를 낳는다.

 

진정한 DIP는 다음 조건을 만족해야 한다

  • 인터페이스가 고수준 정책(비즈니스 관점)을 표현하고 있어야 한다.
  • 저수준 모듈이 이 인터페이스를 구현해야 한다.
  • 고수준 모듈이 이 인터페이스만 알고, 구체 구현은 전혀 몰라야 한다.

 즉, 인터페이스의 존재 여부가 아닌, 누가 누구에게 의존하고 있는가, 그리고 그 의존의 방향이 어떤가가 DIP 적용의 핵심 판단 기준이다.

5-2. 포트를 너무 세분화하면 유지보수 지옥

 의욕적으로 DIP를 도입하려는 팀에서 흔히 발생하는 문제가 바로 **인터페이스 폭발(interface explosion)**이다. 기능 하나하나마다 별도의 포트를 만들고, 이름도 지나치게 자세하게 나누면 다음과 같은 문제가 생긴다:

  • 테스트 시 수십 개의 mock 객체를 작성해야 함
  • 포트 네이밍이 너무 길고 중복됨 (UserEmailSavePort, UserEmailCheckPort, UserEmailVerifyPort)
  • 변경 시 포트/어댑터 쌍을 전부 수정해야 하므로 유지보수 비용 증가

 이런 상황은 오히려 DIP의 목적과 정반대되는 결과를 초래한다. 목적은 결합도 낮추기인데, 과도한 추상화로 인해 오히려 구조가 단단히 얽혀버리는 것이다.

 

따라서 실무에서는 다음과 같은 기준으로 포트 범위를 정해야 한다

  • 하나의 유스케이스 기준으로 포트를 정의 (기능 단위 중심)
  • 동일한 리소스를 다루는 연관 기능은 한 포트에 통합
  • 비즈니스 정책이 달라질 가능성이 있는 경우는 분리

 예를 들어 UserCommandPort와 같이 사용자 변경 관련 기능은 하나로 묶되, 인증/결제/배송 등 전혀 다른 도메인은 포트를 분리하는 것이 바람직하다.

5-3. 도메인 객체가 외부 시스템을 직접 호출하는 구조는 절대 금지

 DIP가 적용된 구조에서는 도메인 객체나 유스케이스가 외부 시스템(DB, 외부 API, 메일 서버 등)을 직접 호출하는 것은 절대 금지해야 한다.

 

예를 들어 다음과 같은 코드는 DIP 위반이다

public class User {
    public void register() {
        MailSender sender = new SmtpMailSender(); // DIP 위반
        sender.sendWelcomeMail(this);
    }
}

 이 구조는 도메인 객체가 구현체에 직접 의존하게 되어, 도메인 테스트도 어려워지고 재사용성도 떨어진다. 바람직한 방식은 다음과 같다

public class RegisterUserUseCase {
    private final MailSenderPort mailSender;

    public RegisterUserUseCase(MailSenderPort mailSender) {
        this.mailSender = mailSender;
    }

    public void register(User user) {
        mailSender.sendWelcomeMail(user); // 도메인은 순수 유지
    }
}​

 이처럼 도메인은 항상 정책(인터페이스)에만 의존하고, 구현은 바깥 계층에서 제공해야 한다.

5-4. 어댑터에 비즈니스 로직이 들어가면 구조가 오염된다

 DIP를 적용해도 Adapter에 너무 많은 책임이 들어가게 되면 구조가 쉽게 무너진다. 특히 외부 연동 처리, 포맷 변환, 트랜잭션 처리 등 외에도 도메인 정책이 어댑터 안으로 들어가는 경우가 종종 있다.

 

public class UserJpaRepositoryAdapter implements SaveUserPort {
    public void save(User user) {
        if (user.getAge() < 19) { // 비즈니스 정책이 어댑터 안에?
            throw new RuntimeException("미성년자는 등록 불가");
        }
        repository.save(user);
    }
}

 이런 구조는 도메인 순수성을 침해하고, 테스트 분리를 어렵게 만든다. 모든 정책은 도메인 혹은 UseCase에 있어야 하며, 어댑터는 단순히 I/O 처리만 담당해야 한다. 구조가 무너지기 시작하는 지점은 대부분 이런 경계 위반에서 발생한다.


6. DIP와 다른 설계 원칙과의 관계

6-1. SRP와의 연결 – 역할 분리의 핵심 도구

 DIP는 SRP(Single Responsibility Principle, 단일 책임 원칙)와 가장 밀접하게 연결된 원칙 중 하나다. SRP는 ‘하나의 모듈은 하나의 변경 이유만 가져야 한다’는 원칙인데, 이를 지키려면 외부 책임을 별도로 분리해야 한다. 바로 이 분리를 가능하게 하는 도구가 DIP다.

 

 예를 들어 결제 처리 로직이 다음과 같다고 하자

public class PaymentService {
    public void processPayment(Order order) {
        // 1. 결제 승인 요청
        // 2. 결제 상태 저장
        // 3. 이메일 발송
    }
}

 이 구조는 결제 API, DB, 이메일 발송이라는 세 가지 책임이 섞여 있으며, SRP를 완전히 위반한다. DIP를 적용하면 이렇게 바뀐다

public class PaymentUseCase {
    private final PaymentGatewayPort paymentGateway;
    private final OrderRepositoryPort orderRepository;
    private final NotificationSenderPort notificationSender;

    public void processPayment(Order order) {
        paymentGateway.approve(order);
        orderRepository.save(order);
        notificationSender.sendConfirmation(order);
    }
}

 책임이 각 포트 단위로 나뉘면서 SRP를 구조적으로 실현할 수 있게 된다.

6-2. OCP와의 연결 – 확장을 가능하게 하는 구조

 OCP(Open/Closed Principle)는 ‘확장에는 열려 있고 변경에는 닫혀 있어야 한다’는 원칙이다. DIP는 이 원칙을 실현하는 구조적 수단을 제공한다.

 

예를 들어 기존 결제 방식이 신용카드뿐이었다가, 카카오페이를 추가해야 한다면 DIP 구조에서는 이렇게 처리된다

  • PaymentProcessorPort 인터페이스는 그대로 유지
  • 새로운 KakaoPayAdapter 클래스 추가
  • DI 설정만 바꿔서 새로운 결제 방식을 주입

 즉, 기존 유스케이스나 도메인 로직을 수정하지 않고 새로운 기능을 확장할 수 있게 된다. 이는 DIP가 제공하는 유지보수성과 유연성의 핵심 포인트다.

6-3. LSP/ISP와의 연결 – 유연성과 대체 가능성 확보

 LSP(Liskov Substitution Principle, 리스코프 치환 원칙)는 ‘서브타입은 언제나 슈퍼타입으로 대체 가능해야 한다’는 원칙이다. DIP 구조에서는 포트를 인터페이스로 설계하고, 그에 대한 다양한 구현체를 만들기 때문에 자연스럽게 LSP가 실현된다. 또한 ISP(Interface Segregation Principle)는 ‘클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리해야 한다’는 원칙이다. DIP 구조에서는 각 포트를 유스케이스 단위로 설계하므로 ISP를 만족하는 인터페이스 구조를 갖게 된다.

 

public interface PaymentProcessorPort {
    void approve(Order order);
}

→ 단순하고 명확한 책임, 과도한 메서드 없음 → ISP 만족

또한

PaymentProcessorPort processor = new KakaoPayAdapter();

→ 어떤 Adapter든 동일한 계약을 만족하는 한 자유롭게 교체 가능 → LSP 만족

 

 결국 DIP는 SOLID 원칙 전체를 구조적으로 구현하는 핵심 축이라 할 수 있다. DIP 없이는 다른 원칙도 현실에서는 유지되기 어렵다.


7. 현실적인 DIP 도입 가이드

7-1. 도입은 ‘전면 적용’이 아니라 ‘핵심 유스케이스 중심’으로

 많은 팀이 DIP를 적용하려다 초기에 무너지는 가장 큰 이유는 너무 많은 범위에 동시에 적용하려 하기 때문이다. 실제로 프로젝트의 모든 서비스, 저장소, 외부 API 연동 기능을 한 번에 포트와 어댑터 구조로 재설계하는 것은 쉽지 않다.

 

따라서 현실적인 접근 방법은 다음과 같다

  • 도메인 핵심 기능부터 DIP 적용
    예: 회원 등록, 주문 생성, 결제 승인 등 비즈니스에 직접적 영향을 주는 로직
  • 외부 의존도가 큰 기능부터 적용
    예: 메일 전송, 결제 API 연동, 메시지 발행 등 I/O가 많은 컴포넌트

 이처럼 시스템의 중심부부터 DIP를 적용해 나가면, 점진적인 전환이 가능하고, 구조의 일관성도 단계적으로 확립할 수 있다. 결국, DIP는 완성되는 구조가 아니라 확장 가능한 방향성으로 이해해야 한다.

7-2. 기존 코드에 DIP를 적용할 때의 단계별 절차

운영 중인 레거시 코드에 DIP를 적용하려면 다음 절차를 추천한다

  1. 기존 클래스에서 외부 연동 부분 추출
    예: DB, 외부 API 호출, 메시지 발행 등
  2. 해당 기능을 인터페이스(Port)로 추상화
    • SaveUserPort, MailSenderPort 등 기능 중심으로 이름 구성
  3. 기존 구현체를 어댑터로 이동시킴
    • JpaUserRepository → JpaUserRepositoryAdapter
    • SmtpMailSender → SmtpMailAdapter 등
  4. UseCase에서 Port만 참조하도록 리팩토링
  5. 의존성 주입 구성 변경 (생성자/스프링 설정 등)

 이 과정을 거치면 구조가 DIP 기반으로 전환되며, 테스트 코드도 구성하기 쉬워진다. 중요한 점은 ‘기능 단위’로 잘라 적용하는 것, 한 번에 시스템 전체를 뜯어고치지 않는 것이다.

7-3. 작은 팀에서의 적용 전략

 소규모 팀에서는 DIP를 적용하기가 더 어렵다고 느낄 수 있다. 그러나 사실상 DIP는 작은 팀일수록 코드 품질과 유지보수성을 높이는 강력한 전략이 된다. 단, 다음과 같은 방식으로 간소화하면 좋다.

  • 포트 인터페이스는 필수 기능만 선언
  • 어댑터 클래스는 최대한 작고 단순하게 유지
  • 도메인과 UseCase 로직은 항상 외부 기능을 포트를 통해 호출
  • Spring 환경에서는 @Primary, @Qualifier 등을 통해 구현체 분리

 이렇게만 해도 DIP는 충분히 실현 가능하고, 향후 팀원이 늘어나거나 기능이 확장될 때 훨씬 유리한 기반을 제공하게 된다.

7-4. 아키텍처 템플릿 및 코드 자동 생성 도구 활용

 팀 전체가 DIP 구조를 유지하려면 일관된 구조와 템플릿이 중요하다. 이를 위해 다음과 같은 전략을 적용할 수 있다:

  • 프로젝트 템플릿에 port/in, port/out, adapter/in, adapter/out, domain 디렉토리를 고정
  • 포트 파일명은 UseCase 기준 + Port 접미사: RegisterUserUseCase, SaveUserPort
  • 코드 생성기 혹은 템플릿 CLI 도구 도입: NestJS, Spring Boot에서 가능

 이러한 자동화는 DIP 구조의 반복적 작성 과정을 줄이고, 구조 일관성을 유지하는 데 큰 도움이 된다. 결국 DIP는 '설계 철학'이지만, 실무에서는 개발자의 습관과 도구 사용법에 따라 성공 여부가 달라진다.


8. DIP가 만드는 지속 가능한 구조

8-1. DIP는 기술 독립성과 구조의 생존력을 만든다

 DIP를 적용하면 가장 먼저 얻게 되는 효과는 기술 독립성이다. 도메인 로직은 어떤 구현체에도 의존하지 않고, 오직 포트를 통해 외부와 소통한다. 이것은 단지 설계상의 이점이 아니라, 시스템의 생존력을 좌우하는 핵심 전략이다.

  • 데이터베이스를 MySQL → PostgreSQL로 바꿔도, 포트 구현만 수정
  • 메일 서버를 SMTP → AWS SES로 바꿔도, 어댑터만 교체
  • 프레임워크를 Spring → Micronaut로 바꿔도, 도메인 로직은 그대로

 즉, DIP 구조는 기술 변화, 시스템 이식, 인프라 마이그레이션에 매우 강한 구조적 탄탄함을 제공한다.

8-2. DIP는 개발 생산성과 리팩토링 효율을 높인다

 DIP 구조는 테스트, 배포, 변경 대응에서 다음과 같은 실질적인 이점을 제공한다.

  • 포트 단위로 변경 사항을 캡슐화하므로 영향 범위가 작음
  • 어댑터 교체만으로 다양한 환경에 맞춤형 대응 가능
  • 비즈니스 로직은 외부 기술과 단절되어 리팩토링이 쉬움
  • 테스트 코드가 간단하고 빠름 → 빠른 피드백 루프 확보

 이는 특히 기능이 자주 변경되고, 실험이 많은 환경에서 더 큰 차이를 만든다. 애자일 팀, CI/CD 파이프라인 운영 팀이라면 DIP 구조는 필수가 될 수 있다.

8-3. DIP 구조는 협업의 기준점을 만든다

 중대형 프로젝트나 팀 협업 환경에서 DIP는 단순한 코드 구조를 넘어 역할과 책임의 기준점을 제공한다.

 

예를 들어

  • 프론트엔드 팀은 adapter/in (Controller)만 알고 유스케이스를 호출
  • 백엔드 개발자는 UseCase와 Domain만 담당
  • 인프라 엔지니어는 adapter/out만 교체해 환경에 맞춤 구성

 이처럼 각 구성원이 DIP 구조에서 자신의 역할을 명확히 인지할 수 있으며, 구조가 ‘기술 문서’ 그 자체로 기능하게 된다. 코드 리뷰, 온보딩, 기술 공유, 협업 시에도 기준점이 명확해 커뮤니케이션 효율이 크게 향상된다.

8-4. DIP는 소프트웨어의 장수 구조를 가능하게 한다

 좋은 소프트웨어는 단지 잘 동작하는 코드가 아니라, 변화에 오래 견디는 구조다. DIP를 통해 시스템은 다음과 같은 장수 구조를 갖게 된다.

  • 외부 기술 변화에 유연하게 대응 가능
  • 로직은 로직대로, I/O는 I/O대로 깔끔히 분리
  • 테스트는 빠르고 가볍게 유지 가능
  • 코드 리뷰나 협업도 구조적으로 진행 가능
  • 리팩토링 시 최소 범위로 안정적 개선 가능

 결과적으로 DIP는 단순한 ‘설계 규칙’이 아니라, 소프트웨어 생명 주기 전체를 관통하는 철학이다.

'컴퓨터공학' 카테고리의 다른 글

SRP vs OCP, 실제 적용 사례  (0) 2025.05.22
SOLID 원칙 완전정복  (0) 2025.05.21
Clean Architecture 원칙  (0) 2025.05.21
헥사고날 아키텍처란?  (0) 2025.05.20
레이어드 아키텍처 구조  (0) 2025.05.20