본문 바로가기
Design Pattern/행동 패턴(Behavioral patterns)

State Pattern, 상태 패턴

by codeyaki 2024. 9. 11.
반응형

상태 패턴이란?

우리가 엘리베이터를 타면 수리 중, 운행 중, 대기 중일 때에 동작이 다르다.

수리 중에는 버튼을 아무리 눌러도 동작하지 않도록 되어있고 운행 중에는 열림버튼이 동작하지 않는다.

대기 중일 때야 비로소 열림버튼이 정상적으로 동작을 한다.

이처럼 상태에 따라서 같은 행위를 하더라도 동작이 달라지도록 만드는 것이 바로 상태 패턴이다.

 

상태 패턴을 사용했을 때의 장점

  • 상태별로 복잡한 조건문을 작성하는 것 대신 클래스로 나누어서 코드 가독성이 높아진다.
  • 상태가 추가되거나 삭제될 때 기존의 코드를 거의 수정하지 않고 추가/수정할 수 있다.

언제 상태 패턴을 고려해야 하는가?

  • 상태에 따라서 객체의 행위가 달라지는 경우
  • 조건문으로 처리하기엔 많은 경우의 수가 존재하는 경우

상태 패턴을 사용했을 때의 단점

  •  다양한 상황을 처리하기 위해서 많은 클래스를 만들어 복잡해질 수 있다.
  • 오히려 시스템이 복잡해질 수 있다.

비슷한 패턴과 비교하기

  • 전략 패턴(Strategy Pattern)
    • 전략 패턴은 상속을 대체
    • 객체들끼리 완전히 독립적이여서 서로를 인식하지 못한다. (상태패턴은 마음대로 상태를 변경할 수 있음)

 

구현방법

https://www.youtube.com/watch?v=BeoiIfvAmpE

구현 예시는 해당 영상을 보고 참고하였습니다.

상태패턴 구조

  • 클라이언트: 컨텍스트를 사용하는 객체
  • 컨텍스트: 상태를 가지는 객체(상태에 따라 행동이 달라지는 클래스)
  • 상태 인터페이스 (State Interface): 각 상태별로 필요한 행동을 캡슐화한 인터페이스
  • 상태 구현체(Concrete State): 각 상태 구현체

구현 예시

호텔의 객실 예약시스템을 예시로 구현해 보면서 익혀보자

먼저 호텔의 객실은 예약가능상태, 예약상태, 체크인상태 3가지 종류가 존재한다.

또한 예약 -> 체크인 -> 체크아웃 순으로 진행되고 체크인 이후에는 예약을 취소할 수 없다.

먼저 상태 패턴을 적용하지 않은 코드를 살펴보자

public enum HotelState {
    AVAILABLE,
    RESERVED,
    OCCUPIED
}

public class HotelSystem {

    private HotelState hotelState;

    public HotelSystem(HotelState hotelState) {
        this.hotelState = HotelState.AVAILABLE;
    }

    public void reserveRoom() {
        if(this.hotelState == HotelState.AVAILABLE) {
            System.out.println("객실 예약 완료");
            this.hotelState = HotelState.RESERVED;
        } else if (this.hotelState == HotelState.RESERVED) {
            System.out.println("이미 예약된 객실입니다.");
        } else if (this.hotelState == HotelState.OCCUPIED) {
            System.out.println("이미 체크인 되었습니다.");
        } else {
            System.out.println("잘못된 예약 상태입니다.");
        }
    }

    public void checkIn() {
        if(this.hotelState == HotelState.RESERVED) {
            System.out.println("체크인 완료");
            this.hotelState = HotelState.OCCUPIED;
        } else if (this.hotelState == HotelState.AVAILABLE) {
            System.out.println("먼저 객실 예약이 필요합니다.");
        } else if (this.hotelState == HotelState.OCCUPIED) {
            System.out.println("이미 체크인 되었습니다.");
        } else {
            System.out.println("잘못된 예약 상태입니다.");
        }
    }

    public void checkOut() {
        if(this.hotelState == HotelState.OCCUPIED) {
            System.out.println("체크아웃 완료");
            this.hotelState = HotelState.AVAILABLE;
        } else if (this.hotelState == HotelState.AVAILABLE) {
            System.out.println("아직 체크인이 완료되지 않았습니다.");
        } else if (this.hotelState == HotelState.RESERVED) {
            System.out.println("아직 체크인이 완료되지 않았습니다.");
        } else {
            System.out.println("잘못된 예약 상태입니다.");
        }
    }

