티스토리 뷰
‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.
단일 상품을 판매하는 자판기에 들어갈 소프트웨어를 개발해 달라는 요구가 있다고 해보자.
이 자판기의 동작 방식은 다음과 같다.
자판기 프로그램의 조건에 따른 코드를 다음과 같이 작성하였다.
public class VendingMachine {
public static enum State {NOCOIN, SELECTABLE}
private State state = State.NOCOIN;
public void insertCoin(int coin) {
switch (state) {
case NOCOIN:
increaseCoin(coin);
state = State.SELECTABLE;
break;
case SELECTABLE:
increaseCoin(coin);
}
}
public void select(int productId) {
switch (state) {
case NOCOIN:
break;
case SELECTABLE:
provideProduct(productId);
decreaseCoin();
if (hasNoCoin()) {
state = State.NOCOIN;
}
break;
}
}
public void increaseCoin(int coin) {
}
public void decreaseCoin() {
}
public void provideProduct(int productId) {
}
public boolean hasNoCoin() {
return true;
}
}
여기서 다음과 같은 새로운 요구 사항이 추가됐다고 가정해보자.
- 자판기에 제품이 없는 경우에는 동전을 넣으면 바로 동전을 되돌려 준다.
- 자동세척 중일 때에도 동전을 넣으면 바로 동전을 되돌려준다.
이같은 요구사항이 추가되었을 때 상태가 많아질수록 복잡해지는 조건문이 여러 코드에서 중복해서 출현하고, 그 만큼 코드 변경을 어렵게 만든다.
위 VendingMachine 클래스의 코드를 다시 살펴보면, 조건문은 다음과 같은 의미를 내포한다.
- 상태에 따라 동일한 기능 요청의 처리를 다르게 함
이렇게 기능이 상태에 따라 다르게 동작해야 할 때 사용할 수 있는 패턴이 상태(State) 패턴이다.
상태 패턴에서는 상태를 별도 타입으로 분리하고, 각 상태 별로 알맞은 하위 타입을 구현한다.
상태 패턴에서 중요한 점은 상태 객체가 기능을 제공한다는 점이다.
State 인터페이스는 동전 증가 처리와 제품 선택 처리를 할 수 있는 두 개의 메서드를 정의한다.
이 두 메서드는 모든 상태에 동일하게 적용되는 기능이다.
콘텍스트는 필드로 상태 객체를 가지고 있다.
콘텍스트는 클라이언트로부터 기능 실행 요청을 받으면, 상태 객체에 처리를 위임하는 방식으로 구현한다.
예를 들어 자판기 기능을 제공하는 VendingMachine 클래스의 insertCoin()과 select() 메서드는 다음과 같이 State 객체에 처리를 위임하는 방식으로 동작한다.
public class VendingMachine {
private State state;
public VendingMachine() {
state = new NoCoinState();
}
public void insertCoin(int coin) {
state.increaseCoin(coin, this); // 상태 객체에 위임
}
public void select(int productId) {
state.select(productId, this); // 상태 객체에 위임
}
public void changeState(State newState) {
this.state = newState;
}
}
위 코드에서 state 필드를 NoCoinState 객체로 초기화했는데, NoCoinState 클래스는 다음과 같이 구현된다.
public class NoCoinState implements State {
@Override
public void increaseCoin(int coin, VendingMachine vm) {
vm.increaseCoin(coin);
vm.changeState(new SelectableState());
}
@Override
public void select(int productId, VendingMachine vm) {
SoundUtil.beep();
}
}
increaseCoin() 메서드는 VendingMachine의 동전 수를 증가시키고, 상태를 변경한다.
select() 메서드는 에러 음을 발생시킨다.
이는 동전 없는 상태에서 음료를 선택하면 에러 음을 발생시킨다는 뜻이다.
NoCoinState 클래스와 유사하게 SelectableState 클래스는 음료 선택이 가능한 상태에서 동전을 넣을 때와 음료를 선택할 때의 자판기 동작 방식을 구현한다.
public class SelectableState implements State {
@Override
public void increaseCoin(int coin, VendingMachine vm) {
vm.increaseCoin(coin);
}
@Override
public void select(int productId, VendingMachine vm) {
vm.provideProduct(productId);
vm.decreaseCoin();
if (vm.hasNoCoin()) {
vm.changeState(new NoCoinState());
}
}
}
상태 패턴의 장점은 새로운 상태가 추가되더라도 콘텍스트 코드가 받는 영향은 최소화된다.
다음 장점은 상태에 따른 동작을 구현한 코드가 각 상태 별로 구분되기 때문에 상태 별 동작을 수정하기가 쉽다.
상태 패턴을 적용한 경우 해당 상태를 표현하는 클래스를 수정해주면 된다.
관련된 코드가 한 곳에 모여 있기 때문에 안전하고 더 빠르게 구현을 변경할 수 있게 된다.
상태 변경은 누가?
상태 패턴을 적용할 때 고려할 문제는 콘텍스트의 상태 변경을 누가 하느냐다.
상태 변경을 하는 주체는 콘텍스트나 상태 객체 둘 중 하나가 된다.
앞서 코드에서는 각 상태 객체에서 콘텍스트의 상태를 변경해 주었다.
상태 객체에서 콘텍스트의 상태를 변경하려면 콘텍스트의 다른 값에 접근해야 할 때도 있다.
SelectableState 클래스의 select() 메서드가 그 예다.
VendingMachine 클래스의 hasNoCoin() 메서드를 사용하고 있다.
이는 상태 객체에서 콘텍스트의 상태를 변경할 수 있는 조건을 확인할 수 있도록 콘텍스트 인터페이스에 메서드를 추가해야 한다는 것을 의미한다.
콘텍스트에서 상태를 변경할 경우 콘텍스트의 코드가 다소 복잡해질 수 있다.
앞서 VendingMachine 클래스 예제에서 콘텍스트가 직접 상태를 변경하도록 VendingMachine 클래스를 수정하면 다음과 같이 코드가 바뀐다.
public class VendingMachine {
private State state;
public VendingMachine() {
state = new NoCoinState();
}
public void insertCoin(int coin) {
state.increaseCoin(coin, this); // 상태 객체에 위임
if (hasCoin()) {
changeState(new SelectableState());
}
}
public void select(int productId) {
state.select(productId, this); // 상태 객체에 위임
if (hasNoCoin()) {
changeState(new NoCoinState());
}
}
private void changeState(State newState) {
this.state = newState;
}
private boolean hasCoin() {
return true;
}
private boolean hasNoCoin() {
return !hasCoin();
}
// 다른 기능은 생략
}
위 메서드들을 private으로 바꾼 이유는 상태 객체에서 콘텍스트의 상태를 변경하기 위해 메서드에 접근할 필요가 없어졌기 때문이다.
이제 상태 객체는 자신이 수행해야 하는 작업만 처리하도록 changeState 사용 코드를 삭제해준다.
콘텍스트의 상태 변경을 누가 할지는 주어진 상황에 맞게 정해 주어야 한다.
1. 콘텍스트에서 상태를 변경할 경우
비교적 상태 개수가 적고 상태 변경 규칙이 거의 바뀌지 않는 경우에 유리하다.
상태 종류가 지속적으로 변경되거나 상태 변경 규칙이 자주 바뀔 경우 콘텍스트의 상태 변경 처리 코드가 복잡해질 가능성이 높기 때문이다.
상태 변경 처리 코드가 복잡해질수록 상태 변경의 유연함이 떨어지게 된다.
2. 상태 객체에서 콘텍스트의 상태를 변경할 경우
콘텍스트에 영향을 주지 않으면서 상태를 추가하거나 상태 변경 규칙을 바꿀 수 있게 된다.
상태 변경 규칙이 여러 클래스에 분산되어 있기 때문에, 상태 구현 클래스가 많아질수록 상태 변경 규칙을 파악하기가 어려워지는 단점이 있다.
한 상태 클래스에서 다른 상태 클래스에 대한 의존도가 발생한다.
결론은 두 방식 중 주어진 상황에 어떤 게 더 잘 맞나 생각한 후 선택해야 한다.
'JAVA > 디자인 패턴' 카테고리의 다른 글
어댑터(Adapter) 패턴 (0) | 2022.03.01 |
---|---|
프록시(proxy) 패턴 (0) | 2022.03.01 |
데코레이터(Decorator) 패턴 (0) | 2022.03.01 |
템플릿 메서드 패턴 (0) | 2022.02.23 |
전략(Strategy) 패턴 (0) | 2022.02.23 |
- Total
- Today
- Yesterday
- 프로그래머스
- Java
- Spring
- Kotlin
- 디자인 패턴
- 디자인패턴
- 알고리즘
- kotest
- programmers
- 클린 아키텍처
- 이팩티브 자바
- AWS
- kkoon9
- Olympiad
- node.js
- Algorithm
- Effective Java
- 테라폼
- BOJ
- JPA
- 백준
- 정규표현식
- BAEKJOON
- 클린 코드
- 코테
- 이펙티브 자바
- 객체지향
- Spring Boot
- C++
- MSA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |