[꼼꼼한 개발자] 꼼코더

[JAVA] - DIP란?(Dependency inversion principle, 의존관계 역전 원칙이란?, 의존관계 역전 원칙 예제) 간단하고 쉽게 이해하기 본문

간단하고 쉽게/JAVA

[JAVA] - DIP란?(Dependency inversion principle, 의존관계 역전 원칙이란?, 의존관계 역전 원칙 예제) 간단하고 쉽게 이해하기

꼼코더 2023. 6. 12. 23:56
반응형

🧹 간단 요약

DIP란 구체화의 의존하지 말고 추상화의 의존하는 것.

쉽게 말해 "구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻"

 

(자세한 내용은 아래에)

 

 

🤷🏻 DIP란?(Dependency inversion principle)

DIP는 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 되며

양쪽 모듈 모두 추상화에 의존해야 한다는 원칙이다.

 

즉, 의존성은 추상화에 의존해야 하며, 세부 구현에 의존해서는 안 된다는 것을 의미한다.

쉽게 말 해 "구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻"

 

 

🙋🏻‍♂️ DIP에 의거한 인터페이스 의존 방법

  1. 상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다.
  2. 두 모듈은 모두 추상화에 의존해야 한다.
  3. 추상화는 세부 구현에 의존해서는 안 된다.
  4. 상위 수준 모듈은 하위 수준 모듈을 추상화한 인터페이스 또는 추상 클래스에 의존해야 한다.

(잘 모르겠지만 아래 예제를 통해 개념을 확실히 잡아보자)

 

🏹 DIP를 지키지 않는 예제

전투 시스템을 구현해 보자.

공격을 수행하는 서비스 클래스에는 '검' 무기를 사용하려고 한다.

 
/**
 * 검 무기 
 */
public class Sword {
  public void attack() {
    System.out.println("검으로 베기!");
  }
}

/**
 * 전투 서비스 코드
 */
public class BattleService {

  private Sword weapon;

  public void attack() {
    weapon.attack();
  }
}

 

👀 2가지의 문제점

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

 

  1. 테스트의 어려움 : '검'클래스가 완벽하게 동작하지 않으면 온전히 BattleService를 테스트할 수 없다.
  2. 확장 및 변경의 어려움 : 만약 '활' 무기가 추가된다면 어떻게 될까? 아래와 같이 서비스 코드를 변경해야 한다.
/**
 * 활 무기
 */
public class Bow {
  public void attack() {
    System.out.println("Attacking with bow!");
  }
}

/**
 * 전투 서비스 코드
 */
public class BattleService {

  private Sword sword;
  private Bow bow;

  public void attack(String weaponName) {
    if (weaponName.equals("Sword")) {
      sword.attack();
    } else {
      bow.attack();
    }
  }
}

만약 여기서 '도끼' 무기 서비스가 추가된다면 또 서비스 코드를 변경해 줘야 한다.

'도끼' 무기 서비스 메서드를 추가하거나, if 문을 사용해서 '활'을 사용할지, '도끼'를 사용할지 정해야 한다.

 

이런 식으로 추가사항에 관련하여 서비스 코드를 바꾸는 것은 좋은 코드가 아니다.

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

 


⚔️ DIP를 지키는 예제

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

  • 고수준 모듈: 전투 서비스
  • 저수준 모듈: 검, 활, 도끼

 

지금까지 사용한 방법은 고수준 모듈이 저수준 모듈에 의존하는 방법이지만

DIP를 적용하게 되면 저수준 모듈이 고수준 모듈에 의존해야 한다.

 

그러기 위해 사용하는 것이 추상 타입 (예: 인터페이스, 추상 클래스)이다. -> Weapon

 

저수준 모듈(무기들)이 상속받을 Weapon 인터페이스를 만들고.

그 후에 저수준 모듈들이 Weapon을 구현하게 해 보자.

/**
 * 저수준 모듈(무기들)이 상속받을 Weapon 인터페이스
 */
public interface Weapon {
  void attack();
}

/**
 * 검 무기
 */
public class Sword implements Weapon {
  @Override
  public void attack() {
    System.out.println("검으로 베기!");
  }
}

/**
 * 활 무기
 */
public class Bow implements Weapon {
  @Override
  public void attack() {
    System.out.println("활로 조준 후 공격!");
  }
}

 

그렇게 되면 서비스 코드를 아래와 같이 변경할 수 있다.

/**
 * 고수준 모듈 영역
 * 전투 서비스 코드
 */
public class BattleService {

  private Weapon weapon;

  public BattleService(Weapon weapon) {
    this.weapon = weapon;
  }

  public void attack() {
    weapon.attack();
  }
}

 

/**
 * 실행 영역 (메인 클래스)
 */
public class Main {
  public static void main(String[] args) {
    Weapon sword = new Sword();
    Weapon bow = new Bow();

    BattleService battleService = new BattleService(sword);
    battleService.attack(); // 검으로 공격

    battleService = new BattleService(bow);
    battleService.attack(); // 활로 공격
  }
}

이제 무기 종류를 추가하거나 변경하는 경우에도 BattleService 코드를 수정하지 않고 확장할 수 있다.

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

이렇게 DIP를 적용하면 무기 서비스에 대한 확장과 변경이 용이해지고 테스트 가능성이 향상됩니다.


🙋🏻‍♂️ 추가 설명 (Mock 객체)

Mock 객체는 실제 동작을 가지지 않고

특정 시나리오를 시뮬레이션하거나 원하는 대로 동작하도록 프로그래밍된 객체이다.

 

따라서 Weapon 인터페이스를 구현한 Mock 객체를 생성하여 BattleService에 주입하면

원하는 시나리오로 BattleService의 동작을 테스트할 수 있다.

🏄🏻 예시

특정 무기가 공격 메서드를 호출하는지 여부를 확인하거나

특정 순서로 다양한 무기를 전달하는 등의 테스트 시나리오를 구현할 수 있다.

 

 

 

 

🧹 마무리 

스프링으로 어노테이션을 사용해 가면서 하면 간단하게 구현하여 무지했던 내용이지만

나름 복잡하거나 쉽게 잊어버릴 수 있을 거 같아서 자주 복습해야겠다..

 

 

 

 

 

 

 

 

Comments