본문 바로가기

JAVA/Design Patterns

상태패턴

반응형

상태 패턴


예)

단일 상품을 판매하는 자판기에 들어갈 소프트웨어를 개발 요청


동전을 넣음 -> 동전없으면 -> 금액을 증가 -> 제품선택가능

동전을 넣음 -> 제품선택 가능이면 -> 금액을 증가 -> 제품선택 가능

제품선택 -> 동전없으면 -> 아무동작하지 않음 -> 동전없음 유지

제품선택 -> 제품선택가능이면 -> 제품주고 잔액감소 -> 잔액있으면 제품선택가능 잔액없으면 동전없음


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 SELECTABLE :

increaseCoin(coin);

}

}

public void select(int productId) {

switch(state) {

case NOCOIN :

//아무것도 하지 않음

break;

case SELECTABLE :

providerProduct(productId);

descreaseCoin();

if(hasNoCoin())

state = State.NOCOIN;

}

}

// increaseCoin, providerProduct, decreaseCoin 구현

}


신규 요구사항

- 자판기에 제품이 없는 경우에는 동전을 넣으면 바로 동전을 되돌려 준다.


public class VendingMachine {

public static enum State { NOCOIN, SELECTABLE, SOLDOUT }

private State state = State.NOCOIN;

public void insertCoin(int coin) {

switch(state) {

case NOCOIN :

increaseCoin(coin);

state = State.SELECTABLE;

break;

case SELECTABLE :

increaseCoin(coin);

break;

case SOLDOUT :

returnCoin();

}

}

public void select(int productId) {

switch(state) {

case NOCOIN :

//아무것도 하지 않음

break;

case SELECTABLE :

providerProduct(productId);

descreaseCoin();

if(hasNoCoin())

state = State.NOCOIN;

case SOLDOUT :

//아무것도 하지 않음

}

}

// increaseCoin, providerProduct, decreaseCoin, returnCoin 구현

}


신규요구사항

- 자동세척 중일 때는 동전을 넣으면 바로 돌려줘야 하는 요구 사항이 추가

insertCoin()과 select() 메서드에 또 다른 조건문이 추가될 것이다.


inserCoin()과 select() 메서드는 동일한 구조의 조건문을 갖고 있다.

이는 상태가 많아질수록 복잡해지는 조건문이 여러 코드에서 중복해서 출현하고, 그만큼 코드 변경을 어렵게 만든다.

또 새로운 상태를 추가하거나 기존 상태를 빼려면 모든 조건문을 찾아서 수정해줘야한다.


VendingMachine 클래스

- 상태에 따라 동일한 기능 요청의 처리를 다르게함

insertCoin(), select() 함수에


case NOCOIN

case SELECTABLE

case SOLDOUT


상태에 따라 다르게 동작한다.

이렇게 기능이 상태에 따라 다르게 동작해야 할때 사용할 수 있는 패턴이 상태패턴이다.


상태패턴에서 상태를 별도 타입으로 분리하고, 각 상태별로 알맞는 하위 타입을 구현한다.


상태패턴에서 중요한점은 상태 객체가 기능을 제공한다는 점이다.

State 인터페이스는 동전 증가 처리와 제품 선택 처를 할 수 있는 두 개의 메서드를 정의하고 있다.

이 두 메서드는 모든 상태에서 동일하게 적용되는 기능이다.


콘텍스트는 필드로 상태 객체를 갖고 있다.

콘텍스트는 클라이언트로부터 기능 실행 요청을 받으면, 상태 객체에 처리를 위임하는 방식으로 구현한다.

자반기 기능을 제공하는 VendingMachine 클래스의 increaseCoin(), select() 메서드는 State 객체에 처리를 위임하는 방식으로 동작한다.


public class VendingMachine {

private State state;

public VendingMachine() {

state = new NoCoinState();

}

public void insertCoin(int coin) {

state.increaseCoin(coin, this); //상태객체에 위임

}

public void select(int productId) {

state.select(productId, this); //상태객체에 위임

}

public void changeState(State newState) {

state = newState;

}

// 기타 다른 기능

}


state 필드를 NoCoinState 객체로 초기화했다.

NoCoinState 클래스


public class NoCoinState implements State {

@Override

public void increaseCoin(int coin, VendingMachine vm) {

vm.increaseCoin(coin);

vm.changeState(new SelectableState());

}

@Override

public void select(int productIn, VendingMachine vm) {

SoundUtil.beep();

}

}


NoCoinState 클래스의 increaseCoin() 메서드는 VendingMachine의 동전 수를 증가시키고, 상태를 SelectableState로 변경한다.

즉, 동전없는 상태에서 동전을 넣으면 동전 수를 증가시키고 선택 가능 상태로 변경하는 기능을 제공하는 것이다.

NoCoinState 클래스의 select() 메서드는 에러 음을 발생시킨다.

이는 동전없는 상태에서 음료를 선택하면 에러 음을 발생시킨다는 것을 뜻한다.


SelectableState 클래스는 음료를 선택이 가능한 상태에서 동전을 넣을때와 음료를 선택할 때의 자판기 동작방식을 구현


public class SelectableState implements State {

public void increaseCoin(int coin, VendingMachine vm) {

vm.increaseCoin(coin);

}

public void select(int prdouctId, VendingMachine vm) {

vm.provideProduct(productId);

vm.decreaseCoin();

if(vm.hasNoCoin())

vm.changeState(new NoCoinState);

}


}


NoCoinState 클래스와 SelectableState 클래스를 보면, 상태 패턴을 적용함으로써 VendingMachine 클래스에 구현되어 있는 상태별 동작 구현코드가 각 상태의 구현 클래스로 이동함을 알 수 있다.

