본문 바로가기
IT/Java

[Java] Java State Pattern

by kyu-nahc 2024. 9. 16.
반응형

 

State Pattern

상태 패턴(State Pattern)은 객체가 특정 상태에 따라 행위를 달리하는 상황에서,
상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌,
상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말한다.

객체 지향 프로그래밍에서의 클래스는 꼭 사물 / 생물 형태의 데이터만 표현할 수 있는 게 아니다.

경우에 따라서 무형태의 행위 / 동작도 클래스로 묶어 표현할 수 있다.

그래서 상태를 클래스로 표현하면 클래스를 교체해서 ‘상태의 변화’를 표현할 수 있고,

객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해 낼 수 있으며,

새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.

 

전략 패턴(Strategy Pattern)이 '전략 알고리즘'을 클래스로 표현한 패턴이라면,

상태 패턴(State Pattern)은 '객체 상태'를 클래스로 표현한 패턴이라고 보면 된다.

그래서 상태 패턴의 클래스 다이어그램을 보면 전략 패턴과 매우 유사하다는 점을 볼 수 있다.

왜냐하면 전략 패턴은 전략을 객체화 한거고, 상태 패턴은 상태를 객체화한 것이기 때문이다.

 

State Pattern 구조

 

  • State (Interface)
    상태를 추상화한 고수준 모듈
  • ConcreteState
    구체적인 각각의 상태를 클래스로 표현.
    State 역할로 결정되는 인터페이스(API)를 구체적으로 구현한다.

    다음 상태가 결정되면 Context에 상태 변경을 요청하는 역할도 한다.
  • Context
    State를 이용하는 시스템.
    시스템 상태를 나타내는 State 객체를 합성(composition)하여 가지고 있다.

    클라이언트로부터 요청받으면 State 객체에 행위 실행을 위임한다.

상태 클래스는 싱글톤 클래스로 구성한다.

전략 패턴의 전략 객체 같은 경우 매개 값에 따라 알고리즘 수행 형태가 달라질 수 있지만, 

상태는 그 객체의 현 폼을 나타내는 것이기 때문에 대부분의 상황에서 유일하게 있어야 한다.

이에 대한 예시 코드를 살펴보면 다음과 같다.

// 상태를 추상화한 객체
public interface State {
    void doAction(Context context);
}
public class ConcreteState1 implements State{

    @Override
    public void doAction(Context context) {
        System.out.println("ConcreteState1 doAction");
        // 상태에서 동작을 실행한 후 바로 다른 상태로 변경 가능
        context.setState(new ConcreteState2());
    }
}
public class ConcreteState2 implements State{

    @Override
    public void doAction(Context context) {
        System.out.println("ConcreteState2 doAction");
    }
}
public class Context {

    private State state;

    // State를 구현한 구현체에서 상태를 변경하기 위한 setter 메서드
    public void setState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }
    // 상태에 의존한 처리 메소드로서 State 객체에 처리를 위임함
    public void doRequest(){
        state.doAction(this);
    }
}
public class Client {

    public static void main(String[] args) {

        // Context 생성
        Context context = new Context();
        // 초기 상태를 ConcreteState1으로 설정
        context.setState(new ConcreteState1());
        // ConcreteState1 doAction() 메소드에서
        // 상태가 ConcreteState2로 변경된다.
        context.doRequest();
        context.doRequest();
    }
}

 

하지만 해당 구조 예시 코드에는 문제점이 존재한다.

ConcreteState1 클래스에서 Context의 setState()를 이용하여 상태 객체를 변경할 때,

새로운 객체를 생성하여 할당하고 있다. 만약 상태 변경이 많이 일어난다면 너무나 많은

필요 없는 객체들이 생성되게 된다. 상태는 새로운 인스턴스를 생성할 필요 없기 때문에,

쓸데없는 메모리를 차지하게 된다. 이를 방지하기 위해 상태 객체들을 싱글톤화 할 필요가 있다.

싱글톤에 대한 자세한 내용은 아래 포스팅을 참고하면 된다.

 

[Java] Java Singleton Pattern

Singleton Pattern싱글톤 패턴(Singleton Pattern)은 객체 지향 프로그래밍에서,특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다. 생성자를 여러 번 호출하더라도 인스턴스가 하

kyu-nahc.tistory.com

public class ConcreteState1 implements State{

