본문 바로가기

기타/객체지향의 탄생(2013)

객체지향의 탄생-State Pattern

 

  1. 스테이트 패턴(State)

1. 객체지향의 중요한 원칙중 하나는 캡슐화이다. 캡슐화는 속성을 숨겨 다른객체가 함부로 접근하지 못하게 하는것도 있다. 객체에 주어진 기능중에 일부를 나머지 부분으로 분리하여 다른 객체 그룹으로 감싸는 것도 있다. 둘다 캡슐화라고 한다.

 

우리는 변하는 많은 요소를 캡슐화 하는 방법을 배웠다. 알고리즘, 구독 프로세스, 장식 알고리즘, 의존성, 명령, 프로세스, 반복등의 요소를 캡슐화하는 방법을 배웠다는 것은, 바람직한 객체지향 개발을 지원하고, 바람직한 객체지향 개발이 궁극적으로 추구하는 유연하고 높은 확장성에 유지보수하기 쉬운 높은 품질을 어플리케이션을 개발하는데 기여하게 될것이다.

 

처음에 나는 알고리즘을 캡슐화하는 것에 대하여, 굳이 디자인패턴이 아니더라도 나도 모르게 프로그래밍할때 구현하곤 해서 익숙하게 이해했다. 하지만 명령을 캡슐화하는 커맨드 패턴이나 반복을 캡슐화하는 이터레이터 패턴은 처음에 낯설었다. 명령같은 경우는 다소 추상적인 요소라서 낯설었고 반복같은 경우 설마 반복같은 로직도 캡슐화할수 있을까라는 낯설음이 있었다.

 

상태라는 요소도 다소 추상적인 요소라서 상태도 캡슐화할수 있다는 사실이 낯설었다. 상태를 캡슐화 해준다는 스테이트 패턴은 클래스 다이어그램은 스트라테지 패턴과 똑같고 매우 단순해서 이해하기 쉬울줄 알았는데 스트라테지 패턴과의 미묘한 차이 때문에 오히려 이해하는데 애를 많이 먹었다.

 

그래도 결국 스테이트 패턴을 알고 보니 상태도 캡슐화하면 여러모로 객체지향 특유의 장점을 누릴 수 있을것 같았다.

 

2. 신입 게임 프로그래머 산골씨는 온라인 RPG게임의 캐릭터의 상태를 변경하는 로직을 짜라는 지시를 받았다. 먼저 캐릭터의 상태를 알아야 한다. 캐릭터 상태의 종류는 크게 보통, 속도업, 파워업, 부상, 사망의 상태로 분류한다. 캐릭터의 상태는 나중에 추가 또는 수정될 가능성도 있다. 그리고 캐릭터의 메소드는 공격, 방어, 충격, 마법, 아이템 사용 등의 메소드가 있다. 이제 요구사항을 파악했으니 산골씨는 로직을 구현하면 된다.

 

산골씨는 고민하기 시작했다. 아무래도 각 메소드마다 보통, 분노, 부상등의 상태를 염두해둔 로직을 개별적으로 구현해야 될것 같았다. 그럴려면 if문을 쓰는 수 밖에 없었다. 산골씨는 신입으로서 이런 if문 노가다는 당연히 해야 한다는 책임감을 가지고 열심히 if문으로 각 상태별 로직을 구현하기 시작했다. 산골씨는 캐릭터의 상태가 나중에 추가 또는 수정될수 있다는 생각이 번뜩 들었다. 산골씨는 씨익~ 웃으며 이렇게 되뇌었다. '신입으로 삽질은 자신있지..추가 수정건 있으면 열심히 코드 수정해보자고..신입은 무조건 발로 뛰어야되.'

 

3. 산골씨는 투덜거리면서 자기 자리로 돌아왔다. 코드가 너무 지저분하다며 다시 짜오라는 지시를 받았다. 산골씨는 고민한다. 도대체 어디가 잘못된거야..신입이지만 프로그래머 특유의 자존심이 강한 산골씨는 괴로워한다. 결국 산골씨는 다시 짜오라는 지시를 내린 깐깐한 선배에게 조언을 구했다. 선배는 하나의 단서를 말해준다. '스테이트 패턴…'

 

