10.State Pattern

2025. 9. 20. 14:34
반응형
Generated Document

1. 일상에서 생각해 볼 수 있는 문제

image.png

우리 생활 속에는 상태에 따라 수행되는 일들이 달라지는 경우가 많다.
일상 생활에서 타는 대중교통을 예로 들어 보자

3,000 원이 충전되어 있고 버스비가 3,000원을 넘긴다면
카드 리더기에서는 잔액이 부족하다고 할 것이다.

3,000 원 이상인 경우는 정상 적으로 결제가 될 것이고
이미 결제가 완료된 경우에는 환승이 찍히고,

환승까지 찍힌상태의 카드는 이미 처리된 카드라고 말하는 것을 들은 적이 있을 것이다.

이를 참고하여 코드를 작성하면 다음과 같다.


BusCard.java

class BusCard {
    // 상태를 나타내는 상수 정의
    private static final int INSUFFICIENT_BALANCE = 0;
    private static final int PAYMENT_COMPLETED = 1;
    private static final int TRANSFER_COMPLETED = 2;
    private static final int ALREADY_PROCESSED = 3;

    private int state;
    private int balance;

    public BusCard(int initialBalance) {
        this.balance = initialBalance;
        // 초기 상태 설정
        if (this.balance < 1200) {
            this.state = INSUFFICIENT_BALANCE;
        } else {
            this.state = PAYMENT_COMPLETED;
        }
    }

    public void tapCard(int fare) {
        if (this.state == INSUFFICIENT_BALANCE) {
            System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
        } else if (this.state == PAYMENT_COMPLETED) {
            if (fare > this.balance) {
                System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
            } else {
                System.out.println("결제가 완료되었습니다. " + fare + "원이 결제되었습니다.");
                this.balance -= fare;
                this.state = TRANSFER_COMPLETED;
            }
        } else if (this.state == TRANSFER_COMPLETED) {
            System.out.println("환승이 완료되었습니다.");
            this.state = ALREADY_PROCESSED;
        } else if (this.state == ALREADY_PROCESSED) {
            System.out.println("이미 처리된 카드입니다.");
        }
    }

    public String getStateName() {
        if (this.state == INSUFFICIENT_BALANCE) {
            return "잔액부족";
        } else if (this.state == PAYMENT_COMPLETED) {
            return "결제완료";
        } else if (this.state == TRANSFER_COMPLETED) {
            return "환승완료";
        } else {
            return "이미처리됨";
        }
    }
}

Execute

public class BusCardSystem {

    public static void main(String[] args) {
        // 잔액이 3000원인 버스 카드를 생성합니다.
        BusCard myCard = new BusCard(3000);

        System.out.println("--- 버스 요금 1200원 결제 시도 ---");
        myCard.tapCard(1200); // 정상 결제
        System.out.println("현재 상태: " + myCard.getStateName());

        System.out.println("--- 30분 이내에 환승 시도 ---");
        myCard.tapCard(0); // 환승
        System.out.println("현재 상태: " + myCard.getStateName());

        System.out.println("--- 30분 이내에 다시 태그 시도 ---");
        myCard.tapCard(0); // 이미 처리된 카드
        System.out.println("현재 상태: " + myCard.getStateName());

        System.out.println("\n--- 잔액 0원으로 새로운 카드 생성 ---");
        BusCard emptyCard = new BusCard(0);
        System.out.println("--- 버스 요금 1200원 결제 시도 ---");
        emptyCard.tapCard(1200); // 잔액 부족
        System.out.println("현재 상태: " + emptyCard.getStateName());
    }
}

이렇게 코드로 나타내면 충분히 나타낼 수 있지만
조건문 ( if - else ) 로 도배가 된 로직을 볼 수가 있다.

여기서 만약 state 가 추가가 된다면? 여기서 만약 tapCard() 기능 뿐만 아니라
balanceCharege() 기능이 추가가 된다면 코드 수정하는 것이 어려워 질 것으로 생각이된다.

2. 논리적으로 풀어보기

이 문제를 해결하기 위해서 추상화 개념을 마음껏 써보자 각
상태마다 특정 메서드에 대한 결과 값이 달라진다는 것을 생각했을 때,

반복되는 상태 라는 개념이 추상화 되면 좋을 것 같다는 생각이 든다 .
이를 추상적으로 하기 위해서 상태를 interface 로 바꿔 보았다.


CardState.java