이 과정에서 VendingMachine클래스의 코드 구현은 상태 객체에 위임하는 방식으로 단순해진다.


상태별처리 코드를 상태로 분리함으로써 콘텍스트의 코드가 간결해지고 변경의 유연함을 얻게 된다.


상태 패턴의 장점은 새로운 상태가 추가되더라도 콘텍스트 코드가 받는 영향은 최소화된다는 점이다.

자판기 청소 상태 구현을 위해 CleaningState 클래스를 추가하더라도 insertCoin() 메서드와 select() 메서드의 코드는 그대로 유지된다.

상태가 많아질 경우 조건문을 이용하는 방식은 코드가 복잡해져서 유지보수를 어렵게 만들지만, 상태 패턴의 경우 상태가 많아지더라도 (클래스의 개수는 증가) 코드의 복잡도는 증가하지 않기 때문에 유지보수에 유리하다.


상태패턴의 다른 장점은 상태에 따른 동작을 구현한 코드가 각 상태별로 구분되기 때문에 상태별 동작을 수정하기기 쉽다는 점이다.

조건문을 이용한 방식을 사용할 경우 동전없음 상태의 동작을 수정하려면 각 메서드를 찾아다니면서 수정해 주어야 하는 반면에, 상태 패턴을 적용한 경우 동전없음 상태를 표현하는 NoCoinState 클래스를 수정해 주면된다.

관련된 코드가 한 곳에 모여 있기 때문에 안전하고 더 빠르게 구현을 변경할 수 있게 된다.


**상태패턴을 적용할때 고려할 문제는 콘텍스트의 상태 변경을 누가 하느냐에 대한것이다.

상태변경을 하는 주체는 콘텍스트나 상태객체 둘 중 하나가 된다.

자판기 예제에선 각 상태 객체에서 콘텍스트의 상태를 변경해줬다.


NoCoinState 클래스의 increaseCoin() 메서드는 VendingMachine의 changeState() 메서드를 호출해서 VendingMachine의 상태를 SelectableState로 변경했다.


// 상태 객체에서 콘텍스트의 상태 변경

vm.changeState(new SelectableState);


상태 객체에서 콘텍스트의 상태를 변경하려면 콘텍스트의 다른값에 접근해야 할 때도 있다.

SelectableState 클래스의 select() 메서드는 VendingMachine의 상태를 NoCoinState로 변경해야 하는지 여부를 확인하기 위해 VendingMachine의 hasNoCoin() 메서드를 사용하고 있다.

상태 객체에서 콘텍스트의 상태를 변경할 수 있는 조건을 확인할 수 있도록 콘텍스트 인터페이스에 메서드를 추가해야 한다는 것을 의미한다.


public class SelectableState implements State {

@Override

public void select(int productId, VendingMachine vm) {

vm.provideProduct(productId);

vm.decreaseCoin();

if(vm.hasNoCoin()) //상태변경을 위해, vm 객체가 동전이 없는지 확인

vm.changeState(new NoCoinState);

}

}


콘텍스트에서 상태를 변경할 경우, 콘텍스트의 코드가 다소 복잡해질 수 있다.

VendingMachine 클래스에서 콘텍스트가 직접 상태를 변경하도록 VendingMachine 클래스를 수정하면 


public class VendingMachine {

private State state;

public VendingMachine() {

state = new NoCoinState();

}

public void insertCoin(int coin) {

state.increaseCoin(coin, this);

if(hasCoin())

changeState(new SelectableState()); // 콘텍스트에서 상태변경

}

public void select(int productId) {

state.select(productId, this);

if(state.isSelectable() && hasNoCoin())

changeState(new NoCoinState()); // 콘텍스트에서 상태변경

}

private void changeState(State newState) {

state = newState;

}

private boolean hasCoin() {

}

private boolean hasNoCoin() {

return ! hasCoin();

// 기타 다른 기능

}


VendingMachine 클래스의 changeState() 메서드, hasCoin(), hasNoCoin() 메서드의 접근범위를 private으로 지정했는데, 이유는 상태 객체에서 콘텍스트의 상태를 변경하기 위한 목적으로 이들 메서드에 접근할 필요가 없어졌기 때문이다.

이제 상태 객체는 자신이 수행해야하는 작엄만 처리하도록 바뀐다.


public void select(int productId, VendingMachine vm) {

vm.provideProduct(prdouctId);

vm.decreaseCoin();

}

콘텍스트의 상태 변경을 누가할지는 주어진 상황에 알맞게 정해 주어야 한다.

먼저 콘텍스트에서 상태를 변경하는 방식은 비교적 상태 개수가 적고 상태 변경 규칙이 바뀌지 않는 경우에 유리하다.

상태 종류가 지속적으로 변경되거나 상태 변경 규칙이 자주 바뀔 경우 콘텍스트의 상태 변경처리코드가 복잡해질 가능성이 높기 때문이다.

상태변경처러리 코드가 복잡해질수록 상태 변경의 유연함이 떨어지게 된다.


반면에 상태 객체에서 콘텍스트의 상태를 변경할 경우, 콘텍스트에 영향을 주지 않으면서 상태를 추가하거나 상태 변경 규칙을 바꿀 수 있게 된다.

하지만, 상태 변경 규칙이 여러 클래스에 분산되어 있기 때문에, 상태 구현 클래스가 많아질수록 상태 변경 규칙을 파악하기가 어려워지는 단점이 있다.

또한 한 상태 클래스에서 다른 상태 클래스에 대한 의존도 발생한다.


두방식은 명확하게 서로 상반되는 장단점을 갖고 있기 때문에, 상태 패턴을 적용할 때에는 주어진 상황에 알맞는 방식을 선택해야한다.



반응형