Justin의 개발 로그

객체 지향 프로그래밍에서 의존관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 이 원칙을 따르면, 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다. 이 원칙은 다음과 같은 내용을 담고 있다.

  첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.

  둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

이 원칙은 '상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다'는 객체 지향적 설계의 대원칙을 제공한다.

 

 

의존 관계 역전 원칙은 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 된다는 원칙이다.
간략히 말해서 쓰임 당하는 메서드를 하위 모듈, 하위 모듈을 사용하는 입장의 메서드를 상위 모듈이라고 하는데 상위 모듈이 하위 모듈에 대해서 의존하면 안된다는 얘기이다.

간단히 설명하면 상위모듈이 하위 모듈의 함수를 가져다 써서 하위 모듈의 함수가 변하게 될 때 상위 모듈의 동작에 문제가 생기는 경우를 의미한다.

일반적인 해결책은 추상 클래스로 상위 모듈과 하위 모듈 사이에 추상화 레이어를 만드는 것이다.
이를 통해 기존 코드를 수정하지 않고 새 하위 모듈을 자유롭게 가져다 쓸 수 있는데, 사실 이는 개방-폐쇄 원칙을 지키는 것과 같은 방법이다. 고로 의존 관계 역전 원칙은 개방-폐쇄 원칙을 지키는 하나의 방법으로 이해하면 더 좋을 것 같다.

 

의존관계 역전 원칙을 고려하지 않은 클래스 설계

전설의 RPG 게임 YS(이스)를 80년대 추억의 터미널 게임으로 개발해 봅시다.

주인공 아돌 크리스틴이 되어 단검 한자루를 들고 슬라임과 각종 몬스터가 등장하는 게임 속에서 몬스터를 무찌르고 성장하면서 
최종 보스를 물리쳐야 합니다.

// 단검 - 게임 초기에 제공되는 기본검 정의
public class Sword {
  protected String name = "단검";
  protected int attackPower = 1; 
  
  public void slash(EnemyCharater otherCharacter) {
    System.out.println(name + "으로 " + otherCharacter.name + "을 배었다.");
    otherCharacter.getDamage(this.attackPower);
  }
}

// 게임 초반에 얻게 되는 강철검
public class SteelSword extends Sword {
  public SteelSword() {
  	this.name = "Steel Sword";
    this.attackPower = 2;
  }
}

// 적 케릭터
public class EnemyCharater {
  protected String name;
  protected int hp;
  protected int attackPower; 
  
  public EnemyCharater (String name, int hp, int attackPower) {
    this.name = name;
    this.hp = hp;
    this.attackPower = attackPower;
  }
  
  public void getDamage(int damage) {
    hp = hp - damage;
    
    System.out.println(name + " HP :" + hp);
  }
    
}

// 주인공
public class HeroCharacter {
  private final String name = "Adol Christin";  //이스 주인공
  private int hp = 100; // 초기 체력 Hits point 
  private Sword sword;	//검 (공격 무기)
  
  public HeroCharacter () {
     sword = new Sword();  //주인공은 기본 무기 단검을 가지고 있음
  }
  
  public void attack(EnemyCharater otherCharacter) {
    if(sword == null) 
      return;
      
    sword.slash(otherCharacter);
  }
  
  public void getSword(Sword newSword) {
    sword = newSword
    
    system.out.println(name + " get a " + sword.name);
  }
}

public class GamePlay {
  public void main(String[] args) {
  
    HeroCharacter hero = new HeroCharacter();
    EnemyCharater slime = new EnemyCharater("슬라임",10,1);
    
    hero.slash(slime);
    hero.slash(slime);

    hero.getSword(new SteelSword());

    hero.slash(slime);
    hero.slash(slime);
  
  }
} 

/*
[Terminal]
단검으로 슬라임을 베었다.
슬라임 HP : 9
단검으로 슬라임을 베었다.
슬라임 HP : 8
Adol Christin get a Steel Sword
강철검으로 슬라임을 베었다.
슬라임 HP : 6
강철검으로 슬라임을 베었다.
슬라임 HP : 4
*/

게임의 재미를 위해 활(Bow) 을 추가한다.

 

/*
Sword, SteelSword, EnemyCharater 생략
*/

// 활 - 가장 기본 활
public class Bow {
  protected String name = "나무활";
  protected int attackPower = 1; 
  