interface CardState {
    void handleTap(BusCard card, int fare);
    String getStateName();
}

위의 코드에서 공통적으로 사용하는 메서드는 카드를 tap 하는 것이 전부이기 때문에
위와 같이 정의했다.

이후 각각의 상태를 아래 처럼 구현 클래스로 정의를 해보았다.


InsufficentBalanceState.java

// 구체적인 상태 클래스들
class InsufficientBalanceState implements CardState {
    @Override
    public void handleTap(BusCard card, int fare) {
        System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
    }

    @Override
    public String getStateName() {
        return "잔액부족";
    }
}

PaymentCompletedState.java

class PaymentCompletedState implements CardState {
    @Override
    public void handleTap(BusCard card, int fare) {
        System.out.println("결제가 완료되었습니다. " + fare + "원이 결제되었습니다.");
        card.setBalance(card.getBalance() - fare);
        card.setState(new TransferState());
    }

    @Override
    public String getStateName() {
        return "결제완료";
    }
}

TransferState.java

class TransferState implements CardState {
    @Override
    public void handleTap(BusCard card, int fare) {
        System.out.println("환승이 완료되었습니다.");
        card.setState(new AlreadyProcessedState());
    }

    @Override
    public String getStateName() {
        return "환승완료";
    }
}

AlreadyProcessedState.java

class AlreadyProcessedState implements CardState {
    @Override
    public void handleTap(BusCard card, int fare) {
        System.out.println("이미 처리된 카드입니다.");
    }

    @Override
    public String getStateName() {
        return "이미처리됨";
    }
}

그 다음 위의 상태 구현체들을 사용하는 클래스를 정의해보자


BusCard.java

class BusCard {
    private CardState state;
    private int balance;
    private static final int BASE_FARE = 1200;

    public BusCard(int initialBalance) {
        this.balance = initialBalance;
        // 초기 상태 설정
        if (balance < BASE_FARE) {
            this.state = new InsufficientBalanceState();
        } else {
            this.state = new PaymentCompletedState();
        }
    }

    public void setState(CardState state) {
        this.state = state;
    }

    public CardState getState() {
        return state;
    }

    public void tapCard(int fare) {
        if (fare > 0 && balance < fare && this.getState().getStateName().equals("결제완료")) {
            // 결제 완료 상태에서만 잔액 부족을 체크
            this.setState(new InsufficientBalanceState());
            this.state.handleTap(this, 0);
        } else {
            this.state.handleTap(this, fare);
        }
    }
    
    // 이외의 메서드는 편의를 위해 추가
    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    public String getStateName() {
        return state.getStateName();
    }
}

이렇게 정의하면 if-else 로 도배되지 않고 새로운 method 가 추가 되면
각 구현체에 로직을 새롭게 정의하면 된다. 또한 각각의 상태에서만 필요한 메서드들을 따로 정의할 수 있어
역할 분담을 명확하게 할 수 있게 된다.

3. State Pattern

위에서 리팩토링 한 결과가 State Pattern 이다.

상태 패턴
객체의 내부 상태가 바뀜에 따라 객체의 행위(behavior)를 변경할 수 있도록 하는 디자인 패턴이다.

주요한 특징으로

  1. 상태 캡슐화

    각 상태를 별도의 클래스로 캡슐화한다. 관련된 로직만 존재 가능

  2. 행위의 다형성

    동일한 인터페이스를 구현하며, 상태에 따라 다양한 결과를 받을 수 있다.

  3. 동적 상태 변경

    런타임에 동적으로 상태가 변경될 수 있다.

  4. 개방-폐쇄 원칙(Open-Closed Principle) 준수

    새로운 상태가 추가되어도 새로운 상태 클래스만 추가하면 된다.


장점

코드 유지보수성 향상

`if-else`나 `switch-case`문이 제거되어 코드가 훨씬 간결해지고 이해하기 쉬워 진다.

확장성

새로운 상태를 추가하기가 매우 쉽다.

상태 전이 로직 분리

상태 간의 전환 로직이 각 상태 클래스 내에 존재하게 되어,전이가 필요한 곳에 로직을 분산시킬 수 있다.


단점

클래스 증가

상태가 많아질수록 클래스의 수가 증가

초기 설계 복잡성 증가

간단한 로직에는 과도한 패턴일 수 있다. 오히려 if-else 가 간결 할 수 있다.

반응형

BELATED ARTICLES

more