티스토리 뷰

Java

DesignPattern#01. Strategy Pattern

LichKing 2019. 9. 21. 16:19

1. 코드 스멜

게임을 만든다고 생각해보자. 각각의 몬스터들을 객체로 만드려고한다. 객체를 만드려면 일단 클래스가 있어야하니 클래스를 정의하자.

public abstract class Monster {
    public abstract void attack();
}

몬스터들간의 다형성을 위해 Monster 추상 클래스를 정의했다. 이번 포스팅은 attack() 메서드부분을 리팩토링해나가는 과정을 작성하려한다. 그래서 메서드는 attack() 만 간단하게 정의했다.

public class Knight extends Monster {
    @Override
    public void attack() {
        System.out.println("칼 공격!");
    }
}

Knight 몬스터를 정의했다. Knight 몬스터 외에 Skeleton 몬스터도 추가하자.

public class Skeleton extends Monster {
    @Override
    public void attack() {
        System.out.println("칼 공격!");
    }
}

Knight 와 Skeleton 은 모두 단거리 몬스터로 칼로 공격을 한다. 슬슬 코드 스멜을 느끼는 분도 계시겠지만 아직은 못느낄수도있다.

public class Warrior extends Monster {
    @Override
    public void attack() {
        System.out.println("칼 공격!");
    }
}

Warrior 몬스터를 추가로 정의했다. 이 몬스터 역시 칼로 공격을 한다. 이전까지 못느꼈던 분들도 이제는 코드 스멜을 느껴야한다. attack() 메서드가 모든 구현체마다 동일한 상태다. 이는 코드 중복이고, 칼 공격을 하는 메서드에 변경을 해야하면 3개의 클래스를 모두 수정해줘야한다. 이를 어떻게 해결할 수 있을까?

 

2. 상속

public abstract class Monster {
    public void attack() {
        System.out.println("칼 공격!");
    }
}

추상 클래스에 추상 메서드로 존재하던 attack() 메서드를 구현 클래스로 변경했다. 확실히 이렇게하면 앞선 3개의 클래스에 존재하는 중복코드는 모두 없앨 수 있을것 같다. 하지만 몬스터는 칼을 쓰는 몬스터만 있는게 아니다.

public class Sniper extends Monster {
    @Override
    public void attack() {
        System.out.println("총 공격!");
    }
}
public class Fighter extends Monster {
    @Override
    public void attack() {
        System.out.println("주먹 공격!");
    }
}

스나이퍼와 파이터 몬스터를 추가했다. 이들은 각각 총과 주먹으로 공격한다. 지금처럼 몬스터의 종류가 총 5개에 불과하다면 지금과 같은 방식도 코드재사용 관점에서는 크게 나쁘지않은것 같다. 하지만 몬스터의 종류가 점점 추가되고 총과 주먹을 이용하는 몬스터가 늘어난다면 앞선 칼을 이용한 몬스터들에서 발생했던 중복코드가 똑같이 재현되게 될것이다.

 

현재 구현된 상태의 클래스다이어그램이다.

3. 문제해결

위에서 맞딱뜨린 문제는 어떻게 해결할 수 있을까? 일단 문제가 뭔지 파악하기위해 현재의 요구사항을 정리해보자.

 

- 몬스터들은 공격방법이 제각각이다.

- 제각각인 와중에도 동일한 공격방법을 취하는 몬스터들이 있다.

 

요구사항을 정리해보니 애초에 각각의 몬스터들이 공격방법을 직접 구현한게 문제인것같다. 현재와 같은 방식으로는 처음부터 중복코드는 발생할 수 밖에 없던 구조인것이다. 이 문제는 현재 상태에서 2가지 방법으로 해결할 수 있어보인다. 첫번째 방법은 현재까지 해오던대로 상속을 이용하는 것이다. 상속을 이용해서 어떻게 해결할 수 있을까? 몬스터 계층도를 중간에 하나 더 추가하는것이다. 가령 예를들어 SwordMonster 라는 추상계층을 추가한다면 해결할 수 있다.

public abstract class Monster {
    public abstract void attack();
}

Monster 클래스는 처음과 같이 추상 메서드로 만들고

public abstract class SwordMonster extends Monster {
    @Override
    public void attack() {
        System.out.println("칼 공격!");
    }
}

중간에 SwordMonster 클래스를 하나 더 만든다음 Knight, Warrior, Skeleton 은 SwordMonster 를 상속받게 하는것이다. 마찬가지로 총은 GunMonster 를 만든다음 이를 상속받게하면 될것이다.

 

현재의 코드베이스에서는 이렇게 해결하면 지금까지 얘기해온 문제들은 모두 해결이 될 수 있다. 하지만 지금은 철저한 샘플코드임을 생각해보자. 몬스터들이 attack() 메서드만 갖고있는게 아니라 소리를 내는 sound() 메서드도 갖고있다고 생각해보자. 그리고 Knight, Skeleton 은 동일한 소리를 내고 Warrior 는 다른 소리를 낸다면 어떻게해야할까? sound() 를 각각의 최하위 클래스들이 구현하게 만들면 attack() 에서 발생했던 문제와 동일한 문제가 발생할 것이다. 그럼 SwordMonster 와 최하위 클래스 사이에 추상계층을 또하나 추가해야할까? 만약 Knight, Skeleton 의 sound() 구현이 같고, Warrior 와 Sniper 의 sound() 구현이 같다면 이는 또 어떻게 해결할 것인가? 이런 다양한 요구사항을 모두 상속으로 풀려고하면 엄청난 수의 클래스들이 추가될것이다.

 

