개요

상태 패턴은 객체 내부의 상태에 따라 행동을 제한하거나, 다른 상태 변경을 제어할 때 유용하게 사용할 수 있다.

상태 패턴을 사용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있습니다. 1

구조 및 참여자

상태 패턴은 전략 패턴과 구조가 비슷하다.

  • State
    • 상태를 정의하는 인터페이스이다.
    • 상태가 할 수 있는 행동에 대해 이 인터페이스에서 정의한다.
  • ConcreteState
    • 상태를 구현하는 구현체로, 구현체마다 자신이 할 수 있는 행동을 구현한다.
    • Context에게 상태 변경을 위임한다.
  • Context
    • State를 사용하는 객체

예제

헤드 퍼스트 디자인 패턴

헤드 퍼스트 디자인 패턴에서는 뽑기 기계를 통해 예시를 들고 있다.

  • 상태는 매진, 동전 없음, 동전 있음, 뽑는중 등이 있다.
  • 행동은 코인 추가, 코인 반환, 손잡이 돌리기, 알맹이 내보내기 등이 있다.

현재 상태에 따라 내가 하려는 행동에 대해 변경할 수 있는/없는 상태가 있고, 이를 if-else 문으로 구현할 수 있다.

kotlin으로 간단하게 작성한 예시로는 다음과 같다.

fun insertQuarter() {
	if (state == "동전 없음") {
		println("코인을 추가합니다")
		state = "동전 있음"
	} else if (state == "매진") {
		println("매진 상태에서는 코인을 넣을 수 없습니다.")	
	} else if (state == "뽑는중") {
		println("뽑는중에서는 코인을 넣을 수 없습니다.")	
	}
}

fun ejectQuarter() {
	// (생략) 코인을 반환하는 행동에 따른 상태 처리 로직 ...
}

fun turnCrank() {
	// (일부 생략) 손잡이를 돌리는 행동에 따른 상태 처리 로직 ...
	
	if (state == "동전 있음") {
		state = "뽑는중"
		dispense()
	}
}

fun dispense() {
	// (생략) 알맹이 내보낸 후에 따른 상태 처리 로직 ...
}

각 행동에 따라 상태 처리에 따른 로직이 모두 달라지게 된다.

그렇다면 위와 같은 코드에서 새로운 상태가 추가된다면? 이벤트로 1/10 확률로 알맹이 2개를 내보내야 한다면 어떻게 처리할 것인가?

물론 if-else문에서 계속 추가하는 방식으로 구현할 수 있다. 하지만 이는 많은 코드 변경이 이루어지고, 복잡한 상태에서 실수할 여지가 커지게 된다.

그렇다면 이런 바뀌는 부분을 캡슐화하도록 설계를 하면 어떨까?

헤드 퍼스트 디자인에서는 다음과 같이 코드를 변경한다.

interface State {
	void insertQuarter();
	void ejectQuarter();
	void turnCrank();
	void dispense();
}

class NoQuarterState implements State {
	GumballMachine gumballMachine;

	public NoQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	@Override
	public void insertQuarter() {
		System.out.println("동전을 투입합니다.");
		this.gumballMachine.setState(gumballMachine.getHasQuaterState());
	}

	// (중략) 나머지 메서드들 ...
}

class HasQuarterState implments State {

	GumballMachine gumballMachine;
	
	public HasQuarterState(GumballMachine gumballMachine) {
		this.gumballMachine = gumballMachine;
	}

	@Override
	public void insertQuarter() {
		System.out.println("이미 동전이 있습니다");
	}

	// (중략) 나머지 메서드들 ...
}

// 나머지 클래스들 ...

상태라는 인터페이스를 정의하고, 해당 인터페이스에 해야할 행동들을 정의한다.

그리고 각 상태별로 행동에 대해 제어한다.

새로운 상태가 추가된다면 상태 인터페이스를 구현하는 구현체를, 행동이 추가된다면 상태 인터페이스에 메서드를 추가한다. 이로 인해 필요한 부분에 대해서만 구현을 할 수 있도록 변경해주며, SOLID 원칙 중 OCP 원칙을 지킬 수 있게 된다.

개인 프로젝트 User 상태 패턴 적용

개인 프로젝트 코드에 상태 패턴을 적용해보면 어떨까 궁금해서 한번 해보았다.

아래는 유저 상태를 상태 패턴으로 작성해본 예제이다.

  • 상태가 register -> activate -> deleted로 변경 가능할 때 다음과 같이 작성할 수 있다.
interface UserState {
    fun signUp()
    fun activate()
    fun delete()
}

class RegisterUserState(
    private val user: User
) : UserState {

    override fun signUp() {
        println("이미 가입된 유저입니다.")
    }

    override fun activate() {
        println("유저가 활성화 되었습니다.")
        user.state = user.activateUserState
    }

    override fun delete() {
        println("활성화 상태에서만 유저를 삭제할 수 있습니다.")
    }
}

class ActivateUserState(
    private val user: User
) : UserState{
    override fun signUp() {
        println("이미 가입한 유저입니다.")
    }

    override fun activate() {
        println("이미 활성화된 유저입니다.")
    }

    override fun delete() {
        println("유저 삭제 처리를 진행합니다.")
        user.state = user.deleteUserState
    }
}

class DeleteUserState(
    private val user: User
): UserState {
    override fun signUp() {
        println("삭제된 유저입니다.")
    }

    override fun activate() {
        println("삭제된 유저입니다.")
    }

    override fun delete() {
        println("이미 삭제된 유저입니다.")
    }
}


class User() {
    var activateUserState: ActivateUserState
    var registerUserState: RegisterUserState
    var deleteUserState: DeleteUserState

    var state: UserState;

    init {
        this.activateUserState = ActivateUserState(this)
        this.registerUserState = RegisterUserState(this)
        this.deleteUserState = DeleteUserState(this)

        this.state = this.registerUserState
    }

    fun activate() {
        this.state.activate()
    }

    fun delete() {
        this.state.delete()
    }
}

fun main() {
    val user = User()
    user.activate()
    user.activate()

    user.delete()
    user.delete()
}

실행 결과

유저가 활성화 되었습니다.
이미 활성화된 유저입니다.
유저 삭제 처리를 진행합니다.
이미 삭제된 유저입니다.

이렇게 보니 지금은 상태 패턴이 과한 것 같다. 상태가 register -> activate -> deleted 단방향으로만 진행되기 때문에 그런 것 같다. deleted 상태에서 다시 activate로 갈 수 있는 등 여러 복잡한 상태에서는 유용하게 사용할 수 있을 것 같다.

참고 자료

  • 헤드 퍼스트 디자인 패턴 - 개정판

각주

  1. 헤드 퍼스트 디자인 패턴 - 개정판 p440