    // 싱글톤 클래스로 변경하여
    // 하나의 Instance만 반환하도록 한다.
    private static class ConcreteState1Holder{
        private static final ConcreteState1 INSTANCE = new ConcreteState1();
    }

    private ConcreteState1(){}

    public static ConcreteState1 getInstance(){
        return ConcreteState1Holder.INSTANCE;
    }

    @Override
    public void doAction(Context context) {
        System.out.println("ConcreteState1 doAction");
        context.setState(new ConcreteState2());
    }
}

public class Client {

    public static void main(String[] args) {

        Context context = new Context();
        // 새로운 객체를 생성하는 것이 아닌
        // getInstace()를 통해 객체를 받아온다.
        context.setState(ConcreteState1.getInstance());
        context.doRequest();
        context.doRequest();
    }
}

 

싱글톤 패턴에 맞추어 ConcreteClass1 클래스를 변형하면 다음과 같다.

State를 구현하는 구현체 객체는 모두 싱글톤 클래스로 구성하는 것이 효율적이다.

위와 같이 싱글톤 객체로 생성해야 필요 없는 객체 생성을 방지하고, 메모리를 낭비하지 않게 된다.

State Pattern을 통해 if elseif 분기문을 통해 상태에 따라 로직을 작성하지 않으므로

새로운 상태를 추가하기에 용이하고 OCP 원리에 부합한다.

 

 

State Pattern 특징

패턴 적용 시기

  • 객체의 행동(메서드)이 상태(state)에 따라 각기 다른 동작을 할 때. 
  • 상태 및 전환에 걸쳐 대규모 조건 분기 코드와 중복 코드가 많을 경우
  • 조건문의 각 분기를 별도의 클래스에 넣는 것이 상태 패턴의 핵심
  • 런타임단에서 객체의 상태를 유동적으로 변경해야 할 때

패턴 장점

  • 상태(State)에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있다.
  • 상태(State)와 관련된 모든 동작을 각각의 상태 클래스에 분산시켜, 코드 복잡도를 줄일 수 있다.
  • 단일 책임 원칙(SRP)을 준수할 수 있다. (특정 상태와 관련된 코드를 별도의 클래스로 구성)
  • 개방 폐쇄 원칙(OCP)을 준수할 수 있다.
    기존 State 클래스나 컨텍스트를 변경하지 않고 새 State를 도입할 수 있다.
  • 하나의 상태 객체만 사용하여 상태 변경을 하므로 일관성 없는 상태 주입을 방지하는데 도움이 된다.

패턴 단점

  • 상태 별로 클래스를 생성하므로, 관리해야 할 클래스 수 증가
  • 상태 클래스 개수가 많고 상태 규칙이 자주 변경된다면,
    Context의 상태 변경 코드가 복잡해지게 될 수 있다.
  • 객체에 적용할 상태가 몇 가지밖에 없거나,
    거의 상태 변경이 이루어지지 않는 경우 패턴을 적용하는 것이 과도할 수 있다.

 

State Pattern 적용 예시

트랜잭션 관리에서 State 패턴을 사용하는 것은 매우 유용하다.

트랜잭션 시스템은 다양한 상태를 가질 수 있으며, 각 상태에 따라 수행되는 행동이 다르다.

State 패턴을 적용하면 이러한 상태 전이에 따른 복잡한 조건문을 피할 수 있으며,

각 상태별로 로직을 분리하여 코드의 가독성과 유지보수성을 높일 수 있다.

다음과 같이 3개의 상태를 가지는 트랜잭션 시스템이 있고, 각 상태마다 다른 행동을 수행한다.

  • 진행 중 (InProgress) 상태
  • 완료 (Completed) 상태
  • 실패 (Failed) 상태

각 상태에 따라 트랜잭션을 취소하거나 완료할 수 있는 행동이 달라진다.

public interface TransactionState {
    // 트랜잭션 처리
    void process(Transaction transaction);
    // 트랜잭션 취소
    void cancel(Transaction transaction);
}
public class InProgressState implements TransactionState {

    private static class InProgressStateHolder{
        static final InProgressState instance = new InProgressState();
    }
    private InProgressState(){}
    public static InProgressState getInstance(){
        return InProgressStateHolder.instance;
    }

    @Override
    public void process(Transaction transaction) {
        System.out.println("Transaction is in progress. Completing the transaction.");
        // 상태 전환: 완료 상태로 변경
        transaction.setState(CompletedState.getInstance());
    }