산골씨는 스테이트 패턴 얘기를 듣고 바로 관련 자료를 찾아보았다. 스테이트 패턴을 이용하면 객체의 상태가 바뀔때 객체의 행동을 바꿀 수 있다고 한다. if문 대신에 스테이트 객체를 바꾸는 방식으로 사용하여 소스의 수정 및 확장이 쉽다고 한다. 산골씨는 스테이트 패턴이 낯설었지만 신입사원으로 회사에 무사히 안착하기 위해 끙끙 앓으며 공부하기 시작했다.

 

다음날 오전 머리가 부시시하고 피부가 푸석한 모습의 산골씨가 자신이 짠 소스를 선배와 함께 리뷰하고 있었다. 선배가 말한다. '수고했어~ 통과~' 선배의 한마디는 건조했다. 하지만 산골씨는 그 한마디에 안도의 한숨과 함께 자신감을 얻었다.

 

4. 산골씨에게 부여받은 캐릭터의 상태변경 요구사항은 다음과 같다.

 

캐릭터의 상태는 크게 보통, 파워업, 피로, 부상, 사망의 다섯가지 상태로 나눈다.

 

이 요구사항을 바탕으로 산골씨는 처음에 많은 if문을 사용하여 문제해결을 시도했다.

 

 

public class Character {

    private int statEnergy;

 

    public int getStatEnergy() {

        return statEnergy;

    }

 

    public void setStatEnergy(int statEnergy) {

        this.statEnergy = statEnergy;

    }

 

    public String getStatEnergy(String changeEnergy) {

        String inStatEnergy = "";

        

        if(changeEnergy.equals("사망")) {

            inStatEnergy = "사망일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("부상")) {

            inStatEnergy = "부상일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("피로")) {

            inStatEnergy = "피로일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("보통")) {

            inStatEnergy = "보통일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("파워업")) {

            inStatEnergy = "파워업일때 캐릭터 상세 정보";

        }

        

        return inStatEnergy;

    }

    

    /**

     * @param args

     */

    public static void main(String[] args) {

        // 캐릭터 초기화

        Character person = new Character();

        person.setStatEnergy(100); // 에너지 100으로 초기화

        

        // 캐릭터 상태변화

        int damage = 40; // 가상으로 데미지를 40으로 준다.

        int changeEnergy = person.getStatEnergy() - damage; 

        String inStatEnergy = "";

        if(changeEnergy <= 0) {

            inStatEnergy = "사망";

        } else if(changeEnergy > 0 && changeEnergy <= 20) {

            inStatEnergy = "부상";

        } else if(changeEnergy > 20 && changeEnergy <= 50) {

            inStatEnergy = "피로";

        } else if(changeEnergy > 50 && changeEnergy <= 100) {

            inStatEnergy = "보통";

        } else if(changeEnergy > 100 && changeEnergy <= 120) {

            inStatEnergy = "파워업";

        }

        

        System.out.println("person stat ["+person.getStatEnergy(inStatEnergy)+"]");

    }

} 

[if문 처리방식]

 

그러나 이방식대로 처리하면 상태의 변경이나 확장시 하나하나 소스내부를 수정하게 되어 객체지향 프로그래밍에서 가장 경계하는 최악의 소스가 된다.

 

public class Character {

    private int statEnergy;

 

    public int getStatEnergy() {

        return statEnergy;

    }

 

    public void setStatEnergy(int statEnergy) {

        this.statEnergy = statEnergy;

    }

 

