본문 바로가기
개인 공부/객체지향 프로그래밍 (OOP)

[Design Patterns] 상태 패턴

by 희조당 2023. 6. 13.
728x90

🙋 들어가기 앞서

디자인 패턴 중에서 행동 패턴에 속해있는 "상태 패턴"에 대해서 알아보는 시간을 가져보겠습니다.

이 글에서는 상태 패턴의 구조를 예시로 알아보고 장단점과 비슷한 다른 패턴을 비교해 보겠습니다.


🫨 상태 패턴

상태 패턴은 객체 내부의 상태가 변경될 때마다 행동이 달라지는 디자인 패턴입니다.

상태에 맞는 행동을 분리해 코드의 응집도를 높이고 새로운 행동이 추가되어도 다른 행동에 영향을 주지 않는 것이 목적입니다.

🚑 구조

다음 그림과 같은 구조를 가지고 있습니다.

객체(Context)의 상태를 분리하고 상태 객체의 참조를 가지게 합니다.

이후 상태에 맞는 행위를 구현한 구현체를 만들어 줍니다.


🧙‍♂️ 예시 (feat. 주문)

배달 주문을 하는 것으로 예시를 들어보겠습니다!

간단하게 어떤 메뉴를 주문했는지와 주문 상태만을 가지도록 구현했습니다.

public class Order {
    private String menu;
    private Status status;

    public Order(String menu) {
        this.menu = menu;
        this.status = Status.ORDERED;
    }

    public Order(String menu, Status status) {
        this.menu = menu;
        this.status = status;
    }
}

 

상태는 다음 세 가지만 가지도록 했습니다. 주문 완료, 결제 완료, 배달 완료

사실 결제 완료랑 주문 완료가 같은데 실수했으니 감안하고 보시기 바랍니다 🤪🤪🤪 

public enum Status {
    ORDERED, PAYED, DELIVERED
}

🙅‍♂️ 패턴 적용 전

if-else 문으로 주어진 상태에 맞춰서 적절하게 상태도 바꾸고 문제도 잘 알려주고 있습니다.

크게 문제가 없어보이지만 상태가 추가될 때 문제가 발생합니다.

 

상태가 추가됨에 따라 로직이 점점 복잡해질 것이고 많은 코드를 수정해야할 것 같습니다.

무엇보다 코드가 응집되지 않아서 보기 힘들 것 같습니다 🥹🥹

public void payed() {
    if (this.status == Status.ORDERED){
        this.status = Status.PAYED;
        System.out.println("결제가 완료되었습니다.");
    } else if (this.status == Status.DELIVERED) {
        System.out.println("[결제 오류] 이미 배달 완료된 건입니다.");
    } else {
        System.out.println("[결제 오류] 이미 결제한 건입니다.");
    }
}

public void delivered() {
    if (this.status == Status.PAYED){
        this.status = Status.DELIVERED;
        System.out.println("배달이 완료되었습니다.");
    } else if (this.status == Status.DELIVERED) {
        System.out.println("[배달 오류] 이미 배달 완료된 건입니다.");
    } else {
        System.out.println("[배달 오류] 결제되지 않은 건입니다.");
    }
}

🙆‍♂️ 패턴 적용 후

상태 패턴을 적용하면 상태에 따른 행동을 다음과 같이 분리할 수 있습니다.

// 상태
public interface Status {
    Status payed(Status status);
    Status delivered(Status status);
}

// 주문 완료
public class Ordered implements Status {
    private static final String ERROR_MESSAGE = "[결제 오류] 결제되지 않은 건입니다.";
    private static final String PAYED = "결제가 완료되었습니다.";

    @Override
    public Status payed(Status status) {
        System.out.println(PAYED);
        return new Payed();
    }

    @Override
    public Status delivered(Status status) {
        System.out.println(ERROR_MESSAGE);
        return this;
    }
}

// 결제 완료
public class Payed implements Status {
    private static final String ERROR_MESSAGE = "[결제 오류] 이미 결제한 건입니다.";
    private static final String DELIVERED = "배달이 완료되었습니다.";

    @Override
    public Status payed(Status status) {
        System.out.println(ERROR_MESSAGE);
        return this;
    }

