티스토리 뷰

배경

토비의 스프링 강의를 듣던 중 DIP 관련 내용을 다루는데,

 

갑자기 내가 담당하고 있던 프로젝트에서 적용되어있는 헥사고널 아키텍쳐에서

각 레이어 간 클래스들의 참조의 방향이 DIP 를 잘 만족하는 방향인가에 대한 의문점이 생겨

 

관련된 내용들에 대한 정의도 정리해보고, 고찰해본 내용을 정리함


DIP (Dependency Inversion Principle)

먼저 DIP. 의존 역전 원칙에 대한 정의는 이렇게 요약할 수 있다.

정의를 잘 살펴보면 두 가지로 분리 가능한 정의 2개의 집합으로 보인다.

  • DIP : 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다

헥사고널 아키텍쳐를 적용했을 때, 코드 레벨의 참조의 방향과 DIP 원칙을 같이 따져보기 위해서는

조금 추상적으로 느껴질 수 있는 ‘의존’의 정의부터 짚고 넘어가야 하는데, 찾아보니 아래와 같다.

  • 의존(dependency) : 클래스 내부에서 직접 참조하거나 호출하는 경우

위와 같은 ‘의존’의 정의에서는 인터페이스를 ‘구현’ 하는것을 엄밀하게는 ‘의존’한다고 볼 수 없지만,

 

아래 그림에서 WebApiExRateProvider 는 ExRateProvider 를 구현하기 때문에,

타입 수준에서 ExRateProvider 에 컴파일 타임 의존성을 가진다.

이 또한 넓은 의미에서 의존한다고 해석이 가능하다.

 

즉, 그림에서 구현을 의미하는 빈 삼각형 화살표의 방향이 결국 의존의 방향과 동일한 방향.

결과적으로 저수준 구현(WebApiExRateProvider, SimpleExRateProvider)이
고수준 정책을 표현하는 추상화(ExRateProvider)에 의존하도록 설계되어 있으므로,
DIP가 의도하는 의존성 역전이 잘 적용된 사례이다.

 

ExRateProvider <-> WebApiExRateProvider, SimpleExRateProvider 에서는 DIP 가 잘 적용된 사례 OK.

 

근데 PaymentService, ExRateProvider 는 그러면 DIP 원칙이 잘못 적용된거 아니야? Class 가 Interface 에 의존하네?!

하는 생각이 들 수 있다. 

 

한 가지 간과한 것은 DIP는 모든 객체 간 참조에 기계적으로 적용되는 규칙이 아니라,
변경 주기와 책임 수준이 다른 개념들 사이에서 의미를 갖는 설계 원칙이다.

 

즉, PaymentService 와 ExRateProvider 는 모두 비즈니스 정책을 표현하는 고수준 영역에 속하므로,
이 둘 사이의 의존 관계를 두고 DIP 위반 여부를 논하는 것은 적절하지 않다. (그런 의문점 자체가 DIP를 명확히 이해 못함)


정리해보면, DIP(Dependency Inversion Principle) 는
동일한 레이어(패키지, 동일한 수준의 모듈 등) 내부에서는 구체 구현에 대한 의존에 특별한 제약을 두지 않는다.

 

다만 레이어 간 구분, 혹은 변경 주기나 책임 수준에 의해 상위, 하위 수준을 개념적으로 구분했을 때는

하위 레이어가 상위 레이어에 직접 의존하지 않고, 반드시 추상화를 통해 의존하도록 강제하는 원칙이라 볼 수 있다.

 

이러한 제약을 두는 이유는 명확하다. 변경에 대한 유연성을 확보하기 위함이다.

 

쉽게 말하면, 나중에 발생할 수 있는 코드 수정을 최소화하기 위해 미리 책임과 경계를 구분해 두는 행위라고 이해할 수 있고,
조금 더 공식적인 언어로 표현하면, 시스템에서 변경이 자주 발생하는 영역(저수준) 과 상대적으로 안정적인 영역(고수준) 을 분리하고,

서로 다른 변경 주기를 갖는 요소들을 독립적으로 관리하려는 의도가 담겨 있다.

 