    public String getStatEnergy(String changeEnergy) {

        String inStatEnergy = "";

        

        if(changeEnergy.equals("사망")) {

            inStatEnergy = "사망일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("부상")) {

            inStatEnergy = "부상일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("피로")) {

            inStatEnergy = "피로일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("보통")) {

            inStatEnergy = "보통일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("파워업")) {

            inStatEnergy = "파워업일때 캐릭터 상세 정보";

        } else if(changeEnergy.equals("무적")) { // 새로 추가된 요구사항, 무적 상태를 추가한다.

            inStatEnergy = "무적일때 캐릭터 상세 정보";

        }

        

        return inStatEnergy;

    }

    

    /**

     * @param args 

     */

    public static void main(String[] args) {

        // 캐릭터 초기화

        Character person = new Character();

        person.setStatEnergy(100); // 에너지 100으로 초기화

        

        // 캐릭터 상태변화

        int damage = 40; // 가상으로 데미지를 40으로 준다.

        int changeEnergy = person.getStatEnergy() - damage;

        String inStatEnergy = "";

        if(changeEnergy <= 0) {

            inStatEnergy = "사망";

        } else if(changeEnergy > 0 && changeEnergy <= 20) {

            inStatEnergy = "부상";

        } else if(changeEnergy > 20 && changeEnergy <= 50) {

            inStatEnergy = "피로";

        } else if(changeEnergy > 50 && changeEnergy <= 100) {

            inStatEnergy = "보통";

        } else if(changeEnergy > 100 && changeEnergy <= 120) {

            inStatEnergy = "파워업";

        } else if(changeEnergy > 120) { // 새로 추가된 요구사항, 무적 상태를 추가한다.

            inStatEnergy = "무적";

        }

        

        System.out.println("person stat ["+person.getStatEnergy(inStatEnergy)+"]");

    }

} 

 

[실습 코드 전체 UML]

 

산골씨는 스테이트 패턴이란 단서를 바탕으로 결국 문제를 이렇게 해결했다.

 

public interface CharacterStat {

    

    // 현재 상태를 리턴한다.

    public String getStatEnergyInfo();

} 

[캐릭터 상태 정보를 담은 클래스 그룹의 인터페이스를 선언한다.]

 

public class CharacterStatDeath implements CharacterStat {

 

    @Override

    public String getStatEnergyInfo() {

        return "사망일때 캐릭터 상세 정보";

    }

} 

 

public class CharacterStatInjury implements CharacterStat {

 

    @Override

    public String getStatEnergyInfo() {

        return "부상일때 캐릭터 상세 정보";

    }

} 

 

public class CharacterStatTired implements CharacterStat {

    @Override

    public String getStatEnergyInfo() {

        return "피로일때 캐릭터 상세 정보";

    }

}

 

public class CharacterStatNormal implements CharacterStat {

 

    @Override

    public String getStatEnergyInfo() {

        return "보통일때 캐릭터 상세 정보";

    }

} 

 

public class CharacterStatPowerUp implements CharacterStat {

 

    @Override

    public String getStatEnergyInfo() {

        return "파워일때 캐릭터 상세 정보";

    }

} 

 

public class CharacterStatInvincible implements CharacterStat {

 

    @Override

    public String getStatEnergyInfo() {

        return "무적일때 캐릭터 상세 정보";

    }

} 

[각각의 상세 상태 정보를 구현한 클래스를 개발한다.]

 

public class CharacterGood {

    CharacterStat stat;

    private int statEnergy;

 

    public int getStatEnergy() {

        return statEnergy;

    }

 

    public void setStatEnergy(int statEnergy) {

        this.statEnergy = statEnergy;

    }

    

    public CharacterStat getStat() {

        return stat;

    }

 

    public void setStat(CharacterStat stat) {

        this.stat = stat;

    }

    

    public String getStatEnergyInfo() {

        return stat.getStatEnergyInfo();

    }

} 

[상태 정보 그룹군 클래스들과 연동할 캐릭터 클래스를 개발한다.]

 

public class GameLauncher {

 

    /**

     * @param args

     */

