디자인 패턴 - State Pattern

bp.chys·2020년 6월 7일
1

OOP & Design Pattern

목록 보기
14/17

상태 패턴

  • 동일한 동작을 수행될 때마다 상황(객체 상태)에 따라서 다른 동작을 하도록 하는 디자인 패턴
  • 변하는 부분을 캡슐화해야 한다. 변하는 것은 상태(State)이므로 상태를 인터페이스로 분리시키고 각 상태에 따른 구현 클래스를 만든다.
  • 전략 패턴과 다른 점은 객체가 동작을 수행할 때, 자신의 상태를 변경하기 위하여 자기 자신을 메소드 매개변수로 전달한다는 점이다.

예시

  • 단일 상품을 판매하는 자판기에 들어갈 소프트웨어를 구현한 코드를 살펴보자.
  • 자판기는 현재 투입된 동전이 없는 상태인지, 아니면 제품 선택 가능한 상태인지에 대한 두 상태 기준으로 로직을 분기할 수 있다.
public class VendingMachine {
    public static enum State { NOCOIN, SELECTABLE }
    
    private State state = State.NOCOIN;
    
    public void insertCoin(int coin) {
        switch(state) {
        case NOCOIN:
            increaseCoin(coin);
            state = State.SELECTABLE;
            break;
        case SELETABLE:
            increaseCoin(coin);
        }
    }
    
    public void select(int productId) {
        switch(state) {
        case NOCOIN:
            break;
        case SELECTABLE:
            provideProduct(productId);
            decreaseCoin();
            if (hasNoCoin()) {
                state = State.NOCOIN;
            }
        }
    }
    ... // increaseCoin, provideProduct, decreaseCoin 구현
}
  • 여기에 만약 자판기에 제품이 없는 경우 동전을 넣으면 바로 동전을 되돌려 준다는 요구사항이 추가되었다고 하자.
  • 그러면 제품이 없는 상태를 표현할 수 있는 SOLDOUT 이라는 state를 Enum에 추가해야 하고, insertCoint 부분에 해당 case를 추가해야 한다.
case SOLDOUT:
    returnCoin();
  • 전략 패턴에서 새로운 정책이 추가될 때마다 if-else 분기가 늘어나는 것과 비슷한 모습이다.
  • 여기서는 상태에 따라 취하는 액션이 다른 경우이기 때문에 상태(state) 패턴을 적용할 수 있다.
  • 상태 패턴에서 중요한 점은 상태 객체가 기능을 제공한다는 점이다.
public interface State {
    State increaseCoin(int coin, VendingMachine vm);

    State select(int productId, VendingMachine vm);
}
  • 위 인터페이스 처럼 State 인터페이스는 동전 증가 처리제품 선택 처리를 할 수 있는 두개의 메서드를 정의하고 있다.
  • 이 두 메서드는 모든 상태에 동일하게 적용되는 기능이다.
  • 이제 위 인터페이스를 구현하는 세 가지 실제 상태 객체를 살펴보면 다음과 같다.
public class NoCoin implements State {
    @Override
    public State increaseCoin(final int coin, final VendingMachine vm) {
        vm.increaseCoin(coin);
        if (vm.isCoinEmpty()) {
            return new NoCoin();
        }
        return new Selectable();
    }

    @Override
    public State select(final int productId, final VendingMachine vm) {
        throw new UnsupportedOperationException();
    }
}

public class Selectable implements State {
    @Override
    public State increaseCoin(final int coin, final VendingMachine vm) {
        vm.increaseCoin(coin);
        return this;
    }

    @Override
    public State select(final int productId, final VendingMachine vm) {
        vm.provideProduct(productId);
        vm.decreaseByProductPrice(productId);
        if (vm.isCoinEmpty()) {
            return new NoCoin();
        }
        return this;
    }
}

public class SoldOut implements State {
    @Override
    public State increaseCoin(final int coin, final VendingMachine vm) {
        return new NoCoin();
    }

    @Override
    public State select(final int productId, final VendingMachine vm) {
        throw new UnsupportedOperationException();
    }
}
  • 각각의 state값들은 공통의 기능을 구현하면서, State를 가지고 있는 객체의 변수 상태와 상황에 맞게 새로운 State를 반환한다.
  • State 에따라 메서드를 지원하지 않는 경우는, 예외를 발생하게 만든다.
public class VendingMachine {
    private final static List<Product> items = new ArrayList<>();

    static {
        items.add(new Product(1, 100));
        items.add(new Product(2, 200));
        items.add(new Product(3, 300));
        items.add(new Product(4, 400));
    }

    private State state;
    private int coin; // coin을 state 내부에서 관리하도록 할 수도 있다.

    public VendingMachine() {
        state = StateFactory.create();
    }

    public void insertCoin(final int coin) {
        state = state.increaseCoin(coin, this);
    }

    public void select(final int productId) {
        state = state.select(productId, this);
    }

    public boolean isCoinEmpty() {
        return coin == 0;
    }

    public void increaseCoin(final int coin) {
        this.coin += coin;
    }

    public void decreaseByProductPrice(final int productId) {
        this.coin -= items.get(productId).getPrice();
    }

    public Product provideProduct(final int productId) {
        return items.get(productId);
    }
}
  • 현재는 상태를 상태 객체 내부 메서드에서 변경하고 있지만, 외부 컨텍스트에서 변경하는 방법도 생각해볼 수 있다.
  • 외부 컨텍스트에서 변경하는 방법은 상태의 개수가 적고, 상태 변경 규칙이 거의 바뀌지 않는 경우에 유리하다.
  • 반면 상태 객체 내부에서 변경할 경우, 외부 컨텍스트에 영향을 미치지 않는다는 장점이 있지만, 상태 구현 클래스가 많아질 경우, 변경 규칙을 파악하기 어려워지는 단점이 있다.

SOLID 관점에서 본 상태 패턴

  • 단일 책임 원칙 : State 구현과 사용을 분리
  • 개방 폐쇄 원칙 : 새로운 상태가 추가 되어도 이를 사용하는 코드는 변경 필요없음.
  • 리스코프 치환 원칙 : 상태 인터페이스에 정의된 메서드를 상태 구현체가 올바른 용도로 재정의하고 있음
  • 인터페이스 분리 원칙 : 상태에 따라 사용하는 메서드와 사용하지 않는 메서드가 구분되기 때문에 제대로 지켜지지 않고 있음
  • 의존성 역전 원칙 : State 인터페이스를 만들고 사용과 구현 측면에서 모두 인터페이스를 바라보고 있으므로 잘 따르고 있다.

결론

상태 패턴은 객체의 상태에 따라 다른 동작을 구현해야할 때 사용할 수 있는 디자인 패턴이다.
전략패턴과 비슷하여 헷갈릴 수도 있지만 전략패턴은 상속의 한계를 해결하면서, 사용자가 캡슐화된 알고리즘 전략을 쉽게 바꿀 수 있도록 유연성을 제공하는 것에 초점이 맞춰져 있지만, 상태 패턴은 한 객체가 보유한 상태에 따라 동작을 다르게 수행해야 할 때 사용한다.

즉, 다시 말해 전략 패턴은 다형성의 주체가 사용자에 있지만, 상태 패턴은 다형성의 주체가 보유한 상태 객체라고 볼 수 있다.


참고자료

  • 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 최범균
profile
하루에 한걸음씩, 꾸준히

0개의 댓글