이때 이러한 구분을 실질적으로 유지하고 변경의 파급 범위를 최소화하려면,
상위 레이어에 추상화(인터페이스, 추상 클래스 등)를 두고 하위 레이어가 이를 구현하도록 설계하는 것이 효과적이다.
그 결과, 변경이 발생하더라도 저수준 구현만 수정하는 것으로 대응할 수 있으며, 시스템은 보다 유연하고 관리하기 쉬운 구조를 갖게 된다.
DIP는 바로 이러한 효과를 기대하며 적용되는 원칙이다.

 

다만 ‘기대한다’는 표현을 쓰는 이유는,

현실의 요구사항이 항상 설계 시점의 가정대로 흘러가지는 않기 때문이다...


실무에서는 변경되지 않을 것이라 판단했던 고수준 영역조차 수정이 필요한 상황이 발생하기도 한다.

그럼에도 불구하고, 변경의 범위를 의식적으로 최소화하려는 시도와 사고방식이 DIP 원칙이 주는  핵심 가치라고 생각한다.


헥사고널 아키텍쳐 (Hexagonal Architecture)

다음은 헥사고널 아키텍쳐 (a.k.a Port & Adapter 패턴)

헥사고널 아키텍처는 클린 아키텍처라는 소프트웨어 설계 개념을 코드 레벨에서 구현하는 여러 방식 중 하나로,

시스템을 domain, application(service), port, adapter 라는 역할 기반의 경계로 구분하여

의존성 방향이 항상 도메인을 향하게 제한하는 소프트웨어 개발 방식이다.

 

도메인 객체와 도메인 로직이 외부 인프라에 의존하지 않기 때문에 

1. 단위 테스트가 용이하며

 

원칙적으로 Port를 통해서 외부 영역과 내부 도메인 영역을 연결하기 때문에 외부 환경의 변화가 발생하더라도

2. 내부 도메인 로직은 변경하지 않고 Adapter 쪽 코드만 교체하여 유연하게 대응할 수 있다는 점이

 

헥사고널 아키텍처를 사용했을 때 느껴지는 가장 큰 장점이다.

 


 

지금은 익숙해졌지만(당연하지만), 헥사고널을 처음 적용하면서 헷갈렸던 부분을 간단하게 정리!

 

Port와 Adapter가 원형 구조로 Application Core를 감싸고 있는 형태이기 때문에,
헥사고널 아키텍처에서는 Port를 driving(Inbound) 과 driven(Outbound) 두 가지 역할로 구분할 수 있으며,

Adapter 역시 자신이 연결되는 Port의 성격에 따라 driving adapter 또는 driven adapter로 나뉜다.

 

따라서 Port의 구현체는 그 Port가 driving(Inbound)인지, driven(Outbound)인지에 따라 위치가 달라질 수 있다

  • Inbound Port -> Application Layer(Service)에 존재할 수도 있고
  • Outbound Port -> Adapter Layer(Persistence, External Adapter)에 존재할 수도 있다

 

위 그림이 내가 헥사고널 아키텍쳐를 코드로 구현할 때 머리속으로 생각하고 있는 코드 레벨의 실제 의존성 관계와 가장 적합하다 ㅎ

 


헥사고널 아키텍쳐에서 DIP 를 만족하는가?

자 돌고 돌아... 이 글을 작성하게 된 목적!

헥사고널 아키텍쳐에서는 DIP 원칙이 잘 지켜지는지고 있을까?!

 

아래와 같은 헥사고널 아키텍쳐 구조에서 시스템을 domain, application(service), port, adapter 같은 경계를 나누는 것은,

개념적으로 저수준과 고수준의 레이어로 구분한 구조라고 할 수 있고,

따라서 변경과 책임의 주기가 다른 개념들 사이에서 DIP의 만족 여부를 따져보는 질문은 유의미하다! 

 

아래 구조처럼 adapter와 application(port/service), domain을 분리하는게 코드 레벨의 대표적인 모습인데,

코드 레벨 의존 관계는 대체로 다음과 같이 정리할 수 있다.

  • Adapter 가 Port 를 사용(의존)하고,
  • Port는 동일한 레이어의 Service에 의해 구현(의존)되며,
  • Service에서 Domain 모델/규칙을 사용(의존)하는 형태로 구성되게 된다.