    public static void main(String[] args) {

        // 캐릭터 초기화

        CharacterGood person = new CharacterGood();

        person.setStatEnergy(100); // 에너지 100으로 초기화

        

        // 캐릭터 상태변화

        int damage = 40; // 가상으로 데미지를 40으로 준다.

        int changeEnergy = person.getStatEnergy() - damage;

        CharacterStat stat = null;

        if(changeEnergy <= 0) {

            stat = new CharacterStatDeath();

        } else if(changeEnergy > 0 && changeEnergy <= 20) {

            stat = new CharacterStatInjury();

        } else if(changeEnergy > 20 && changeEnergy <= 50) {

            stat = new CharacterStatTired();

        } else if(changeEnergy > 50 && changeEnergy <= 100) {

            stat = new CharacterStatTired();

        } else if(changeEnergy > 100 && changeEnergy <= 120) {

            stat = new CharacterStatNormal();

        } else if(changeEnergy > 120) { // 새로 추가된 요구사항, 무적 상태를 추가한다.

            stat = new CharacterStatPowerUp();

        }

        

        person.setStat(stat); // 상태 클래스를 셋팅한다.

        System.out.println("person stat ["+person.getStatEnergyInfo()+"]");

    }

} 

[캐릭터 정보를 가져 오는 로직 클래스를 개발한다.]

 

if문으로 상태 로직을 처리한것과 스테이트 패턴으로 상태 로직을 별도의 객체그룹으로 캡슐화한 방식과의 차이점을 생각해보고, 스테이트 패턴으로 상태 로직을 처리했을때의 장점을 그동안 배운 객체지향 디자인원칙과 비교하여 떠올려보자.

 

5. 바뀌는 것은 캡슐화한다. 이 명제를 충실하게 따라본다. 캐릭터의 요소중 상태가 바뀐다. 상태가 바뀌므로 캡슐화를 하는 것이 유익하다. 하지만 산골씨는 직관적으로 떠오르는 if문 방식으로 문제를 해결하였다. 바뀌는 것이 고스란히 바뀌지 않는 코드에 잠복하여 있으므로 산골씨의 코드는 수정과 확장 이벤트에 제대로 대응하지 못하고 겨우 작동할 것이다.

 

스테이트 패턴은 if문에 감싸진 캐릭터의 상태 처리 로직을 모두 다른 객체 그룹군으로 캡슐화했다. 이제 산골씨의 소스는 깔끔해졌다. 각 상태의 행동을 별도의 상태 클래스로 모듈화하였다. 너저분한 작업실같고 악취나는 if문을 모두 제거했다. 상태가 변경되거나 확장되더라도 소스 내부를 번거롭게 수정할 필요가 없이 상태 클래스를 확장하면 된다. 객체지향 디자인 원칙중 OCP원칙을 충실하게 수행한다. 스테이트 패턴으로 구현된 모습을 다이어그램으로 그려보면 요구사항을 직관적으로 이해하기 쉽게 변경할수 있어, 다른 개발자가 봐도 이애하기 쉽게 구현되었다.

 

스테이트 패턴은 상태라는 요소를 캡슐화 한것이라 생각하고, 그 다이어그램이 전형적인 구성 구조라 이해하기 쉬워야 한다. 그러나 나는 스테이트 패턴을 이해하는 애를 먹었다. 이유는 스트라테지 패턴과 다이어그램이 완전히 똑같은데 두 패턴과의 차이점을 이해하고 설명하기가 어려웠기 때문이다.

 

스테이트 패턴과 스트라테지는 용도가 다르다. 스트라테지패턴은 교체 가능한 알고리즘을 캡슐화 했다가 클라이언트가 사용하고 싶은 알고리즘을 취사선택하여 인자로 넘기는식으로 쓰인다. 예를 들어 DB커넥션 풀을 사용하는데 MySQL과 연결하고 싶으면 MySQL커넥션 연결을 구현한 클래스를 취사선택하는 식으로 쓴다. 스테이트 패턴은 말 그대로 대상 객체의 상태를 변경할때 쓰이는 패턴이다. 주로 수많은 if문을 집어넣을 상태 처리 로직 대신에 사용하는 패턴이다. 예를 들어 어느 캐릭터의 생존, 부상, 죽음, 파워업등의 상태를 변경할때 쓰이는 패턴이다.

 