4. 컴포지션(Composition)

위에서 이 문제를 해결할 수 있는 방법은 2가지가 있다고했다. 그리고 그 중 하나인 상속을 통해 해결했을때의 문제를 위에서 확인해봤다. 이번엔 2번째 해결방식을 살펴보자. 이펙티브자바에는 다음과 같은 아이템이 하나있다.

 

Item18. 상속보다는 컴포지션을 사용하라

 

대표적으로 이펙티브자바를 언급한것일뿐 상속보다 컴포지션을 사용하라는 격언은 각종 디자인 격언에서 마주하는 내용이다. 그럼 지금 이 문제를 어떻게 컴포지션을 통해 해결할 수 있을까? 위와같은 문제의 핵심은 공격방식을 몬스터와 그 하위 클래스에서 직접 구현하기때문에 발생하는 문제다. 문제를 아예 다른각도로봐보고, 공격방식에 대한 클래스 계층을 별도로 설계하자.

public interface Attackable {
    void attack();
}

Attackable 이라는 인터페이스를 추가했다. 이제 각각에 대한 공격내용은 이 인터페이스를 구현한다.

public class SwordAttack implements Attackable {
    @Override
    public void attack() {
        System.out.println("칼 공격");
    }
}

public class GunAttack implements Attackable {
    @Override
    public void attack() {
        System.out.println("총 공격");
    }
}

칼과 총에 대한 구현체를 추가했다. 이 코드는 다른곳에 중복으로 구현하고싶어도 마땅히 중복 구현할 여지가 없어보인다.

public abstract class Monster {
    private Attackable attackable;

    public Monster(Attackable attackable) {
        this.attackable = attackable;
    }

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

이제 무기를 장착할 수 있게 Monster 클래스를 수정해야할 차례다. 위와 같이 인스턴스 필드로 Attackable 인터페이스를 참조하게 만들자.

public class Warrior extends Monster {
    public Warrior(Attackable attackable) {
        super(attackable);
    }
}

Warrior 와 같은 Monster 구현체들도 상위 클래스인 Monster 의 생성자만 호출할 수 있도록 구현해주면 칼, 총, 주먹 그리고 그 이상의 무기들도 장착함에 따라서 유연하게 무기를 사용할 수 있다.

public interface Soundable {
    void sound();
}

public class HiSound implements Soundable {
    @Override
    public void sound() {
        System.out.println("hi");
    }
}

public abstract class Monster {
    private Attackable attackable;
    private Soundable soundable;

    public Monster(Attackable attackable, Soundable soundable) {
        this.attackable = attackable;
        this.soundable = soundable;
    }

    public void attack() {
        attackable.attack();
    }
    
    public void sound() {
        soundable.sound();
    }
}

attack() 뿐만 아니라 sound() 가 추가되더라도 상속으로 해결할때처럼 폭발적인 클래스 증가가 아니라 인터페이스와 구현체 하나를 추가해주는 수준에서 요구사항을 반영할 수 있다. 이렇게 요청받은 객체가 자신이 직접 처리하지않고 내부에 갖고있던 필드로 요청을 전달하는걸 forwarding 이라고한다.

 

5. 무기교체

이제 거의 다 왔다. 컴포지션을 통해 요구사항 추가와 상속으로 해결할때의 문제점을 모두 해결해냈다. 다만 위 코드는 생성자를 통한 주입만 받고있어 객체가 완성된 이후에는 런타임시에 무기를 교체할수없다. 런타임시에 무기를 교체할 수 있도록해보자.

public abstract class Monster {
    private Attackable attackable;
    private Soundable soundable;

    public Monster(Attackable attackable, Soundable soundable) {
        this.attackable = attackable;
        this.soundable = soundable;
    }

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

    public void sound() {
        soundable.sound();
    }

    public void setAttackable(Attackable attackable) {
        this.attackable = attackable;
    }

    public void setSoundable(Soundable soundable) {
        this.soundable = soundable;
    }
}

흔히 setter 라고 부르는 메서드들이 추가됐다. setter 는 다들 익숙할거라고 생각한다. 이렇게 내부 필드를 교체할 수 있는 setter 를 제공함으로써 Monster 클래스의 전략(strategy)을 런타임시에 교체할 수 있게됐다. Knight 몬스터가 칼을 들고 공격하다가 setter 를 호출해서 활로 바꿔들고 원거리 공격을 할수도 있게된 셈이다. 이렇게 일련의 알고리즘을 런타임시에 교체해가며 유연하게 사용하는걸 strategy pattern 이라 한다.

 

strategy pattern 은 추상화된 타입에 의존하여 다형성을 이용해서 그 구현체들을 취할수있는게 핵심이다. 다음은 strategy pattern을 적용한 이후의 클래스 다이어그램. Monster 클래스가 추상화된 인터페이스인 Attackable 에 의존하고있으며 그 구현체인 SwordAttack 이나 GunAttack 의 존재는 알지 못한다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함