    public void cancelReservation() {
        if(this.hotelState == HotelState.RESERVED) {
            System.out.println("객실 예약 취소 완료");
            this.hotelState = HotelState.AVAILABLE;
        } else if (this.hotelState == HotelState.AVAILABLE) {
            System.out.println("예약되어 있지 않습니다.");
        } else if (this.hotelState == HotelState.OCCUPIED) {
            System.out.println("체크인 된 객실은 예약을 취소할 수 없습니다.");
        } else {
            System.out.println("잘못된 예약 상태입니다.");
        }

    }
}
  • 일단 딱 봐도 코드를 한눈에 파악하기 쉽지 않다.
  • 무엇보다 만약 객실의 상태가 추가되는 경우에는 많은 부분을 수정해야 한다... 

이를 상태패턴을 적용해서 개선해 보자.

먼저 호텔객실에 필요한 메소드들을 추출하여 인터페이스로 만들자.

public interface HotelRoomState {
    void reserveRoom();
    void checkIn();
    void checkOut();
    void cancelReservation();
}

이렇게 4가지의 메소드를 추출하였다.

각각 예약, 체크인, 체크아웃, 예약 취소의 기능을 담당한다

이를 사용해 상태별로 동작을 만들어주자.

public class ReservedRoomState implements HotelRoomState {

    private final HotelRoom room;

    public ReservedRoomState(HotelRoom room) {
        this.room = room;
    }

    @Override
    public void reserveRoom() {
        System.out.println("이미 예약된 객실입니다.");
    }

    @Override
    public void checkIn() {
        System.out.println("체크인 완료");
        room.changeState(new OccupiedRoomState(room));
    }

    @Override
    public void checkOut() {
        System.out.println("아직 체크인이 완료되지 않았습니다.");
    }

    @Override
    public void cancelReservation() {
        System.out.println("객실 예약 취소");
        room.changeState(new AvailableRoomState(room));
    }
}
public class OccupiedRoomState implements HotelRoomState{
    private final HotelRoom room;

    public OccupiedRoomState(HotelRoom room) {
        this.room = room;
    }

    @Override
    public void reserveRoom() {
        System.out.println("체크인 상태의 객실입니다.");
    }

    @Override
    public void checkIn() {
        System.out.println("이미 체크인 되었습니다.");
    }

    @Override
    public void checkOut() {
        System.out.println("체크아웃 완료");
        room.changeState(new AvailableRoomState(room));
    }

    @Override
    public void cancelReservation() {
        System.out.println("이미 체크인 되었습니다. 취소할 수 없습니다.");
    }
}
public class AvailableRoomState implements HotelRoomState{

    private final HotelRoom room;

    public AvailableRoomState(HotelRoom room) {
        this.room = room;
    }

    @Override
    public void reserveRoom() {
        System.out.println("객실 예약 완료");
        room.changeState(new ReservedRoomState(room));
    }

    @Override
    public void checkIn() {
        System.out.println("객실 예약이 필요합니다");
    }

    @Override
    public void checkOut() {
        System.out.println("아직 체크인이 완료되지 않았습니다.");
    }

    @Override
    public void cancelReservation() {
        System.out.println("예약된 객실이 없습니다.");
    }
}

이렇게 각 상태별로 자신의 상태일 때 어떻게 동작할지만  신경 써서 작성해 주면 된다.

이렇게 되면 상태별 동작을 한눈에 파악할 수 있다.

 

이런 상태를 가지고 있는 객체인 HotelRoom을 살펴보자

public class HotelRoom {

    private HotelRoomState state;


    public void changeState(HotelRoomState state) {
        this.state = state;
    }

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

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

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

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

    public String getState() {
        return state.getClass().getSimpleName();
    }
}
  • 호텔룸은 상태를 가지고 있고 상태에 따라 어떻게 행동할지를 HotelRoomState에게 동작시킨다. 이렇게 만들어줘서 상태에 따라 어떻게 행동할지를 정할 수 있다.

 

마지막으로 클라이언트의 코드를 살펴보자.

public class Client {
    public static void main(String[] args) {
        HotelRoom room = new HotelRoom();
        room.changeState(new AvailableRoomState(room));

        room.checkIn(); //객실 예약이 필요합니다
        room.checkOut(); //아직 체크인이 완료되지 않았습니다.

        room.reserveRoom(); //객실 예약 완료
        room.reserveRoom(); //이미 예약된 객실입니다.

        room.checkOut(); //아직 체크인이 완료되지 않았습니다.
        room.checkIn(); //체크인 완료
        room.cancelReservation(); //이미 체크인 되었습니다. 취소할 수 없습니다.

        room.checkOut(); //체크아웃 완료
        System.out.println("현재 상태: " + room.getState()); //현재 상태: AvailableRoomState
    }
}

 

 

반응형