행동패턴 중 하나인 상태패턴에 대해 알아봅시다.
정의
상태패턴은 객체의 내부 상태가 변경될 때 해당 객체가 자신의 행동을 변경할 수 있도록 하는 디자인 패턴입니다.
예시
1000원짜리 상품을 판매하는 자판기가 있습니다.
자판기의 상태는 코인이 부족할때, 상품을 구매할만큼 코인이 있을때로 나뉠 수 있습니다.
자판기의 상태에 대해 enum으로 상태를 만들었습니다.
public enum State {
NOCOIN,
ENOUGHCOIN,
}
다음은 자판기 클래스입니다.
코인입력했을때(insertCoin) 자판기 상태에 따라 행동이 달라집니다. switch문을 통해 상태에 따른 행동을 결정합니다.
마찬가지로 상품을 구매할때(buyProduct) 자판기 상태에 따라 행동이 달라집니다.
public class VendingMachine {
private State state;
private int coin;
private int productPrice = 1000;
public VendingMachine() {
this.state = State.NOCOIN;
this.coin = 0;
}
public void insertCoin(int coin) {
System.out.println(coin + "이 정상적으로 입력되었습니다.");
switch (state) {
case NOCOIN:
addCoin(coin);
if (validSelect()) {
state = State.ENOUGHCOIN;
}
break;
case ENOUGHCOIN:
addCoin(coin);
break;
}
}
public void buyProduct() {
switch (state) {
case NOCOIN:
System.out.println("코인이 부족합니다.");
break;
case ENOUGHCOIN:
giveProduct();
removeCoin();
if (!validSelect()) {
state = State.NOCOIN;
}
break;
}
}
private int bringBackCoin() {
int temp = this.coin;
this.coin = 0;
return temp;
}
private void giveProduct() {
System.out.println("[상품구매완료] 상품을 받아주세요!");
}
private void removeCoin() {
this.coin -= this.productPrice;
}
private void addCoin(int coin) {
this.coin += coin;
}
private boolean validSelect() {
return coin >= productPrice;
}
}
이렇게 요구사항을 구현한 자판기가 있습니다.
이때 상품이 품절됐을 경우를 추가하라는 요구사항이 생겼습니다.
어떻게 구현해야할까요?
State enum 클래스에 품절 상태를 추가하고 insertCoin과 buyProduct의 switch문에 품절 상태를 추가하면 될 것입니다.
public enum State {
NOCOIN,
ENOUGHCOIN,
SOLDOUT
}
public class VendingMachine {
private State state;
private int coin;
private int productPrice = 1000;
public VendingMachine() {
this.state = State.NOCOIN;
this.coin = 0;
}
public void insertCoin(int coin) {
System.out.println(coin + "이 정상적으로 입력되었습니다.");
switch (state) {
case NOCOIN:
addCoin(coin);
if (validSelect()) {
state = State.ENOUGHCOIN;
}
break;
case ENOUGHCOIN:
addCoin(coin);
break;
case SOLDOUT:
bringBackCoin();
break;
}
}
public void buyProduct() {
switch (state) {
case NOCOIN:
System.out.println("코인이 부족합니다.");
break;
case ENOUGHCOIN:
giveProduct();
removeCoin();
if (!validSelect()) {
state = State.NOCOIN;
}
break;
case SOLDOUT:
System.out.println("상품이 품절됐습니다.");
break;
}
}
private int bringBackCoin() {
int temp = this.coin;
this.coin = 0;
return temp;
}
private void giveProduct() {
System.out.println("[상품구매완료] 상품을 받아주세요!");
}
private void removeCoin() {
this.coin -= this.productPrice;
}
private void addCoin(int coin) {
this.coin += coin;
}
private boolean validSelect() {
return coin >= productPrice;
}
}
이렇게 상태가 추가될때마다 switch 조건이 추가되며 자판기의 내부 로직이 추가변경되는 셈입니다. OCP를 위반한 것입니다.
또한 늘어날때마다 코드량도 길어져 가독성도 떨어지게됩니다.
이렇게 기능(insertCoin, buyProduct)이 상태(State)에 따라 다르게 동작해야할때 상태패턴을 사용합니다.
상태패턴 적용
기존 State를 자판기 기능에 대한 인터페이스를 만듭니다.
public interface State {
void insertCoin(int coin, VendingMachine vendingMachine);
void buyProduct(VendingMachine vendingMachine);
}
State 상태 인터페이스를 각 상태가 구현합니다.
public class NoCoinState implements State{
@Override
public void insertCoin(int coin, VendingMachine vendingMachine) {
vendingMachine.addCoin(coin);
if (vendingMachine.validSelect()) {
vendingMachine.state = new EnoughCoinState();
}
}
@Override
public void buyProduct(VendingMachine vendingMachine) {
System.out.println("코인이 부족합니다.");
}
}
public class EnoughCoinState implements State{
@Override
public void insertCoin(int coin, VendingMachine vendingMachine) {
vendingMachine.addCoin(coin);
}
@Override
public void buyProduct(VendingMachine vendingMachine) {
vendingMachine.giveProduct();
vendingMachine.removeCoin();
if (!vendingMachine.validSelect()) {
vendingMachine.state = new NoCoinState();
}
}
}
public class SoldOutState implements State{
@Override
public void insertCoin(int coin, VendingMachine vendingMachine) {
vendingMachine.bringBackCoin();
}
@Override
public void buyProduct(VendingMachine vendingMachine) {
System.out.println("상품이 품절됐습니다.");
}
}
자판기 클래스의 최종 코드는 다음과 같습니다.
package Behavioral.state;
public class VendingMachine {
public State state;
public int coin;
public int productPrice = 1000;
public VendingMachine() {
this.state = new NoCoinState();
this.coin = 0;
}
public void insertCoin(int coin) {
System.out.println(coin + "이 정상적으로 입력되었습니다.");
state.insertCoin(coin, this);
}
public void buyProduct() {
state.buyProduct(this);
}
public int bringBackCoin() {
int originCoin = this.coin;
this.coin = 0;
return originCoin;
}
public void giveProduct() {
System.out.println("[상품구매완료] 상품을 받아주세요!");
}
public void removeCoin() {
this.coin -= this.productPrice;
}
public void addCoin(int coin) {
this.coin += coin;
}
public boolean validSelect() {
return coin >= productPrice;
}
}
코드가 기존 코드보다 훨씬 깔끔해졌습니다. 또한 새로운 상태가 추가되더라도 자판기 클래스에서 코드변경이 발생하지 않습니다.
상태변경의 주체
마지막으로 상태패턴에서 상태 변경의 주체에 따라 단점이 있습니다. 위 예에서는 자판기의 상태를 자판기 본인이 직접 변경하는지, 상태가 알아서 변경하는지 차이입니다.
1) 자판기가 상태를 변경할 경우
state에 기능이 추가될 수 있습니다. 기능이 추가되면 State 인터페이스에 기능이 추가됩니다. 그렇게되면 모든 State 구현체에 기능에 대한 메소드를 구현해줘야합니다. 이럴때는 State에 대한 추상 클래스로 공통된 기능을 구현해줘서 어느정도 문제를 해결할 수 있습니다.
2) 상태가 상태를 변경할 경우
이 경우에는 1)과 같은 문제는 없지만 어떻게 변경이 이루어졌는지 파악하기 어렵고 상태간 의존성이 발생할 수 있는 단점이 있습니다.
https://steady-coding.tistory.com/387
https://refactoring.guru/ko/design-patterns/state
https://tecoble.techcourse.co.kr/post/2021-04-26-state-pattern/
'리팩토링 > 디자인패턴' 카테고리의 다른 글
| [디자인패턴] 컴포지트 패턴 (1) | 2023.01.11 |
|---|---|
| [디자인 패턴] 데코레이터 패턴 (3) | 2022.12.26 |
| [디자인 패턴] 템플릿 메서드 패턴 (1) | 2022.12.10 |
| [디자인패턴] 퍼사드 패턴 (1) | 2022.11.26 |
| [디자인패턴] 어댑터 패턴 (1) | 2022.11.14 |