    @Override
    public void cancel(Transaction transaction) {
        System.out.println("Transaction is cancelled.");
        // 상태 전환: 실패 상태로 변경
        transaction.setState(FailedState.getInstance());
    }
}
public class CompletedState implements TransactionState {

    private static class CompletedStateHolder{
        static final CompletedState instance = new CompletedState();
    }

    private CompletedState(){}
    public static CompletedState getInstance(){
        return CompletedStateHolder.instance;
    }

    @Override
    public void process(Transaction transaction) {
        System.out.println("Transaction is already completed.");
    }

    @Override
    public void cancel(Transaction transaction) {
        System.out.println("Cannot cancel a completed transaction.");
    }
}

public class FailedState implements TransactionState {

    private static class FailedStateHolder{
        static final FailedState instance = new FailedState();
    }

    private FailedState(){}
    public static FailedState getInstance(){
        return FailedStateHolder.instance;
    }

    @Override
    public void process(Transaction transaction) {
        System.out.println("Transaction has failed. Cannot process further.");
    }

    @Override
    public void cancel(Transaction transaction) {
        System.out.println("Transaction has already failed. No need to cancel.");
    }
}

 

상태 전이에 따른 행동 절차를 살펴보면 다음과 같다.

초기 상태에서는 트랜잭션이 진행되고 있는 상태이다.

트랜잭션이 process() 호출을 통해 완료될 수 있으며, 이때 상태는 CompletedState로 변경된다.

완료 상태에서는 더 이상 트랜잭션을 취소하거나 다시 처리할 수 없다.

process()를 호출하면 "이미 완료된 트랜잭션"이라는 메시지가 출력되며,

cancel()을 호출하면 "완료된 트랜잭션을 취소할 수 없음" 메시지가 출력된다.

트랜잭션이 취소되면 상태가 FailedState로 전환되며,

이 상태에서는 더 이상 트랜잭션을 처리할 수 없다.

public class Transaction {
    private TransactionState state;

    // 초기 상태는 진행상태로 설정
    public Transaction() {
        this.state = InProgressState.getInstance();
    }

    public void setState(TransactionState state) {
        this.state = state;
    }
    public TransactionState getState(){
        return this.state;
    }
    // 현재 상태에 따른 처리 로직 수행
    public void process() {
        state.process(this);
    }
    // 현재 상태에 따른 취소 로직 수행
    public void cancel() {
        state.cancel(this);
    }
}

public class Client {

    public static void main(String[] args) {

        Transaction transaction = new Transaction();
        // 트랜잭션을 처리
        transaction.process();
        // 완료된 트랜잭션을 다시 처리 시도
        transaction.process();
        // 완료된 트랜잭션을 취소 시도
        transaction.cancel();
    }
}

 

트랜잭션 관리 시스템에서 상태 패턴을 사용하면 여러 상황에서 매우 유용하며,

아래와 같이 실무에서 사용하는 예시가 존재한다.

  • 온라인 결제 시스템
    결제가 진행 중, 성공, 실패 상태를 가지고 있을 때 각각의 상태에 맞는 로직을 분리
  • 배송 관리 시스템
    주문이 접수, 배송 준비 중, 배송 완료, 취소된 상태에 따라 다른 작업을 수행
  • 예약 시스템
    예약이 진행 중, 완료됨, 취소됨 상태에 따라 각각의 행동을 정의

State 패턴은 상태에 따라 복잡한 조건문을 작성하는 것을 피하고,

상태별로 행동을 분리하여 시스템의 유지보수성을 높일 수 있다는 장점이 존재한다.

단 해당 패턴을 사용 시 관리해야 할 클래스의 수가 늘어나므로, 상태의 수가 적고

상태가 거의 변하지 않는다면, 해당 패턴을 사용 시 구조적인 복잡성만 늘어날 수 있다.

따라서 상태의 조건과 수를 고려하여 State Pattern을 적용할 필요가 있다.

 

 

참고자료

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%83%81%ED%83%9CState-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Composite Pattern  (11) 2024.09.16
[Java] Java Template Method Pattern  (1) 2024.09.16
[Java] Java Iterator Pattern  (0) 2024.09.15
[Java] Java Adapter Pattern  (1) 2024.09.15
[Java] Java Singleton Pattern  (2) 2024.09.14

loading