    @Override
    public Status delivered(Status status) {
        System.out.println(DELIVERED);
        return new Delivered();
    }
}

// 배달 완료
public class Delivered implements Status {
    private static final String ERROR_MESSAGE = "[배달 오류] 이미 배달 완료된 건입니다.";

    @Override
    public Status payed(Status status) {
        System.out.println(ERROR_MESSAGE);
        return this;
    }

    @Override
    public Status delivered(Status status) {
        System.out.println(ERROR_MESSAGE);
        return this;
    }
}

 

그럼 이전에 사용하던 payed()와 delivered()는 어떻게 변화했을까요?

다음과 같이 상태가 추가되어도 동일하게 동작하고 코드를 수정할 필요가 없어졌습니다.

나아가 각각 행동에 대한 로직이 집중되어 더욱 보기 쉬워졌고 원래 객체(Content)의 응집도 또한 높아졌습니다.

public void payed() {
    this.status = status.payed(this.status);
}

public void delivered() {
    this.status = status.delivered(this.status);
}

😎 장단점과 주의점

무조건 패턴을 사용한다고 좋을까요? 분명히 Trade-Off가 존재할겁니다 😋😋

이번에는 상태 패턴의 장단점을 비교해보겠습니다.

👍 장점

  • 쉬운 유지보수 : 상태에 따른 행동을 각 상태를 별도의 클래스로 관리합니다. 기능에 맞춰 코드가 집중되있어 더 깔끔해지고 유지보수가 더 쉬워집니다. 
  • OCP 준수 : 새로운 상태가 추가되더라도 기존 코드 변경없이 새로운 상태 클래스를 추가하기만 하면 쉽게 확장이 가능합니다. 이는 Open Closed Principle(OCP) 지키는 것을 말합니다.
  • 로직의 복잡도 감소 : 행동이 새로운 상태를 반환합니다. 상태를 변화시키는 로직이 한 곳에 몰려있지 않고, 자체적으로 상태를 변화시켜 로직을 이해하기 더 쉬워집니다.

👎 단점

  • 구조의 복잡도 증가 : 상태 별로 클래스를 가집니다. 따라서 수가 많아질 수록 관리하기 어려워지고 어떤 상태로 변화하는지 쉽게 이해하기 어려워집니다.

⛔️ 주의

상태가 많지 않거나 자주 변화하지 않는다면 상태 패턴을 사용하는 것은 오히려 복잡도를 증가시키는 행위일 수 있습니다.

따라서, 이 패턴을 도입할 때는 기준이 필요하다고 생각합니다.

저는 상태 패턴을 도입할 때 다음과 같은 저만의 기준을 만들었습니다.

  • 상태가 3개 이상인가?
  • 상태가 추가될 가능성이 있는가?

저만의 기준이기 때문에 참고만 하시기 바랍니다 😜😜


👀 비슷한 디자인 패턴

상태 패턴과 가장 비슷하다고 볼 수 있는 패턴은 전략 패턴입니다. 어떤 점에서 차이가 날까요?

♟️ 전략 패턴은?

전략 패턴은 객체마다 사용하는 전략을 추상화해서 전략에 맞게 클래스 단위로 구현합니다.

이후 인스턴스를 생성할 때 맞는 전략을 주입해주는 형태로 사용합니다. 

전략을 추상화해서 클래스 단위로 구현하고 사용하는 점이 상태 패턴과 매우 유사해보입니다.

🧐 그래서 차이점은?

인스턴스를 생성할 때 전략을 주입한다는 뜻은 인스턴스가 생성된 뒤에도 계속 동일한 행동을 한다는 말입니다.

나아가 동일한 행동을 한다는 뜻은 행동의 결과물 즉, 상태가 변화하지 않는다는 말과 동일합니다.

그렇기 때문에 상태의 유무에서 첫번째 차이가 발생합니다.

 

추가로, 전략 패턴은 로직을 캡슐화를 하고 이를 쉽게 교체하는 것이 초점인 것에 비해서

상태 패턴은 상태에 따라 어떻게 행동할지에 초점을 두고 있습니다.

또한, 전략 패턴은 클라이언트가 전략을 선택하는 반면 상태 패턴은 상태가 변화하는 로직이 객체 내부에 존재합니다. 


-reference:

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

 

😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋

 

댓글