8. 상태 패턴 (State)
객체의 내부 상태가 바뀔 때 행동도 함께 변하게 하는 상태 패턴을 알아봅니다.
한 줄 요약
객체가 “지금 어떤 상태인지”에 따라 다르게 행동해야 할 때, 상태마다 클래스를 하나씩 만들고 객체는 지금 상태에게 일을 넘기게 하는 패턴이에요.
문제: 상태가 늘어날수록 커지는 갈래 나누기
자판기를 만든다고 해 볼게요. 자판기는 세 가지 상태 중 하나예요.
- 동전없음: 동전을 기다리는 중
- 동전있음: 버튼만 누르면 음료수가 나오는 중
- 품절: 재고가 없어서 아무것도 못 하는 중
같은 버튼을 눌러도 상태에 따라 하는 일이 달라요. 가장 먼저 떠오르는 방법은 상태를 글자로 기억해 두고, 메서드마다 ~에 따라 나누자로 갈래를 나누는 거예요. 한 값을 보고 여러 갈래로 나눌 때 가장 깔끔한 방법이죠.
[자판기]를 설계하자:
'상태'를 [문자열]인 "동전없음"으로 정하자
'재고'를 [숫자]인 0으로 정하자
처음 만들어질 때 ([숫자]인 '재고') 다음과 같이 하자:
'나'의 '재고'를 '재고'로 정하자
만약 ('재고'가 0과 같다) 라면:
'나'의 '상태'를 "품절"로 정하자
<동전넣기>를 만들자 ():
'나'의 '상태'에 따라 나누자:
"동전없음" 인 경우:
"동전을 넣었어요."를 출력하자
'나'의 '상태'를 "동전있음"으로 정하자
"동전있음" 인 경우:
"이미 동전이 들어 있어요."를 출력하자
"품절" 인 경우:
"품절이에요. 동전을 돌려드릴게요."를 출력하자
<뽑기>를 만들자 ():
'나'의 '상태'에 따라 나누자:
"동전없음" 인 경우:
"동전을 먼저 넣어 주세요!"를 출력하자
"동전있음" 인 경우:
"음료수가 나와요! 덜컹!"을 출력하자
'나'의 '재고'를 ('나'의 '재고' - 1)로 정하자
만약 ('나'의 '재고'가 0과 같다) 라면:
'나'의 '상태'를 "품절"로 정하자
그렇지 않다면:
'나'의 '상태'를 "동전없음"으로 정하자
"품절" 인 경우:
"품절이에요."를 출력하자
'기계'를 [자판기]인 새로운 [자판기](1)로 정하자
'기계'의 <뽑기>()를 실행하자
'기계'의 <동전넣기>()를 실행하자
'기계'의 <뽑기>()를 실행하자
'기계'의 <동전넣기>()를 실행하자
실행해 보면 잘 돌아가고, 코드도 깔끔해 보여요.
실행 결과
동전을 먼저 넣어 주세요!
동전을 넣었어요.
음료수가 나와요! 덜컹!
품절이에요. 동전을 돌려드릴게요.
그런데 이 코드는 자라면서 아파져요. “점검 중” 상태를 하나 더 넣는다고 생각해 보세요.
<동전넣기>와<뽑기>, 모든 메서드의따라 나누자에"점검중" 인 경우:를 하나씩 더 붙여야 해요. 메서드가 열 개라면 열 곳이에요.- 한 곳이라도 빼먹으면 그 상태에서 버튼을 눌렀을 때 아무 일도 일어나지 않아요. 맞는 갈래가 없어도 에러가 나지 않아서 찾기도 어려워요.
"동전있음"을"동전 있음"으로 한 번만 잘못 써도 그 갈래는 조용히 건너뛰어져요.- “동전있음일 때 자판기는 어떻게 행동하지?“를 알려면 메서드 전부를 훑어봐야 해요. 한 상태의 이야기가 여기저기 흩어져 있거든요.
따라 나누자로도 안 되는 것
따라 나누자는 한 메서드 안의 긴만약사다리를 깔끔하게 정리해 줘요. 하지만 여러 메서드에 같은 나누기가 반복되는 것은 고쳐 주지 못해요. 상태가 늘어날 때 아픈 건 바로 이 반복이에요.
해결책: 상태를 객체로 만들기
상태 패턴은 생각을 뒤집어요. “메서드 안에서 상태를 나누는” 대신 “상태 안에 메서드를 모아요.”
- 모든 상태가 지킬 약속, 곧 규정을 만들어요. 상태라면
<동전넣기>와<뽑기>를 할 줄 알아야 해요. - 상태마다 클래스를 하나씩 만들고, 그 상태일 때 할 일만 적어요.
- 자판기는
'현재상태'하나만 기억하고, 버튼이 눌리면 지금 상태에게 그대로 넘겨요. - 상태를 바꾸고 싶으면
'현재상태'에 다른 상태 객체를 끼워요.
비유: 휴대폰의 소리 모드
휴대폰에 전화가 오는 일은 늘 똑같은데, 벨소리 모드면 울리고, 진동 모드면 떨리고, 무음 모드면 화면만 켜져요. 휴대폰이 “만약 벨소리 모드라면…“을 매번 따지는 게 아니라, 모드 버튼으로 반응하는 방식 자체를 갈아 끼운다고 생각하면 돼요. 상태 패턴의 상태 객체가 바로 이 “모드”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 상태 약속 | [상태] (규정) |
모든 상태가 가져야 할 메서드를 정해요 |
| 구체적인 상태 | [동전없음상태], [동전있음상태], [품절상태] |
그 상태일 때의 행동, 그리고 다음 상태로 넘어가는 시점을 알아요 |
| 맥락(주인) | [자판기] |
지금 상태 하나를 들고 있고, 일을 넘기기만 해요 |
하리로 만들기
똑같은 자판기를 상태 패턴으로 다시 만들어 볼게요. 코드 속 (참고: n단계)를 따라 읽어 보세요.
(참고: 1단계 — 모든 상태가 지킬 약속)
[상태]를 규정하자:
<동전넣기>가 있어야 한다 ()
<뽑기>가 있어야 한다 ()
(참고: 2단계 — 자판기는 지금 상태에게 일을 넘기기만 해요)
[자판기]를 설계하자:
'현재상태'를 [상태]인 비어있음으로 정하여 숨기자
'재고'를 [숫자]인 0으로 정하자
처음 만들어질 때 ([숫자]인 '재고') 다음과 같이 하자:
'나'의 '재고'를 '재고'로 정하자
만약 ('재고'가 0과 같다) 라면:
'나'의 '현재상태'를 새로운 [품절상태]()로 정하자
그렇지 않다면:
'나'의 '현재상태'를 새로운 [동전없음상태]('나')로 정하자
<상태바꾸기>를 만들자 ([상태]인 '새상태'):
'나'의 '현재상태'를 '새상태'로 정하자
<동전넣기>를 만들자 ():
'나'의 '현재상태'의 <동전넣기>()를 실행하자
<뽑기>를 만들자 ():
'나'의 '현재상태'의 <뽑기>()를 실행하자
(참고: 3단계 — 상태마다 클래스 하나, 그 상태일 때 할 일만 적어요)
[상태]를 따르는 [동전없음상태]를 설계하자:
'기계'를 [자판기]인 비어있음으로 정하여 숨기자
처음 만들어질 때 ([자판기]인 '기계') 다음과 같이 하자:
'나'의 '기계'를 '기계'로 정하자
<동전넣기>를 만들자 ():
"동전을 넣었어요."를 출력하자
'나'의 '기계'의 <상태바꾸기>(새로운 [동전있음상태]('나'의 '기계'))를 실행하자
<뽑기>를 만들자 ():
"동전을 먼저 넣어 주세요!"를 출력하자
[상태]를 따르는 [동전있음상태]를 설계하자:
'기계'를 [자판기]인 비어있음으로 정하여 숨기자
처음 만들어질 때 ([자판기]인 '기계') 다음과 같이 하자:
'나'의 '기계'를 '기계'로 정하자
<동전넣기>를 만들자 ():
"이미 동전이 들어 있어요."를 출력하자
<뽑기>를 만들자 ():
"음료수가 나와요! 덜컹!"을 출력하자
'나'의 '기계'의 '재고'를 ('나'의 '기계'의 '재고' - 1)로 정하자
만약 ('나'의 '기계'의 '재고'가 0과 같다) 라면:
'나'의 '기계'의 <상태바꾸기>(새로운 [품절상태]())를 실행하자
그렇지 않다면:
'나'의 '기계'의 <상태바꾸기>(새로운 [동전없음상태]('나'의 '기계'))를 실행하자
[상태]를 따르는 [품절상태]를 설계하자:
<동전넣기>를 만들자 ():
"품절이에요. 동전을 돌려드릴게요."를 출력하자
<뽑기>를 만들자 ():
"품절이에요."를 출력하자
'기계'를 [자판기]인 새로운 [자판기](1)로 정하자
'기계'의 <뽑기>()를 실행하자
'기계'의 <동전넣기>()를 실행하자
'기계'의 <뽑기>()를 실행하자
'기계'의 <동전넣기>()를 실행하자
결과는 앞의 코드와 한 글자도 다르지 않아요. 바뀐 건 코드의 모양뿐이에요. 이렇게 겉으로 하는 일은 그대로 두고 속 구조만 고치는 걸 리팩터링이라고 불러요.
실행 결과
동전을 먼저 넣어 주세요!
동전을 넣었어요.
음료수가 나와요! 덜컹!
품절이에요. 동전을 돌려드릴게요.
단계별로 다시 보기
- 1단계 — 약속 만들기.
[상태]를 규정하자는 “상태라면 이 메서드들이 꼭 있어야 해”라는 약속이에요. 새 상태를 만들면서<뽑기>를 빼먹으면 하리가 바로 알려 줘요(아래 예제). 앞의 코드처럼 조용히 아무 일도 안 일어나는 일이 없어져요. - 2단계 — 자판기는 일을 넘기기만.
[자판기]의<동전넣기>와<뽑기>에는따라 나누자도만약도 없어요. 지금 상태가 무엇이든'현재상태'에게 그대로 넘겨요. - 3단계 — 상태마다 클래스 하나. “동전있음일 때는 어떻게 행동하지?“가 궁금하면
[동전있음상태]만 보면 돼요. 다음에 어느 상태로 넘어갈지도 상태가 스스로 정해요.
약속을 어기면 어떻게 되는지 직접 볼까요? <뽑기>를 빼먹은 상태를 만들면, 그 상태를 쓰기도 전에 클래스를 만드는 순간 에러가 나요.
[상태]를 규정하자:
<동전넣기>가 있어야 한다 ()
<뽑기>가 있어야 한다 ()
[상태]를 따르는 [점검중상태]를 설계하자:
<동전넣기>를 만들자 ():
"점검 중이에요."를 출력하자
[품절상태]에는 왜'기계'가 없나요?
품절은 끝 상태라서 다른 상태로 넘어갈 일이 없어요. 자판기를 바꿀 일이 없으니 기억할 필요도 없어요. 상태 객체는 꼭 필요한 것만 들고 있으면 돼요.
새 상태를 더해 보면
이 구조의 진짜 힘은 상태를 하나 더 넣을 때 드러나요. “점검 중” 상태를 만든다면:
- 앞의 방식: 모든 메서드를 열어서
"점검중" 인 경우:를 붙여야 해요. - 상태 패턴:
[상태]를 따르는 [점검중상태]라는 새 클래스 하나만 만들면 돼요. 이미 있는 상태 클래스는 거의 건드리지 않아요.
직접 해 보기
위 코드에[점검중상태]를 더해 보세요. 동전을 넣으면 “점검 중이에요.“를 출력하고, 뽑기를 하면 아무것도 나오지 않아요. 그리고[자판기]에<점검시작>을 만들어서 지금 상태를[점검중상태]로 바꿔 보세요. 기존 상태 클래스는 하나도 고칠 필요가 없을 거예요.
언제 쓰면 좋을까요?
- 객체가 상태에 따라 완전히 다르게 행동하고, 상태가 세 개 이상이거나 앞으로 늘어날 것 같을 때
- 여러 메서드에 같은 상태 값으로 나누는 갈래(
따라 나누자나만약사다리)가 반복될 때 - “이 상태에서 저 상태로 넘어가는 규칙”이 복잡해서 한눈에 보고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 한 상태의 행동이 클래스 하나에 모여 있어 읽기 쉬워요 | 클래스 수가 늘어나요 |
| 새 상태를 더할 때 기존 코드를 거의 안 고쳐요 | 상태가 두 개뿐이고 늘 일 없다면 오히려 과해요 |
| 규정 덕분에 빠뜨린 행동을 하리가 잡아 줘요 | 상태끼리 서로를 알아야 해서(다음 상태를 만드니까) 얽힐 수 있어요 |
| 상태 값을 글자로 비교하지 않아서 오타로 틀릴 일이 없어요 |
너무 이른 패턴은 독이에요. 상태가 “켜짐/꺼짐” 두 개뿐이고 메서드도 하나라면,
만약한 번이 더 읽기 쉬워요. 갈래 나누기가 실제로 아프기 시작할 때 상태 패턴으로 옮겨도 늦지 않아요.