10.State Pattern
1. 일상에서 생각해 볼 수 있는 문제
우리 생활 속에는 상태에 따라 수행되는 일들이 달라지는 경우가 많다.
일상 생활에서 타는 대중교통을 예로 들어 보자
3,000 원이 충전되어 있고 버스비가 3,000원을 넘긴다면
카드 리더기에서는 잔액이 부족하다고 할 것이다.
3,000 원 이상인 경우는 정상 적으로 결제가 될 것이고
이미 결제가 완료된 경우에는 환승이 찍히고,
환승까지 찍힌상태의 카드는 이미 처리된 카드라고 말하는 것을 들은 적이 있을 것이다.
이를 참고하여 코드를 작성하면 다음과 같다.
BusCard.java
class BusCard {
// 상태를 나타내는 상수 정의
private static final int INSUFFICIENT_BALANCE = 0;
private static final int PAYMENT_COMPLETED = 1;
private static final int TRANSFER_COMPLETED = 2;
private static final int ALREADY_PROCESSED = 3;
private int state;
private int balance;
public BusCard(int initialBalance) {
this.balance = initialBalance;
// 초기 상태 설정
if (this.balance < 1200) {
this.state = INSUFFICIENT_BALANCE;
} else {
this.state = PAYMENT_COMPLETED;
}
}
public void tapCard(int fare) {
if (this.state == INSUFFICIENT_BALANCE) {
System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
} else if (this.state == PAYMENT_COMPLETED) {
if (fare > this.balance) {
System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
} else {
System.out.println("결제가 완료되었습니다. " + fare + "원이 결제되었습니다.");
this.balance -= fare;
this.state = TRANSFER_COMPLETED;
}
} else if (this.state == TRANSFER_COMPLETED) {
System.out.println("환승이 완료되었습니다.");
this.state = ALREADY_PROCESSED;
} else if (this.state == ALREADY_PROCESSED) {
System.out.println("이미 처리된 카드입니다.");
}
}
public String getStateName() {
if (this.state == INSUFFICIENT_BALANCE) {
return "잔액부족";
} else if (this.state == PAYMENT_COMPLETED) {
return "결제완료";
} else if (this.state == TRANSFER_COMPLETED) {
return "환승완료";
} else {
return "이미처리됨";
}
}
}
Execute
public class BusCardSystem {
public static void main(String[] args) {
// 잔액이 3000원인 버스 카드를 생성합니다.
BusCard myCard = new BusCard(3000);
System.out.println("--- 버스 요금 1200원 결제 시도 ---");
myCard.tapCard(1200); // 정상 결제
System.out.println("현재 상태: " + myCard.getStateName());
System.out.println("--- 30분 이내에 환승 시도 ---");
myCard.tapCard(0); // 환승
System.out.println("현재 상태: " + myCard.getStateName());
System.out.println("--- 30분 이내에 다시 태그 시도 ---");
myCard.tapCard(0); // 이미 처리된 카드
System.out.println("현재 상태: " + myCard.getStateName());
System.out.println("\n--- 잔액 0원으로 새로운 카드 생성 ---");
BusCard emptyCard = new BusCard(0);
System.out.println("--- 버스 요금 1200원 결제 시도 ---");
emptyCard.tapCard(1200); // 잔액 부족
System.out.println("현재 상태: " + emptyCard.getStateName());
}
}
이렇게 코드로 나타내면 충분히 나타낼 수 있지만
조건문 ( if - else ) 로 도배가 된 로직을 볼 수가 있다.
여기서 만약 state 가 추가가 된다면? 여기서 만약 tapCard() 기능 뿐만 아니라
balanceCharege() 기능이 추가가 된다면 코드 수정하는 것이 어려워 질 것으로 생각이된다.
2. 논리적으로 풀어보기
이 문제를 해결하기 위해서 추상화 개념을 마음껏 써보자 각
상태마다 특정 메서드에 대한 결과 값이 달라진다는 것을 생각했을 때,
반복되는 상태 라는 개념이 추상화 되면 좋을 것 같다는 생각이 든다 .
이를 추상적으로 하기 위해서 상태를 interface 로 바꿔 보았다.
CardState.java
interface CardState {
void handleTap(BusCard card, int fare);
String getStateName();
}
위의 코드에서 공통적으로 사용하는 메서드는 카드를 tap 하는 것이 전부이기 때문에
위와 같이 정의했다.
이후 각각의 상태를 아래 처럼 구현 클래스로 정의를 해보았다.
InsufficentBalanceState.java
// 구체적인 상태 클래스들
class InsufficientBalanceState implements CardState {
@Override
public void handleTap(BusCard card, int fare) {
System.out.println("잔액이 부족합니다. 결제에 실패했습니다.");
}
@Override
public String getStateName() {
return "잔액부족";
}
}
PaymentCompletedState.java
class PaymentCompletedState implements CardState {
@Override
public void handleTap(BusCard card, int fare) {
System.out.println("결제가 완료되었습니다. " + fare + "원이 결제되었습니다.");
card.setBalance(card.getBalance() - fare);
card.setState(new TransferState());
}
@Override
public String getStateName() {
return "결제완료";
}
}
TransferState.java
class TransferState implements CardState {
@Override
public void handleTap(BusCard card, int fare) {
System.out.println("환승이 완료되었습니다.");
card.setState(new AlreadyProcessedState());
}
@Override
public String getStateName() {
return "환승완료";
}
}
AlreadyProcessedState.java
class AlreadyProcessedState implements CardState {
@Override
public void handleTap(BusCard card, int fare) {
System.out.println("이미 처리된 카드입니다.");
}
@Override
public String getStateName() {
return "이미처리됨";
}
}
그 다음 위의 상태 구현체들을 사용하는 클래스를 정의해보자
BusCard.java
class BusCard {
private CardState state;
private int balance;
private static final int BASE_FARE = 1200;
public BusCard(int initialBalance) {
this.balance = initialBalance;
// 초기 상태 설정
if (balance < BASE_FARE) {
this.state = new InsufficientBalanceState();
} else {
this.state = new PaymentCompletedState();
}
}
public void setState(CardState state) {
this.state = state;
}
public CardState getState() {
return state;
}
public void tapCard(int fare) {
if (fare > 0 && balance < fare && this.getState().getStateName().equals("결제완료")) {
// 결제 완료 상태에서만 잔액 부족을 체크
this.setState(new InsufficientBalanceState());
this.state.handleTap(this, 0);
} else {
this.state.handleTap(this, fare);
}
}
// 이외의 메서드는 편의를 위해 추가
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public String getStateName() {
return state.getStateName();
}
}
이렇게 정의하면 if-else 로 도배되지 않고 새로운 method 가 추가 되면
각 구현체에 로직을 새롭게 정의하면 된다. 또한 각각의 상태에서만 필요한 메서드들을 따로 정의할 수 있어
역할 분담을 명확하게 할 수 있게 된다.
3. State Pattern
위에서 리팩토링 한 결과가 State Pattern 이다.
상태 패턴은
객체의 내부 상태가 바뀜에 따라 객체의 행위(behavior)를 변경할 수 있도록 하는 디자인 패턴이다.
주요한 특징으로
- 상태 캡슐화
각 상태를 별도의 클래스로 캡슐화한다. 관련된 로직만 존재 가능
- 행위의 다형성
동일한 인터페이스를 구현하며, 상태에 따라 다양한 결과를 받을 수 있다.
- 동적 상태 변경
런타임에 동적으로 상태가 변경될 수 있다.
- 개방-폐쇄 원칙(Open-Closed Principle) 준수
새로운 상태가 추가되어도 새로운 상태 클래스만 추가하면 된다.
장점
코드 유지보수성 향상
`if-else`나 `switch-case`문이 제거되어 코드가 훨씬 간결해지고 이해하기 쉬워 진다.
확장성
새로운 상태를 추가하기가 매우 쉽다.
상태 전이 로직 분리
상태 간의 전환 로직이 각 상태 클래스 내에 존재하게 되어,전이가 필요한 곳에 로직을 분산시킬 수 있다.
단점
클래스 증가
상태가 많아질수록 클래스의 수가 증가
초기 설계 복잡성 증가
간단한 로직에는 과도한 패턴일 수 있다. 오히려 if-else 가 간결 할 수 있다.



