무지를 아는 것이 곧 앎의 시작

Java

상태 패턴 정리

Alex96 2022. 4. 12. 02:49

상태 패턴은 현재 상태에서 어떤 행위를 했을 때 그 행위를 실행하고 다음 상태를 반환하는 것을 말합니다.

무슨 뜻이냐면, 어떤 상태라는 것을 인터페이스로 추상화 하고 그 인터페이스에 행위 메서드들을 정의합니다.

그리고 특정 상태를 나타내는 구체 클래스들을 구현하고, 그 상태에서 하는 행위마다 다음 상태를 반환하도록 메서드를 구현합니다.

 

설명은 이게 전부고 바로 코드로 알아보죠.🧐

 

초등학교 시절 게임방에서 100원 넣고 했던 격투 게임이 떠올라서 그걸 토대로 상태를 한번 디자인 해봤는데요.👊

게임 캐릭터를 기준으로 상태를 한번 정의해봤어요!

 

일단 구조를 파악할 수 있게 클래스 다이어그램을 봅시다!

격투게임 캐릭터 상태 클래스 다이어그램

 

CharacterState에서는 게임 캐릭터가 할 수 있는 모든 행위를 정의!

public interface CharacterState {

    CharacterState punch();

    CharacterState kick();

    CharacterState jump();

    CharacterState crouch();

    CharacterState guard();

    CharacterState stand();

    CharacterState hit();

    CharacterState skill1();

    CharacterState skill2();
}

 

구조를 설명 드릴게요! 자 일단 게임을 시작한 모든 캐릭터의 공통되는 행위 부분을 Playing이라는 추상 클래스에 메서드로 정의했어요! 피격이라든지, 하던 동작이 끝났을 때 Standing상태로 되는 stand()메서드를 모든 상태에서 사용하는 공통 메서드로 생각하고 정의했습니다.

 

Playing

public abstract class Playing implements CharacterState {

    @Override
    public CharacterState hit() {
        System.out.println("맞았으니까 경직 ㅋㅋ");
        return new Rigid();
    }

    @Override
    public CharacterState stand() {
        System.out.println("하던 동작이 멈추면 똑바로 섬!");
        return new Standing();
    }
}

 


 

자 맨 꼭대기에 Playing이라는 메서드로 게임에 참여하는 캐릭터 상태를 추상화 했는데요, 여기서 요구사항에 가만히 서있는 상태에서만 스킬을 사용할 수 있다! 라는 요구사항이 들어왔다고 칩시다. 그래서 스킬을 사용할 수 있는 상태와 사용이 불가능한 상태를 나눴어요. 각각 메서드를 보면 해당 행위를 한 이후에 다음 상태를 반환합니다.

 

UnableToSkill, AbleToSkill

public abstract class UnableToSkill extends Playing {

    @Override
    public CharacterState skill1() {
        throw new IllegalStateException("이 상태에선 스킬을 못써요!");
    }

    @Override
    public CharacterState skill2() {
        throw new IllegalStateException("이 상태에선 스킬을 못써요!");
    }
}

public abstract class AbleToSkill extends Playing {
}

UnableToSkill의 하위 구현 클래스들은 당연히 스킬을 사용하는게 문제가 되겠죠, 그래서 스킬을 사용하는 메서드들은 공통으로 IllegalStateException을 던지도록 구현했습니다.

 


 

AbleToSkill의 하위에는 Standing이라는 구체 클래스가 있는데요, 이 상태는 캐릭터가 가만히 서 있는 상태라서 모든 행위가 가능해야 합니다.

 

Standing

public class Standing extends AbleToSkill {

    @Override
    public CharacterState punch() {
        System.out.println("서서 주먹 공격!");
        return new Standing();
    }

    @Override
    public CharacterState kick() {
        System.out.println("서서 발차기 공격!");
        return new Standing();
    }

    @Override
    public CharacterState jump() {
        System.out.println("점프해서 공중에 뜸!");
        return new Jumping();
    }

    @Override
    public CharacterState crouch() {
        System.out.println("아래로 숙이기!");
        return new Crouching();
    }

    @Override
    public CharacterState guard() {
        System.out.println("가드 올림!");
        return new Guard();
    }

    @Override
    public CharacterState skill1() {
        System.out.println("가벼운 스킬1 사용!");
        return new Standing();
    }

    @Override
    public CharacterState skill2() {
        System.out.println("스킬2는 좀 무거운 스킬이라 사용후 경직 있음!");
        return new Rigid();
    }
}

상위 클래스에서 구현한 공통된 행위들을 제외하고는 모든 행위를 실행하고 다음 상태를 반환하도록 구현했습니다.

 


 

자 그럼 UnableToSkill의 하위 클래스들을 봅시다. 여기선 또 공격이라는 행위를 할 수 있는 상태와 못하는 상태로 나뉘어요.

 

UnableToAttack, AbleToAttack

public abstract class UnableToAttack extends UnableToSkill {

    @Override
    public CharacterState punch() {
        throw new IllegalStateException("이 상태에선 공격이 불가능합니다!");
    }

    @Override
    public CharacterState kick() {
        throw new IllegalStateException("이 상태에선 공격이 불가능합니다!");
    }
}

public abstract class AbleToAttack extends UnableToSkill {
}

UnableToAttack의 하위 상태에선 공격 메서드를 호출하면 안되기 때문에 예외를 던지도록 구현했습니다!

 


 

그럼 UnableToAttack 상태의 하위 구체 클래스들을 봅시다.

캐릭터가 경직일 상태를 나타내는 Rigid와 가드를 올리고 있는 상태인 Guard가 있네요

 