  public void shoot(EnemyCharater otherCharacter) {
    System.io.println(name + "(으)로 " + otherCharacter.name + "을 맞췄다.";
    otherCharacter.getDamage(this.attackPower);
  }
}

// 게임 초반에 얻게 되는 강철활
public class SteelBow extends Bow {
  public SteelSword() {
  	this.name = "Steel Bow";
    this.attackPower = 2;
  }
}

// 주인공
public class HeroCharacter {
  private final String name = "Adol Christin";  //이스 주인공
  private int hp = 100; // 초기 체력 Hits point 
  private Sword sword;	//검 (공격 무기)
  private Bow bow;	  //활 (장거리 공격 무기)
  
  public HeroCharacter () {
     sword = new Sword();  //주인공은 기본 무기 단검을 가지고 있음
  }
  
  public attack(EnemyCharater otherCharacter) {
    if(sword != null) 
    {
      sword.slash(otherCharacter);
      return;
    } else if(bow != null) {
      bow.shoot(otherCharacter);
    }
  }
  
  public getSword(Sword newSword) {
    sword = newSword;
    bow = null;
    system.io.println(name + " get a " + sword.name);
  }
  
  public getBow(Bow newBow) {
    sword = null;
    bow = newBow;
    system.io.println(name + " get a " + bow.name);
  }
}

public class GamePlay {
  public void main(String[] args) {
  
    HeroCharacter hero = new HeroCharacter();
    EnemyCharater slime = new EnemyCharater("슬라임",10,1);
    
    hero.attack(slime);
    hero.attack(slime);

    hero.getSword(new SteelSword());

    hero.attack(slime);
    hero.attack(slime);
    
    hero.getBow(new SteelBow());
    hero.attack(alime);
  }
} 

/*
[Terminal]
단검(으)로 슬라임을 베었다.
슬라임 HP : 9
단검(으)로 슬라임을 베었다.
슬라임 HP : 8
Adol Christin get a Steel Sword
강철검(으)로 슬라임을 베었다.
슬라임 HP : 6
강철검(으)로 슬라임을 베었다.
슬라임 HP : 4
Adol Christin get a Steel Bow
강철활(으)로 슬라임을 맞췄다.
슬라임 HP : 2
*/

main() 메소드에서 활을 사용하는 게임 시나리오를 추가했다. (OK)

  • 무기(Bow) 추가로 인해 클래스 HeroCharacter의 여러 곳이 영향을 받았다. 
  • attack 메서드의 if, else if 등 복잡도가 증가했다. 무기가 추가될 때 마다 attack 메소드가 지저분해 질 것이다. 생각만해도 끔찍하다.

 

의존관계역전을 없애려면

  • 상위 모듈과 하위 모듈 모두 추상화(abstract class, interface)에 의존
  • 세부사항(Class, Method)이 추상화(abstract, interface)에 의존해야 한다.
/*
상위 모듈과 하위 모듈 모두 추상화(abstract class, interface)에 의존
세부사항(Class, Method)이 추상화(abstract, interface)에 의존해야 한다.
*/


// 전략 패턴 : 구상 클래스에 동일하게 구현해야 하는 공통 메소드를 인터페이스로 정의
//    변경 가능한 행동을 캡슐화하고, 구상(구현) 클래스로 로직을 위임
public interface IWeapon {
    void attack(EnemyCharacter otherCharacter);
    void showAttackStatus(String targetName);
    void damage(EnemyCharacter otherCharacter);
}


/* 
    추상 클래스를 통해 추상화 함으로서 
    상위 모듈(하위 모듈을 사용하는 측)에서는 자식 클래스의 확장이나 로직 추가로 인해
    영향도가 발생하지 않으며, 추가된 하위 모듈의 사용이 가능하도록 했다.
    
    즉, 의존관계 역전 원칙을 준수하도록 설계됨
*/
public abstract class Weapon implements IWeapon {
    protected String name;
    protected int attackPower;

    public Weapon(String name, int attackPower) {
        this.name = name;
        this.attackPower = attackPower;
    }

    /*
	// 템플릿 메소드 패턴 : 
    // 로직(알고리즘)의 구조는 유지하면서 로직의 일부 단계를 서브클래스에서 구현할 수 있게 한다.    
    //   attack에서 공격상태를 보여주고, 데미지를 차감하는 로직의 순서는 유지하면서
    //   상태를 보여주거나(showAttack), 데미지를 차감하는 로직은 각 서브클래스에서 구현할 수 있게 했다.
    */
    @Override
    public void attack(EnemyCharacter otherCharacter) {
        // 공격 상태를 보여준다.
        showAttackStatus(otherCharacter.name);
        damage(otherCharacter);
    }

