1. 개요

이 사용방법(예제)에서는 동작 GoF 디자인 패턴 중 하나인 상태 패턴을 소개합니다.

처음에는 목적에 대한 개요를 제공하고 해결하려는 문제를 설명합니다. 그런 다음 State의 UML 다이어그램과 실제 예제 구현을 살펴보겠습니다.

2. 상태 디자인 패턴

상태 패턴의 주요 아이디어는  객체가 클래스를 변경하지 않고 동작을 변경할 수 있도록 허용하는 것입니다. 또한 이를 구현하면 if/else 문이 많이 없어도 코드가 더 깔끔해집니다.

우체국으로 보내지는 소포가 있다고 상상해보십시오. 소포 자체를 주문한 다음 우체국으로 배달하고 최종적으로 고객에게 받을 수 있습니다. 이제 실제 상태에 따라 배송 상태를 인쇄하려고 합니다.

가장 간단한 접근 방식은 일부 부울 플래그를 추가하고 클래스의 각 메서드 내에 간단한 if/else 문을 적용하는 것입니다. 간단한 시나리오에서는 그다지 복잡하지 않습니다. 그러나 처리할 상태가 더 많아지면 코드가 복잡해지고 오염되어 더 많은 if/else 문이 생성될 수 있습니다.

게다가 각 상태에 대한 모든 논리는 모든 메서드에 분산됩니다. 이제 여기에서 State 패턴을 사용하는 것으로 간주할 수 있습니다. 상태 디자인 패턴 덕분에 로직을 전용 클래스에 캡슐화하고, 단일 책임 원칙  및  개방/폐쇄 원칙  을 적용하고 , 더 깨끗하고 유지 관리하기 쉬운 코드를 가질 수 있습니다.

3. UML 다이어그램

상태 디자인 패턴의 UML 다이어그램

UML 다이어그램에서 우리는  Context  클래스 에 프로그램 실행 중에 변경될 관련 State 가 있음을 알 수 있습니다.

우리의 컨텍스트는 동작을 상태 구현에 Delegation할 것입니다. 즉, 들어오는 모든 요청은 상태의 구체적인 구현에 의해 처리됩니다.

로직이 분리되어 있고 새 상태를 추가하는 것이 간단하다는 것을 알 수 있습니다. 필요한 경우 다른 상태  구현을 추가하는 것으로 귀결됩니다.

4. 시행

애플리케이션을 설계해 봅시다. 이미 언급했듯이 패키지는 주문, 배달 및 수령할 수 있으므로 세 가지 상태와 컨텍스트 클래스를 갖게 됩니다.

먼저 패키지 클래스가 될 컨텍스트를 정의해 보겠습니다.

public class Package {

    private PackageState state = new OrderedState();

    // getter, setter

    public void previousState() {
        state.prev(this);
    }

    public void nextState() {
        state.next(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

우리가 볼 수 있듯이 상태 관리를 위한 참조가 포함되어 있습니다  . 작업을 상태 객체에 Delegation하는 previousState(), nextState() 및  printStatus() 메서드에 주목하십시오. 상태는 서로 연결되며 모든 상태는 두 메서드에 전달 된 참조 를 기반으로 다른 상태를 설정 합니다.

클라이언트는  Package 클래스와 상호 작용하지만 상태 설정을 처리할 필요가 없으며 클라이언트가 해야 할 일은 다음 또는 이전 상태로 이동하는 것뿐입니다.

다음으로   다음 서명이 있는 세 가지 메서드가 있는 PackageState 가 있습니다.

public interface PackageState {

    void next(Package pkg);
    void prev(Package pkg);
    void printStatus();
}

이 인터페이스는 각 구체적인 상태 클래스에 의해 구현됩니다.

첫 번째 구체적인 상태는  OrderedState 입니다 .

public class OrderedState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new DeliveredState());
    }

    @Override
    public void prev(Package pkg) {
        System.out.println("The package is in its root state.");
    }

    @Override
    public void printStatus() {
        System.out.println("Package ordered, not delivered to the office yet.");
    }
}

여기서는 패키지 주문 후 발생할 다음 상태를 가리킵니다. 주문한 상태는 루트 상태이며 명시적으로 표시합니다. 두 가지 방법 모두에서 상태 간 전환이 처리되는 방식을 볼 수 있습니다.

DeliveredState 클래스 를 살펴보겠습니다  .

