변화하는 상태, 변화하는 행동
같은 사람이라도 기분에 따라, 상황에 따라 완전히 다른 모습을 보이곤 합니다. 아침에 일어났을 때의 나와 커피를 마신 후의 나는 다른 사람 같고, 배가 고플 때의 나와 맛있는 식사를 한 후의 나 역시 전혀 다른 반응을 보입니다.
우리 주변의 모든 것들이 상태에 따라 다르게 행동합니다. 자판기는 동전이 들어가기 전과 후가 다르고, 신호등은 빨간불일 때와 초록불일 때 전혀 다른 의미를 갖습니다. '상태에 따른 행동 변화'를 스테이트라고 합니다.
자판기를 예로 들어보겠습니다. 처음엔 그저 큰 상자 같아 보이지만, 자세히 들여다보면 복잡한 상태 기계입니다. 동전을 넣지 않은 상태에서는 아무리 버튼을 눌러도 "동전을 넣어주세요"라는 차가운 메시지만 뜹니다. 하지만 동전을 넣는 순간, 자판기는 완전히 다른 존재가 됩니다. 갑자기 친절해지며 "원하는 음료를 선택해 주세요"라고 말하고, 모든 버튼이 반짝이며 여러분의 선택을 기다립니다.
전통적인 프로그래밍 방식이라면 이런 자판기를 만들 때 수많은 if-else 문을 사용했을 것입니다. "만약 동전이 없다면 이렇게 하고, 동전이 있다면 저렇게 하고, 음료가 떨어졌다면 또 다르게 하고..." 하지만 이런 방식은 상태가 복잡해질수록 로직이 얽히게 됩니다.
스테이트 패턴은 이런 문제를 해결하기 위해 각 상태를 별도의 클래스로 만듭니다. '동전 없음' 상태 클래스, '동전 있음' 상태 클래스, '음료 선택 중' 상태 클래스처럼 말이죠. 각 상태 클래스는 그 상태에서만 가능한 행동들을 담고 있습니다. 마치 연극에서 배우가 장면에 따라 다른 역할을 연기하듯이, 자판기는 상황에 따라 다른 상태 클래스의 행동을 수행합니다.
1943년 신경생리학자 워렌 맥컬록(Warren McCulloch)과 논리학자 월터 피츠(Walter Pitts)가 발표한 논문 "A Logical Calculus Immanent in Nervous Activity"에서 유한 상태 기계(FSM: Finite State Machine)의 개념을 제시했습니다. 이 논문은 신경망의 추상적인 모델을 제시하며, 유한 오토마타의 초기 형태를 보여주었습니다. 이후 컴퓨터 과학의 발전과 함께 FSM 개념이 더욱 구체화되고 정교해졌습니다.
‘유한 상태 기계(FSM)’의 개념을 기본으로 GoF는 상태를 단순한 변수가 아닌 '객체'로 취급하여 각 상태가 고유한 행동을 가질 수 있도록 하는 혁신적인 접근법을 제시한 패턴이 스테이트 패턴입니다.
간단하게 전등의 상태에 대한 예제를 보겠습니다.
// State 인터페이스
interface LightState {
void pressButton();
String getStatus();
}
// Context - 전등
class Light {
private LightState offState;
private LightState lowState;
private LightState mediumState;
private LightState highState;
private LightState currentState;
public Light() {
offState = new OffState(this);
lowState = new LowState(this);
mediumState = new MediumState(this);
highState = new HighState(this);
currentState = offState; // 초기 상태는 꺼진 상태
}
public void pressButton() {
currentState.pressButton();
}
public void setState(LightState state) {
currentState = state;
}
public String getStatus() {
return currentState.getStatus();
}
// Getter 메서드들
public LightState getOffState() { return offState; }
public LightState getLowState() { return lowState; }
public LightState getMediumState() { return mediumState; }
public LightState getHighState() { return highState; }
}
// 구체적인 상태들
class OffState implements LightState {
private Light light;
public OffState(Light light) {
this.light = light;
}
@Override
public void pressButton() {
System.out.println("� 약하게 켜짐");
light.setState(light.getLowState());
}
@Override
public String getStatus() {
return "꺼짐";
}
}
class LowState implements LightState {
private Light light;
public LowState(Light light) {
this.light = light;
}
@Override
public void pressButton() {
System.out.println("� 보통으로 밝아짐");
light.setState(light.getMediumState());
}
@Override
public String getStatus() {
return "약함";
}
}
class MediumState implements LightState {
private Light light;
public MediumState(Light light) {
this.light = light;
}
@Override
public void pressButton() {
System.out.println("� 최대 밝기!");
light.setState(light.getHighState());
}
@Override
public String getStatus() {
return "보통";
}
}
class HighState implements LightState {
private Light light;
public HighState(Light light) {
this.light = light;
}
@Override
public void pressButton() {
System.out.println("⚫ 꺼짐");
light.setState(light.getOffState());
}
@Override
public String getStatus() {
return "강함";
}
}
// 사용 예제
public class LightDemo {
public static void main(String[] args) {
Light light = new Light();
System.out.println("3단계 조명 테스트");
System.out.println("현재 상태: " + light.getStatus());
// 버튼을 5번 눌러보기
for (int i = 1; i <= 5; i++) {
System.out.print(i + "번째 버튼 누르기: ");
light.pressButton();
System.out.println(" → 현재 상태: " + light.getStatus());
}
}
}
위의 코드를 실행하면 아래 결과와 같이 전등의 상태가 변화되는 것을 볼 수 있습니다.
3단계 조명 테스트
현재 상태: 꺼짐
1번째 버튼 누르기: � 약하게 켜짐
→ 현재 상태: 약함
2번째 버튼 누르기: � 보통으로 밝아짐
→ 현재 상태: 보통
3번째 버튼 누르기: � 최대 밝기!
→ 현재 상태: 강함
4번째 버튼 누르기: ⚫ 꺼짐
→ 현재 상태: 꺼짐
5번째 버튼 누르기: � 약하게 켜짐
→ 현재 상태: 약함
Light 객체에서 currentState는 현재의 상태를 나타내고, pressButton이 실행될 때마다 해당 상태 클래스를 전환합니다.
위처럼 객체의 상태에 따라 행동이 달라지는 경우, 스테이트 패턴을 사용하지 않으면 if-else if 또는 switch-case 문이 남용되기 쉽습니다. 이러한 코드는 상태가 추가되거나 변경될 때마다 수정이 필요하며, 코드의 복잡성이 증가하고 유지보수가 어려워집니다. 스테이트 패턴은 각 상태를 별도의 클래스로 분리함으로써 이러한 복잡성을 줄이고, 각 상태에 대한 코드를 응집도 있게 관리할 수 있도록 합니다. 이는 코드의 가독성을 높이고, 특정 상태와 관련된 로직을 쉽게 찾고 수정할 수 있게 해 줍니다.
상태(State)는 현재의 상황을 보여줍니다. FrontEnd 개발에서는 가장 중요한 핵심이며, BackEnd 비즈니스 로직을 수행하는 트리거입니다. 상태를 정의하거나, 상태가 전환될 때 어떤 동작과 수행, 노출이 되는지 미리 정의하는 것이 설계입니다.
우리는 고정된 하나의 '나'가 아니라, 주변 환경, 관계, 감정 상태에 따라 끊임없이 변화하는 '상태 객체'들로 이루어진 복합적인 존재입니다. 각 상태가 '현재의 나'를 정의하는 한 조각이라면, 우리는 그 상태에서 어떤 행동을 할 것인지에 대한 자유와 책임을 가지며, 어떤 상태로 전이될 것인지도 개인의 의지와 선택에 달려 있습니다. 자기 계발이란 이런 상태 전환에 따라 어떤 선택과 정의를 하는지 설계하는 것입니다.