Rigid

public class Rigid extends UnableToAttack {
    @Override
    public CharacterState jump() {
        throw new IllegalStateException("경직 상태에선 암것도 못해용 ㅠㅠ");
    }

    @Override
    public CharacterState crouch() {
        throw new IllegalStateException("경직 상태에선 암것도 못해용 ㅠㅠ");
    }

    @Override
    public CharacterState guard() {
        throw new IllegalStateException("경직 상태에선 암것도 못해용 ㅠㅠ");
    }
}

Rigid상태일 때는 어떤 행위도 하면 안된다고 설계했어요! 모든 공통 행위인 hit랑 stand만 가능하도록! 상위 클래스에서 다른 행위 메서드들은 모두 막혀있고, 막히지 않은 나머지 메서드들은 구현해줬습니다!

 

Guard

public class Guard extends UnableToAttack {

    @Override
    public CharacterState hit() {
        throw new IllegalStateException("가드중이라 피격이 안돼요! 다막음!");
    }

    @Override
    public CharacterState jump() {
        throw new IllegalStateException("가드 풀고 점프하세요~!");
    }

    @Override
    public CharacterState crouch() {
        throw new IllegalStateException("가드 풀고 웅크리세요!");
    }

    @Override
    public CharacterState guard() {
        System.out.println("계속 가드!");
        return new Guard();
    }
}

Guard는 유일하게 hit가 불가능한 상태네요. hit는 맨 위 추상 클래스인 Playing에 구현되어 있었지만, Guard에서는 맞는게 불가능 하게 설계해서 hit시 예외를 발생하도록 구현했습니다.

 


자 그럼 AbleToAttack의 하위 구체 클래스들을 볼까요? AbleToAttack은 클래스 계층 구조상 스킬을 사용할 수 없는 상태지만, 기본 공격은 사용할 수 있는 상태가 되겠네요. 그런 상태는 웅크린 상태인 Crouching, 점프중인 상태인 Jumping이 있습니다.

 

Crouching

public class Crouching extends AbleToAttack {
    @Override
    public CharacterState punch() {
        System.out.println("웅크란 상태에서 주먹공격하면 하단 주먹 공격!");
        return new Crouching();
    }

    @Override
    public CharacterState kick() {
        System.out.println("웅크란 상태에서 주먹공격하면 하단 발차기 공격!");
        return new Crouching();
    }

    @Override
    public CharacterState jump() {
        throw new IllegalStateException("웅크린 상태에선 점프 불가능!");
    }

    @Override
    public CharacterState guard() {
        throw new IllegalStateException("웅크린 상태에선 가드 불가능!");
    }

    @Override
    public CharacterState crouch() {
        System.out.println("계속 웅크리기!");
        return new Crouching();
    }
}

Crouching에서는 하단 공격을 하고 나서 웅크린 상태를 계속 유지하기 때문에 Crouching을 다시 반환하네요!

 

Jumping

public class Jumping extends AbleToAttack {
    @Override
    public CharacterState punch() {
        System.out.println("점프 손 공격!");
        return new Jumping();
    }

    @Override
    public CharacterState kick() {
        System.out.println("점프 킥 공격!");
        return new Jumping();
    }

    @Override
    public CharacterState jump() {
        throw new IllegalStateException("이미 점프중!");
    }

    @Override
    public CharacterState crouch() {
        throw new IllegalStateException("점프 상태에서 못 웅크려요 ㅠㅠ");
    }

    @Override
    public CharacterState guard() {
        throw new IllegalStateException("점프 상태에서 가드 못해용 ㅠㅠ");
    }
}

점프 상태를 나타내는 클래스도 공격을 한 이후에 같은 Jumping을 반환하고 있어요!

 


자, 위 예제 코드와 설명을 보시고 나면 처음 설명했던 '특정 상태를 나타내는 구체 클래스들을 구현하고, 그 상태에서 하는 행위마다 다음 상태를 반환하도록 메서드를 구현합니다.' 라는 말이 이해가 되실까요?😁 현재 상태에서의 그 메서드에 대한 로직을 실행한 후에 다음 상태를 반환하도록 모든 상태들이 구현되어 있어요. 그리고 이 상태를 사용할 Character라는 클래스가 필드 변수로 갖고 

 

public class Character {
    private CharacterState state;

    public void punch() {
        // 펀치 로직~
        state = state.punch();
    }
    // ...
}

요런 식으로 구현할 수 있을 것 같네요. 메서드를 호출하면 상태에서도 메서드를 호출해서 다음 상태를 반환하게 하고 그걸 자신의 상태로 바꾸는거죠.

 


자, 그래서 이렇게 하면 장점이 뭐가 있느냐...

바로 분기문이 많이 줄어듭니다! 예를 들면 상태패턴 없이 점프시엔 점프공격 해야해! 라는 로직을 구현한다면,

if(isJumping()) {
	// 점프 공격 로직
} else {
	// 일반 공격 로직
}

뭐 이런 코드가 나올텐데, 상태패턴에선 이미 Jumping이라는 상태에서 호출하는 공격 메서드는 점프공격이 되죠. 특히나 분기문이 많은 구조일 때 효율적으로 분기문을 줄일 수 있습니다.🤗

'Java' 카테고리의 다른 글

옵셔널(Optional) 클래스  (0) 2022.05.02
타입 안전 이종 컨테이너  (0) 2022.04.05
멤버 클래스는 되도록 static으로  (2) 2022.03.30
Stream.forEach() vs for-each  (0) 2022.03.14
표준 함수형 인터페이스  (0) 2022.03.08