이는 저수준이 고수준에 의존하는 DIP 원칙을 잘 지킨 사례라고 볼 수 있다!

src/
├── adapter/
│   ├── in/                       # 입력 포트를 처리하는 어댑터
│   │   ├── rest/                 # REST API 요청 처리
│   │   └── graphql/              # GraphQL 요청 처리
│   ├── out/                      # 출력 포트를 처리하는 어댑터
│   │   ├── db/                   # DB 관련 구현
│   │   └── messaging/            # 메시징 관련 구현
│
├── application/
│   ├── port/                     # 포트 인터페이스
│   │   ├── in/                   # 입력 포트 (use case)
│   │   │   └── UseCasePort.java  # UseCase 관련 인터페이스
│   │   ├── out/                  # 출력 포트
│   │   │   └── UserRepository.java  # 데이터 저장 관련 인터페이스
│   │   └── service/              # 포트를 구현한 서비스
│   │       └── UseCaseService.java  # usecase 처리 로직
│
├── domain/
│   ├── model/                    # 도메인 모델
│   │   ├── User.java             # 사용자 모델 예시
│   │   └── Order.java            # 주문 모델 예시

 

DIP 원칙을 자세히 들여다 보면 다음 두 가지로 구분할 수 있다.

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다.
  2. 둘 두 추상화에 의존해야 한다

이 관점에서 보면, Adapter가 Port(인터페이스)에 의존하는 것은 ‘추상화에 대한 의존’이므로 문제가 없었다.

 

그러나, UseCaseService가 User엔티티(구체 클래스)에 직접 의존하는 것은

‘추상화된 인터페이스’가 아니라 ‘구체적 구현’에 의존하는 것이므로 2번째 원칙을 위반하는 게 아닌가 하는 의문이 생겼다.

 

Application Service가 Domain 엔티티(구체 클래스)에 의존하는 것을 DIP 위반으로 봐야할까?

 

 

이를 검토하며 여러 의견을 찾아본 결과,
DIP의 핵심은 “정책(고수준)이 인프라 디테일(저수준)을 직접 의존하지 말라”는 데 있다는 점을 확인할 수 있었다.

 

Domain은 아래 특징들에 의해 저수준 디테일이 아니라 정책 그 자체에 해당하기에

  • 기술적, 환경적 의존성으로부터 독립적이며
  • 비즈니스 규칙의 변경 외에는 거의 변화하지 않는다.

따라서 코드 레벨에서 Application Service가 Domain 엔티티(구체 클래스)에 직접 의존하더라도,
이는 정책 내부의 의존 관계로 볼 수 있으므로 DIP 위반으로 해석하기는 어렵다고 볼 수 있다!

 

개발하다보면 실무적으로 편의를 위해 도메인 객체를 JPA 엔티티로 그대로 사용하는 경우도 존재하는데,

이 경우 역시 이를 개념적으로 DIP 위반이라고 단정하기는 어렵지만....

이렇게 설정하는 것은 DIP 와 별개로 영속성 프레임워크의 개념이 도메인에 침투할 수 있어

도메인의 독립성이 약화되는 트레이드오프가 발생하기에 순수한 헥사고널 관점에서는 경계가 흐려질 수 있음을 인지할 필요가 있다!


결론

결론적으로 DIP를 이해할 때

문장 그대로 ‘추상화 = 인터페이스, 추상 클래스 ’, ‘구체적인 사항 = 클래스’ 라고 단순 암기하기보다는

DIP에서 말하는 추상화란 구체적인 구현 세부사항으로부터 독립된 역할에 대한 ‘약속’을 뜻한다고 이해하자 😊

'개발' 카테고리의 다른 글

Spring 에서 Cache 사용하기  (0) 2025.12.30
Spring @Scheduled 동작 및 Logging, Metric 관리  (0) 2025.12.30
OLAP에서의 정규화 전략  (0) 2025.12.21
OLTP에서의 정규화 전략  (0) 2025.12.21
TLS 1.2, 1.3  (0) 2025.12.19
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함