본문 바로가기

Backend/JAVA

DIP(Dependency Inversion Principle)

DIP에 대해 알아보도록 하겠습니다. DIP는 객체지향설계 5원칙(SOLID)에서 D에 해당하는 원칙이며, 아래와 같은 의미를 가지고 있습니다.

저수준 모듈이 고수준 모듈에 의존하게 되는 것

하지만 이 설명만 봐서는 도저히 무슨 소리인지 감이 잘 잡히지 않습니다. 그래서 DIP가 무엇인지, 왜 사용하는지, 어떻게 사용하는지에 대해 조금 더 자세히 알아보도록 하겠습니다.

혹시 글을 읽으면서 잘못된 내용이 있으면 댓글로 알려주시면 감사하겠습니다!

 

🌳 계층 구조 아키텍처

보통 계층 구조는 위 사진과 같은 구조로 되어 있습니다.

  • 표현 계층 : 사용자의 요청을 받아 응용 영역에 전달함과 동시에 처리 결과를 사용자에게 표시
  • 응용 영역 : 사용자에게 제공해야 할 기능 구현
  • 도메인 영역 : 도메인의 핵심 로직 & 도메인 모델 구현
  • 인프라 계층 : 구현 기술을 다룸 (ex. DB 연동)

각 계층은 위와 같은 특징을 가지고 있으며, 상위 계층은 하위 계층에게 의존하지만, 하위 계층은 상위 계층에 의존하지 않습니다. 하지만 그렇게 되면 인프라 계층에게 종속적인 현상이 많이 일어나게 됩니다.

예제를 보도록 하겠습니다.


👩‍👦 기존 코드의 문제

알람 서비스를 구현하려고 합니다. 알람을 보내는 서비스는 A사에서 제공하는 알람 기능을 사용하려고 합니다.

/**
 * A사의 알람 서비스
 */
public class A {
  public String beep() {
    return "beep!";
  }
}
/**
* 서비스 코드
*/
public class AlarmService {

  private A alarm;

  public String beep() {
    return alarm.beep();
  }

}

 

하지만 위 코드에는 두 가지 문제가 있습니다.

 

1. 테스트의 어려움

위 코드에서 AlarmService만 온전히 테스트할 수 없습니다. 인프라 계층에 속하는 A가 완벽하게 동작해야만 AlarmService를 테스트할 수 있습니다.

 

2. 확장 및 변경이 어려움

만약 알람 서비스에 B사가 추가된다면 어떻게 될까요? 아래와 같이 서비스 코드를 변경해야합니다.

/**
 * B사의 알림 서비스
 */
public class B {
  public String beep() {
    return "beep";
  }
}
/**
* 서비스 코드
*/
public class AlarmService {

	private A alarmA;
  private B alarmB;

  public String beep(String company) {
		if (company.equals("A")) {
			return alarmA.beep();
		} else {
		   return alarmB.beep();
		}
  }
}

만약 여기서 C사의 알림서비스가 추가된다면 또 서비스 코드를 바꿔야 합니다. C사의 알림 서비스 메소드를 추가하거나, if 문을 사용해서 B사의 알림 서비스를 사용할지, C사의 알림 서비스를 사용할지 정해야합니다. 이런 식으로 인프라 계층이 바뀌거나 추가될 때마다 서비스 코드를 바꾸는 것은 좋은 코드가 아닙니다.

여기서 DIP를 적용하면 위 문제를 해결할 수 있습니다.

 

👭 DIP

방금 설명한 기능을 고수준 모듈과 저수준 모듈로 분리하면 아래와 같습니다.

  • 고수준 모듈 : 알림
  • 저수준 모듈 : A사의 알림 서비스, B사의 알림 서비스

지금까지 사용한 방법은 고수준 모듈이 저수준 모듈에 의존하는 방법이지만, DIP를 적용하게 되면 저수준 모듈이 고수준 모듈에 의존해야 합니다. 그러기 위해 사용하는 것이 추상 타입(ex. 인터페이스, 추상 클래스)입니다.

public interface Alarm {
  String beep();
}

저수준 모듈이 상속 받을 Alarm 인터페이스를 만들어줍니다.

 

/**
 * A사의 알람 서비스
 */
public class A implements Alarm {
  @Override
  public String beep() {
    return "beep!";
  }
}
/**
 * B사의 알림 서비스
 */
public class B implements Alarm {
  @Override
  public String beep() {
    return "beep";
  }
}

그 후에 저수준 모듈들이 Alarm을 구현하게 하면 됩니다. 그렇게 되면 서비스 코드를 아래와 같이 변경할 수 있습니다.

 

/**
* 서비스 코드
*/
public class AlarmService {

  private Alarm alarm;

  public AlarmService(Alarm alarm) {
    this.alarm = alarm;
  }

  public String beep() {
    return alarm.beep();
  }

}

더 이상 AlarmService는 알람 서비스가 추가된다고 코드를 변경하거나 추가해야 하는 일이 없어집니다.

또한, 알람 관련 객체는 무조건 인터페이스인 Alarm에 의존하기 때문에 Alarm을 Mock 객체로 만들어 다양한 시나리오로 AlarmService 기능을 온전히 테스트할 수 있다는 장점도 가져갈 수 있습니다.

 

코드를 클래스 다이어그램으로 나타내면 위와 같습니다. 즉, 저수준 모듈이 고수준 모듈에 의존하게 되는 것을 DIP(의존관계 역전 원칙)라고 합니다.

만약 위 예제와 같은 문제가 발생한 적이 있다면, DIP를 적용해보면 좋을 것 같습니다!

혹시 글을 읽으면서 잘못된 내용이 있으면 댓글로 알려주시면 감사하겠습니다! 읽어주셔서 감사합니다! 😊

 

🌛 참고

DDD START!