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

2024. 6. 25. 18:35· Spring/Spring Framework
목차
  1. 의존성 역전 원칙: Dependency Inversion Principle
  2. 요구사항
  3. DIP를 준수하지 않은 코드
  4. DIP를 준수한 코드
  5. DIP와 OCP는 서로 같은 개념?
  6. 마치며

의존성 역전 원칙: Dependency Inversion Principle


의존성 역전의 원칙은 다음과 같이 정의할 수 있습니다.

  • 상위 수준 모듈은 하위 수준 모듈의 세부 사항에 의존해서는 안됩니다.
  • 하위 수준 모듈은 상위 수준 모듈에 정의된 추상 타입에 의존해야 합니다.

의존성 역전의 원칙은 모듈 간 의존성을 낮추고, 유연성과 유지보수성을 향상시킵니다.

해당 원칙의 주요 개념을 요약하자면 다음과 같습니다.

  • 상위 모듈: 비즈니스 로직이나 주요 기능을 정의하는 모듈 (ex Controller)
  • 하위 모듈: 구체적인 구현을 포함하는 모듈 (ex Service, Repository)
  • 추상 타입: 인터페이스나 추상 클래스를 통해 상위 모듈과 하위 모듈 간의 의존성을 정의

DIP를 설명하기 위해 간단한 예제를 통해 설명하겠습니다.

 

요구사항


  1. 카드 결제 시스템을 만들고자 한다.
  2. 현재 결제를 지원하는 카드는 신한카드 하나만 있다.
  3. 추후 새로운 카드가 지속적으로 추가될 예정이다.

 

DIP를 준수하지 않은 코드


위 그림은 PaymentController(상위 모듈)가 ShinhanCardPaymentService(하위 모듈)를 의존하고 있습니다. 이를 코드로 보는 경우 다음과 같습니다.


            
class PaymentController {
@PostMapping("/rom/payment")
public void pay(@RequestBody ShinhanCardPaymentRequest req){
shinhanCardPaymentService.pay(req);
}
}
class ShinhanCardPaymentService {
public void pay(ShinhanCardPaymentRequest req) {
shinhanCardApi.pay(req);
}
}

현재 신한카드 결제만 지원한다면 큰 문제는 없어보이기도 합니다. 그러나 추후 새로운 카드가 추가된다면 문제가 발생합니다. 즉, 카드 결제 기능(상위 모듈)이 신한카드 결제(하위 모듈)에 지나치게 의존적인 상황입니다. 이러한 지나친 의존 관계는 결합도가 높아지는 문제를 가집니다.

 

지나친 의존 관계


            
@PostMapping("/rom/payment")
public void payment(@RequestBody PaymentRequest request) {
switch (request.getType()) {
case CardType.SHINHAN -> shinhanCardPaymentService.pay(request);
case CardType.WOORI -> wooriCardPaymentService.pay(request);
default -> throw new IllegalArgumentException("Unknown card type: " + request.getType());
}
}

위처럼 새로운 카드가 추가될 때마다 상위 모듈은 수정해야하는 번거로움을 가집니다. 이는 절대 좋은 구조가 아니며, 반복적인 코드는 리팩토링 대상일 확률이 매우 높습니다.

 

추가될 카드의 결제를 담당할 XXXPaymentService 클래스(하위 모듈)들이 지속적으로 의존하게 될 것이며, 그 결과 PaymentController는 컨트롤러 계층임에도 너무 많은 책임을 가지게 됩니다. 이는 확장에 어렵고, 유지 보수에 취약한 구조를 가지게 됩니다.

 

DIP를 준수한 코드


상위 모듈(카드 결제)은 하위 모듈(신한카드 결제)에 의존해서는 안됩니다. 상위 모듈(카드 결제)은 신한카드 결제가 들어올지 다른 카드 결제가 들어오는지 알지 못해도 본인의 책임인 카드 결제가 진행되어야 합니다. 즉, 상위 모듈은 구체화된 하위 모듈을 바라보는 것이 아닌 추상화된 인터페이스에 의존해야 합니다.


            
class PaymentController {
@PostMapping("/rom/payment")
public void pay(@RequestBody CardPaymentRequest req){
CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
cardPaymentService.pay(req);
}
}
public CardPaymentFactory {
private final WooriCardPaymentService wooriCardPaymentService;
private final ShinhanCardPaymentService shinhanCardPaymentService;
public CardPaymentService getType(CardType type) {
switch (type) {
case CardType.SHINHAN -> return shinhanCardPaymentService;
case CardType.WOORI -> return wooriCardPaymentService;
default -> throw new IllegalArgumentException("Unknown card type: " + request.getType());
}
}
}
public interface CardPaymentService {
void pay(CardPaymentRequest req);
}
public class ShinhanCardPaymentService implements CardPaymentService {
@Override
public void pay(CardPaymentRequest req) {
shinhanCardApi.pay(req);
}
}
public class WooriCardPaymentService implements CardPaymentService {
@Override
public void pay(CardPaymentRequest req) {
wooriCardApi.pay(req);
}
}