스테이트 패턴과 스트라테지는 구성구조로 연결된 객체를 사용하는 메소드 범위가 다르다. 간혹 다를때도 있지만 보통 스트라테지 패턴은 보통 하나의 메소드를 위임 처리 할때 쓰이곤 한다. 예를 들어 다음의 그림은 DB커넥션을 가져올때 getConnection 하나의 메소드를 가져올때만 위임처리하고 있다.

 

[스트라테지 패턴은 보통 하나의 메소드를 위임처리할 때 사용한다.]

 

스테이트 패턴은 보통 컨텍스트 객체(클라이언트 클래스)와 메소드 명세를 똑같이 구현하여 스테이트 클래스를 만들어 통채로 위임처리하곤 한다. 마치 객체의 클래스가 통채로 바뀌는 효과가 일어난다.

 

[스테이트 패턴은 컨텍스트 객체의 메소드 명세를 똑같이 구현한다.]

 

스트라테지패턴은 주로 실행시에 전략 객체를 변경할 수 있는 유연성과 확장성을 제공하기 위한 용도로 쓰인다. 상속을 이용해서 클래스의 행동을 정의하다보면 행동을 변경할때 마음대로 변경하기 힘들다.

 

스테이트패턴은 컨텍스트 객체에 수많은 조건문을 집어넣는 대신에 사용하는 패턴이다. 행동을 상태 객체 내에 캡슐화시키면 컨텍스트 내의 상태 객체를 바꾸는 것만으로도 컨텍스트 객체의 행동을 바꿀수 있다.

 

스테이트 패턴과 스트라테지는 구성구조로 연결된 객체의 생성하고 사용하는 주체가 다르다. 스트라테지 패턴은 보통 클라이언트에서 컨텍스트 객체한테 어떤 전략 객체를 사용할지를 지정한다. 그리고 그 이후에는 컨텍스트 객체가 전략을 직접 변경하는 경우는 드물다. 컨텍스트 객체는 전략 객체의 새부 클래스들을 모르기 때문이다.

 

그러나 스테이트 패턴은 컨텍스트 객체를 초기 생성할때 최초 상태를 지정해주긴 하지만 그 후로는 컨텍스트 객체가 알아서 자신의 상태를 변경한다. 오히려 클라이언트가 직접 상태 객체하고 연락을 하는 경우는 없다.

 

스테이트 패턴은, 객체의 상태가 바뀔때 객체의 행동을 별도로 캡슐화된 상태 클래스로 교체하는 패턴이다. 보통 애플리케이션을 만들때 스테이트를 쓰지 않으면 상당히 복잡한 조건문을 사용해야 한다. 하지만 별도로 캡슐화된 객체를 사용하면 상태를 명확하게 표현할수 있기 때문에 유연하고 확장하기 쉬우며 가독성도 높아 유지보수하기 편리한 어플리케이션을 개발할 수 있다.

 

[스테이트 패턴 원본]

 

덧 ) 이 객체지향의 탄생 원고는 제가 책으로 내려다가 일단 잘 안되었는데요. 이유는 비문이 많다. 단락내 주제가 중복된다. 어떤 상황 설명을 과장한다.등 입니다. 그래도 원고를 일단 블로그에 몽땅 풀어보고 언젠가 제대로 교정해서 다시 도전할 생각입니다. 이점을 감안해서 읽고 객체지향을 이해하는데 도움이 되셨으면 좋겠습니다. 의견도 주셨으면 좋겠습니다. 원고 조금만 교정하면 괜찮을것 같은 출판사 관계자분의 피드백도 환영합니다. 매주 월요일 발행 예정입니다.