public class DeliveredState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new ReceivedState());
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new OrderedState());
    }

    @Override
    public void printStatus() {
        System.out.println("Package delivered to post office, not received yet.");
    }
}

다시, 우리는 주 사이의 연결을 봅니다. 패키지의 상태가 주문됨에서 배송됨으로 변경되고 있으며  printStatus() 의 메시지 도 변경됩니다.

마지막 상태는  ReceivedState 입니다 .

public class ReceivedState implements PackageState {

    @Override
    public void next(Package pkg) {
        System.out.println("This package is already received by a client.");
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new DeliveredState());
    }
}

이것은 우리가 마지막 상태에 도달하는 곳이며 이전 상태로만 롤백할 수 있습니다.

우리는 이미 한 상태가 다른 상태에 대해 알고 있기 때문에 약간의 보상이 있음을 알고 있습니다. 우리는 그것들을 단단히 결합시키고 있습니다.

5. 테스트

구현이 어떻게 작동하는지 살펴보겠습니다. 먼저 설정 전환이 예상대로 작동하는지 확인하겠습니다.

@Test
public void givenNewPackage_whenPackageReceived_thenStateReceived() {
    Package pkg = new Package();

    assertTrue(pkg.getState() instanceOf OrderedState);
    pkg.nextState();

    assertTrue(pkg.getState() instanceOf DeliveredState);
    pkg.nextState();

    assertTrue(pkg.getState() instanceOf ReceivedState);
}

그런 다음 패키지가 해당 상태로 되돌아갈 수 있는지 빠르게 확인합니다.

@Test
public void givenDeliveredPackage_whenPrevState_thenStateOrdered() {
    Package pkg = new Package();
    pkg.setState(new DeliveredState());
    pkg.previousState();

    assertTrue(pkg.getState() instanceOf OrderedState);
}

그런 다음 상태 변경을 확인하고 printStatus() 메서드의 구현이 런타임 시 구현을 변경하는 방법 을 살펴보겠습니다.

public class StateDemo {

    public static void main(String[] args) {

        Package pkg = new Package();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();
    }
}

그러면 다음과 같은 결과가 표시됩니다.

Package ordered, not delivered to the office yet.
Package delivered to post office, not received yet.
Package was received by client.
This package is already received by a client.
Package was received by client.

컨텍스트의 상태를 변경하면서 동작이 변경되었지만 클래스는 동일하게 유지됩니다. 우리가 사용하는 API뿐만 아니라.

또한 상태 간 전환이 발생하여 클래스가 상태와 결과적으로 동작을 변경했습니다.

6. 단점

상태 패턴 결점은 상태 간 전환을 구현할 때 보상입니다. 그러면 상태가 하드코딩되며 이는 일반적으로 나쁜 습관입니다.

그러나 우리의 필요와 요구 사항에 따라 문제가 될 수도 있고 그렇지 않을 수도 있습니다.

7. 상태 VS 전략 패턴

두 가지 디자인 패턴은 매우 유사하지만 UML 다이어그램은 동일하며 그 뒤에 있는 아이디어는 약간 다릅니다.

첫째, 전략 패턴 은 상호 교환 가능한 알고리즘 계열을 정의합니다 . 일반적으로 이들은 동일한 목표를 달성하지만 예를 들어 정렬 또는 렌더링 알고리즘과 같이 다른 구현을 사용합니다.

상태 패턴에서 동작은 실제 상태에 따라 완전히 변경될 수 있습니다.

다음으로, 전략에서 클라이언트는 가능한 전략을 사용하고 명시적으로 변경할 수 있음을 알고 있어야 합니다. 반면 상태 패턴에서는 각 상태가 다른 상태와 연결되어 유한 상태 머신과 같은 흐름을 생성합니다.

8. 결론

상태 디자인 패턴은 기본 if/else 문을 피하고 싶을 때 유용합니다 . 대신, 우리 는 로직을 추출하여 클래스를 분리 하고 컨텍스트 객체가 동작 을 상태 클래스에 구현된 메서드에 Delegation하도록 합니다. 게다가 하나의 상태가 컨텍스트의 상태를 변경할 수 있는 상태 간의 전환을 활용할 수 있습니다.

일반적으로 이 디자인 패턴은 상대적으로 단순한 애플리케이션에 적합하지만 보다 고급 접근 방식을 보려면 Spring의 State Machine 예제 을 살펴볼 수 있습니다 .

평소와 같이 전체 코드는  GitHub 프로젝트 에서 사용할 수 있습니다 .

Generic footer banner