의존관계를 인터페이스를 통해 추상화시켜 의존성을 역전시켰습니다. 컴파일 단계에서는 PaymentController는 PaymentService 인터페이스를 바라보고 있지만 런타임에서는 CardPaymentFactory를 통해 CardType을 찾아 적절한 XXXPaymentService 구체 클래스를 바라보게 해 줍니다.

 

언뜻 보면 초기 작업이 다소 많아 보일 수 있습니다. 그러나 DIP의 핵심은 시스템 확장이 유연해지며, 유지 보수에 용이해진다는 점입니다. 예를 들어, 서비스가 확장되어 결제 방식이 추가된다면 DIP의 장점은 더욱 명확해집니다.

 

DIP와 OCP는 서로 같은 개념?


그렇다면 문득 DIP와 OCP는 서로 확장과 유지보수성을 향상시키기 위함인데 서로 같은 의미가 아닌가?라는 생각이 들기도 합니다. 결론부터 말하면 지향하는 바는 같지만 방법에 차이가 있습니다.

 

목적의 차이: 

  • OCP는 시스템의 확장성을 높이고, 기존 코드를 수정하지 않고 기능을 추가할 수 있게 하는 것
  • DIP는 모듈 간의 의존성을 낮추고, 고수준 정책이 저수준 세부사항에 의존하지 않도록 하는 것

적용 대상의 차이: 

  • OCP는 주로 클래스나 모듈의 확장과 변경을 관리하는 데 사용
  • DIP는 모듈 간의 관계를 관리하고, 의존성을 제어하는 데 사용

방법의 차이: 

  • OCP는 인터페이스나 추상 클래스, 다형성을 통해 기능 확장을 가능하게 합니다.
  • DIP는 상위 모듈과 하위 모듈 모두가 인터페이스나 추상 클래스에 의존하게 만들어 의존성을 역전시킵니다.

 

마치며


DIP를 따른다면 모든 구현체에 인터페이스를 바라봐야 하는 것인가라는 질문이 듭니다. 제 생각으로는 항상 인터페이스를 바라보는 것은 적절하지 않다고 생각합니다. 모든 변경사항에 대비하여 설계하는 것은 현실적으로 어렵습니다. 추후 변경사항이 미비한 경우 구체 클래스를 사용하고 추가될 상황이 오면 그때 인터페이스를 적용하는 것도 하나의 방법이라고 생각합니다.

 

그러나 위 예제처럼 지속적으로 결제 방식이 추가되는 요구사항처럼 예측 가능한 상황에서는 인터페이스를 통해 DIP를 적용시키는 것이 바람직합니다. 특히, 인프라스트럭처(Infrastructure)는 언제든지 대체될 수 있는 영역이므로, 인터페이스를 사용해 의존성을 역전시키는 것이 적절하다고 생각합니다.

'Spring > Spring Framework' 카테고리의 다른 글

인터페이스 분리 원칙(Interface Segregation Principle, ISP)  (0) 2024.07.03
리스코프 치환 원칙(Liskov Subsitution Principle, LSP)  (0) 2024.07.01
단일 책임의 원칙(Single Responsibility Principle, SRP)  (0) 2024.06.27
개방 폐쇄 원칙(Open-Close Principle, OCP)  (0) 2024.06.26
스프링(Spring)이란?  (0) 2024.02.03
  1. 의존성 역전 원칙: Dependency Inversion Principle
  2. 요구사항
  3. DIP를 준수하지 않은 코드
  4. DIP를 준수한 코드
  5. DIP와 OCP는 서로 같은 개념?
  6. 마치며
'Spring/Spring Framework' 카테고리의 다른 글
  • 리스코프 치환 원칙(Liskov Subsitution Principle, LSP)
  • 단일 책임의 원칙(Single Responsibility Principle, SRP)
  • 개방 폐쇄 원칙(Open-Close Principle, OCP)
  • 스프링(Spring)이란?
Hui._.
Hui._.
High hope
Hui._.
개발 일기
Hui._.
전체
오늘
어제
  • 분류 전체보기 (57)
    • Java (4)
    • Spring (26)
      • Spring Framework (6)
      • Spring Security (9)
      • JPA (11)
    • CS (2)
    • 알고리즘 (19)
      • 문제풀이 (16)
      • 자료구조 (3)
    • ETC (3)
    • Project (3)
      • Trouble Shooting (3)

블로그 메뉴

  • 홈
  • 글쓰기
  • 설정

공지사항

인기 글

태그

  • 스프링 시큐리티6.1
  • 원칙
  • oauth
  • 엔티티
  • java
  • HashMap
  • Spring Boot 3.2.3
  • 최소공배수
  • Oauth 2.0
  • 호제법
  • 인터페이스
  • jpa
  • jwt
  • 최대공약수
  • 유클리드
  • dynamic programming
  • 매핑
  • persist
  • SOLID
  • Spring Security 6.1
  • 분리
  • 프로그래머스
  • 지연
  • 추상화
  • 회원 기능
  • 코딩테스트
  • Spring Security
  • 스프링 부트3
  • Spring Security + JWT + OAuth 2.0
  • 알고리즘

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
Hui._.
의존성 역전 원칙(Dependency Inversion Principle, DIP)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.