    @Override
    public void showAttackStatus(String targetName) {
        System.out.println(name + "(으)로 " + targetName + "(을)를 공격했다.");
    }

    @Override
    public void damage(EnemyCharacter otherCharacter) {
        otherCharacter.getDamage(attackPower);
    }
}

public class Sword extends Weapon {
    public Sword() {
    super("단검", 1);
}

    public Sword(String name, int attackPower) {
        super(name, attackPower);
    }

    @Override
    public void showAttackStatus(String targetName) {
        System.out.println(name + "(으)로 " + targetName + "(을)를 베었다.");
    }
}

public class SteelSword extends Sword {
    public SteelSword() {
        super("강철검", 2);
    }
}

public class Bow extends Weapon {
    public Bow() {
        super("나무활", 1);
    }

    public Bow(String name, int attackPower) {
        super(name, attackPower);
    }

    @Override
    public void showAttackStatus(String targetName) {
        System.out.println(name + "(으)로 " + targetName + "(을)를 쐈다.");
    }
}


public class SteelBow extends Bow {
    public SteelBow() {
        super("강철활", 2);
    }
}


// 적 케릭터
public class EnemyCharacter {
    protected String name;
    protected int hp;
    protected int attackPower;

    public EnemyCharacter (String name, int hp, int attackPower) {
        this.name = name;
        this.hp = hp;
        this.attackPower = attackPower;
    }

    public void getDamage(int damage) {
        hp = hp - damage;

        System.out.println(name + " HP :" + hp);
    }
}

// 주인공
public class Hero {
    private final String name = "Adol Christin";  //이스 주인공
    private int hp = 100; // 초기 체력 Hits point
    private Weapon weapon;	// 무기

    public Hero () {
        weapon = new Sword();  //주인공은 기본 무기 단검을 가지고 있음
    }

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

    public void getWeapon(Weapon newWeapon) {
        weapon = newWeapon;
        System.out.println(name + " 이 새로운 무기 " + weapon.name + "(을)를 얻었다.");
    }
}


public class PlayGame {
    public static void main(String[] args)
    {
        Hero hero = new Hero();
        EnemyCharacter slime = new EnemyCharacter("슬라임",10,1);

        hero.attack(slime);
        hero.attack(slime);

        hero.getWeapon(new SteelSword());

        hero.attack(slime);
        hero.attack(slime);

        hero.getWeapon(new SteelBow());

        hero.attack(slime);
        hero.attack(slime);

    }
}

> Task :GamePlay.main()
단검(으)로 슬라임(을)를 베었다.
슬라임 HP :9
단검(으)로 슬라임(을)를 베었다.
슬라임 HP :8
Adol Christin 이 새로운 무기 강철검(을)를 얻었다.
강철검(으)로 슬라임(을)를 베었다.
슬라임 HP :6
강철검(으)로 슬라임(을)를 베었다.
슬라임 HP :4
Adol Christin 이 새로운 무기 강철활(을)를 얻었다.
강철활(으)로 슬라임(을)를 쐈다.
슬라임 HP :2
강철활(으)로 슬라임(을)를 쐈다.
슬라임 HP :0
  • 새로운 무기를 추가할 때 영향 받는 클래스가 없다. - 새로운 무기가 등장해야 하는 main() 메소드(게임 시뮬레이션) 제외
  • 상위 모듈, 하위 모듈 모두 Weapon 추상 클래스에 의존하도록 만들었으며, 
  • 세부사항(하위모듈의 실제 로직 - attach, showAttachStatus, damage)의 구현도 IWeapon을 구현하도록 했으며, 중복 로직은 Weapon의 메소드 및 무기원형(Sword, Bow)에 한 번만 구현하도록 했다.
  • Sword, Bow의 무한 확장에도 영향받는 클래스가 없다.
  • 사용된 주된 패턴은 디자인 패턴의 초반에 등장하는 전략 패턴(Strategy Pattern) 및 템플릿 메소드 패턴(Template Method Pattern) 
    • 디자인패턴은 대부분 객체지향 5원칙을 준수해서 설계가 되었다.
    • 의존관계 역전 원칙을 준수하도록 설계된 패턴의 대표적인 예는 전략 패턴, 템플릿 메소드 패턴, 커맨드 패턴 등이 있다.
      (사실상 대부분 원칙을 준수함)
profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!