Java/OOP / / 2024. 11. 12. 15:10

[객체지향] 의존성 역전 원칙(DIP)을 지키는 코드는 뭘까?

오늘도 썸네일은 채찍피티~

객체지향 SOLID 원칙

먼저, 객체지향 프로그래밍 (Object-Oriented Programming, OOP)를 공부하게 되면 그 중심에는 SOLID 원칙이 있다.

SOLID 원칙이란 다음 다섯 가지 원칙을 지켜 좋은 객체지향 설계를 위한 원칙이다.

  • SRP 단일 책임 원칙
    • 하나의 클래스는 하나의 책임만 가져야 한다.
    • 하나의 책임이라는 것은 모호하므로 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이라고 할 수 있다.
  • OCP 개방-폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
    • 인터페이스를 구현한 새로운 클래스를 만드는 것은, 기존의 코드를 변경하는 게 아니다.
  • LSP 리스코프 치환 원칙
    • 다형성에서 하위 클래스는 인터페이스가 세운 원칙을 따라야 한다.
  • ISP 인터페이스 분리 원칙
    • 특정 클라이언트를 위한 여러 개의 인터페이스가 범용 인터페이스 하나보다 낫다.
  • DIP 의존관계 역전 원칙
    • 프로그래머는 추상화에 의존해야 하며, 구체화에 의존하면 안 된다
    • 구현 클래스에 의존하지 말고 인터페이스에 의존해야 한다.
스프링 프레임워크를 사용한다는 것은, 
객체지향프로그래밍 원칙들이 우리의 코드에 녹아들어서 훨씬 좋은 퀄리티의 코드를 뽑아내는 것
- 토비의 스프링 6 中

의존성 역전 원칙(DIP)의 핵심

의존성 역전 원칙을 조금 더 자세히 살펴보면, 다음과 같이 정의되어 있다.

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 
  2. 두 개의 모듈은 모두 추상화에 의존해야 한다.
  3. 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.

사실 처음 읽으면 대체 무슨 말인지 이해하기 어렵다. 일단 아래로 내려가면서 파악해보자.

결국 추상화에 의지한다는 것은, 인터페이스를 만들고 사용하는 것이다.

자바의 패키지와 모듈

다시 DIP의 핵심내용을 보면, 다음과 같은 말이 있다.

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

 

그럼 모듈은 무엇일까?

패키지란, 하나의 디렉터리로 비슷한 성격의 클래스들을 모아둔 클래스들의 집합이다. 

모듈이란, JDK 9 버전부터 지원한 패키지보다 높은 수준의 집합체이다. 서로 다른 프로젝트가 주고받을 수 있는 (재사용이 가능한) 패키지들의 집합이다. 

  1. 신뢰할 수 있는 구성: 모듈성은 모듈 간의 의존성을 명시적으로 선언할 수 있는 메커니즘을 제공
  2. 강력한 캡슐화: 모듈의 패키지는 명시적으로 내보내지 않으면 다른 모듈이 접근 불가
  3. 확장 가능한 Java 플랫폼: Java 플랫폼은 이제 95개의 모듈로 나뉘어 있으며, 필요한 모듈만 선택하여 사용자 맞춤 런타임을 생성
  4. 향상된 플랫폼 무결성: 내부 API가 실제로 캡슐화되어 있으며, 이는 플랫폼 보안을 개선

위와 같은 목적을 가지고 Java 9 버전에서 새롭게 모듈의 개념을 도입하였다.

 

결국 모듈은 패키지의 집합, 패키지는 비슷한 성격을 갖는 클래스들의 집합이므로,

이 글에서는 이해가 쉽기 위해 패키지와 모듈을 거의 동일한 개념이라고 생각해 보겠다.

예제 프로그램 설계

간단한 환율을 가져와서, 결제 금액을 생성하는 프로그램을 설계했다. (토비의 스프링 6 강의 中)

PaymentService 는 외화의 값을 매개변수로 받아서결제 금액을 생성하는 비즈니스 로직을 포함한다.

또한, ExRateProvider 인터페이스를 구현한 클래스에 의존하여 환율을 가져오는 작업을 수행한다.

 

이런 프로그램에서 모듈은 다음과 같이 분석할 수 있다.

Policy LayerMechanism Layer로 구분을 하여 모듈을 나누고,

Policy LayerMechanism Layer의 클래스를 사용하기 때문에, Policy Layer고수준 모듈 Mechanism Layer저수준 모듈로 볼 수 있다.

 

강의 중에 짠 코드의 패키지 구성을 보면 다음과 같다.

└── hellospring
    ├── Client.java
    ├── ObjectFactory.java
    ├── exrate
    │   ├── CachedExRateProvider.java
    │   ├── ExRateData.java
    │   ├── ExRateProvider.java
    │   ├── SimpleExRateProvider.java
    │   └── WebApiExRateProvider.java
    └── payment
        ├── Payment.java
        └── PaymentService.java

 

Separated Interface 패턴

의존성 역전 법칙에서 Separated Interface 패턴(분리 인터페이스 패턴)이 언급된다.

 

이 패턴은 인터페이스와 그 구현을 별개의 패키지에 위치시키는 패턴이다.

클라이언트(코드의 사용자)가 인터페이스(추상화)에 의존하도록 하면, 클라이언트는 실제 구현에 대해 완벽하게 알지 못하게 된다.

또한 인터페이스는 서비스가 제공하는 기능만을 정의하고, 실제 구현 클래스는 해당 인터페이스를 구현하여 구체적인 로직을 제공한다.

 

따라서, 느슨한 결합도와 변경 용이성을 증가시켜 시스템의 유지보수성과 확장성을 확보해 주는 패턴이다.

이 디자인 패턴에 대해선 마틴파울러의 글을 자세히 읽어보는 것도 좋을 것 같다.

DIP를 온전하게 구현하려면?

위에서 설계한 프로젝트에서, 인터페이스를 Policy Layer로 옮기면 의존성 역전이 온전히 구현된다. 

└── hellospring
    ├── Client.java
    ├── ObjectFactory.java
    ├── exrate
    │   ├── CachedExRateProvider.java
    │   ├── ExRateData.java
    │   ├── SimpleExRateProvider.java
    │   └── WebApiExRateProvider.java
    └── payment
        ├── ExRateProvider.java
        ├── Payment.java
        └── PaymentService.java

 

DIP를 온전하게 구현함을 통해 의존관계 주입 시 다음과 같은 장점이 발생한다.

  1. 기능을 수정할 때, 더 이상 양쪽 모듈을 수정할 필요가 없다. Mechanism Layer만 수정하면 Policy Layer의 기능이 바뀐다.
  2. 더 높은 응집도와 낮은 결합도를 보장한다.
  3. 더 이상 상위수준의 모듈은 하위수준의 모듈을 의존하지 않는다.

결론

결과적으로는 SOLID 원칙의 관점에서는 Separated Interface Pattern을 통해 더 좋은 코드가 되었다.

 

그러나 객체지향 프로그래밍 원칙을 매우 엄격하게 지킨 코드는 제 3자가 코드베이스를 분석하기에 너무 복잡할 수 있다.

또한 이 패턴을 적용하기 위해 인터페이스의 위치를 이동하기 전에도 스프링 프레임워크를 사용하는 코드였기에 적용은 선택적인 사항이라고 여겨진다.

 

팀에서 이 내용에 대해서 어떻게 적용할지 컨벤션이 존재할 테니 잘 따르면 될 것 같다.

이 포스팅은 DIP에 대해 명확히 알게 되는 계기가 되었다

참고한 내용

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유