Holder 패턴

5.디자인패턴 2016. 8. 28. 22:03

Holder 패턴이 생소할지 모르니 좀 더 익숙한 FactoryMethod 패턴을 먼저 이야기 해보겠다.


FactoryMethod 패턴은 객체의 생성을 책임지는 패턴이다. 특히 상위 타입을 가지고 있는 여러 하위 객체들 중에서 외부에서 입력한 조건에 맞게 생성해 주는 역할을 한다. 그리고 생성된 객체를 전달해 줄 때는 상위 타입으로 캐스팅해서 리턴한다. 따라서 FactoryMethod 패턴을 구현한 클래스 외부에서는 객체의 구체적인 타입을 알 수 없다. 이것은 구체적인 타입에 의존하면서 발생하는 강한 결합과 코드의 중복 등 객체지향에서 지양하는 여러 문제들이 발생하지 않도록 방지해 준다.


Holder 패턴은 FactoryMethod 패턴과 유사점이 많다. 우선 객체의 생성을 담당한다. Holder 패턴을 구현한 클래스는 클래스 내부에서 각 하위 객체를 생성하여 가지고 있다. 그리고 외부에서 특정 조건에 맞는 객체를 요구할 경우 그에 해당하는 객체를 제공해 준다. 이 경우에도 FactoryMethod와 마찬가지로 하위 객체의 타입을 알리지 않기 위해서 상위 타입으로 캐스팅 해서 내보낸다. 기본적인 동작과 목적에 있어서 Factory Method 패턴과 Holder 패턴은 매우 유사하다.


다른 점이 딱 한가지 있다. Factory Method 패턴은 외부에서 객체를 요구하면 매번 생성해서 전달해 주는데 비해 Holder 패턴은 하위 객체 각각에 대하여 딱 한번씩만 생성한다는 점이다. 이 점이 FactoryMethod와 Holder 패턴을 구분하는 가장 큰 차이점이다.


그러면 이제 Holder 패턴을 구현해 보도록 하겠다. 우리는 이 예제에서 온도 상태를 나타내는 객체를 생성하고자 한다. 온도 상태 객체는 ITemperature라는 인터페이스 형태의 상위 타입을 가진다. 그리고 하위 객체들은 구체적으로 Hot / Normal / Cold 라는 상태를 표현하는 객체이다. Holder 객체는 외부에서 받은 온도 값이 30도 이상이면 Hot 객체를, 0도 미만이면 Cold 객체를, 나머지의 경우에는 Normal 객체를 외부에 제공하게 될 것이다.


우선 상위 타입을 나타내는 ITemperature 인터페이스를 정의하도록 한다.



interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

}


이 ITemperature 인터페이스는 하위 객체의 구체적인 타입을 감추도록 만들어 준다. 인터페이스에서 구현하고 있는 메소드들이 크게 중요하진 않지만 이해를 돕기 위해서 설명을 하자면 다음과 같다. 우선 toDbString() 메소드는 이 온도 상태 객체가 데이터베이스를 만났을 때 자신의 상태에 대한 문자열을 생성하여 제공해 주는 메소드이다. operateThermostat() 메소드는 각 온도 상태에 따라 온도조절기(Thermostat)를 동작 시킨다. 상태 객체별로 다른 동작을 수행해야 하므로 온도조절기 객체를 인자로 받도록 되어 있다.


다음은 Hot/Normal/Cold를 나타내는 구체 객체를 구현한 코드이다.


class HotTemperature implements ITemperature{

    String dbString = "HOT";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

}

class NormalTemperature implements ITemperature{

    String dbString = "NORMAL";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){thermostat.stop();}

}

class ColdTemperature implements ITemperature{

    String dbString = "COLD";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

}


Hot/Normal/Cold 상태를 표현하는 3개의 클래스가 정의되었다. 셋은 모두 ITemperature 인터페이스을 구현하고 있고, 모두 각자의 상태에서 해야할 인터페이스들을 구현하였다.


Holder 패턴을 구현하는 TemperatureHolder 클래스는 아래와 같이 정의 될 수 있다.


class TemperatureHolder{

    public static final ITemperature HOT = new HotTemperature();

    public static final ITemperature NORMAL = new NormalTemperature();

    public static final ITemperature COLD = new ColdTemperature();

    public static ITemperature getTemperature(int temperature){

        if(temperature > 30) return HOT;

        if(temperature >= 0) return NORMAL;

        else return COLD;

    }

}


우선 객체의 위쪽에 ITemperature 타입으로 Hot/Normal/Cold 총 3개의 객체를 선언하고 있음을 알 수 있다. 이 객체들은 단 한 개 씩만 생성될 것이므로 static final로 선언된다.


그리고 getTemperature() 라는 메소드가 있다. 이 메소드는 ITemperature 타입을 리턴하도록 되어 있고 온도 값(temperature)을 매개 변수로 받도록 되어 있다. Holder 객체는 외부에서 받은 온도 값이 30도 이상이면 Hot 객체를, 0도 미만이면 Cold 객체를, 나머지의 경우에는 Normal 객체를 외부에 제공하도록 구현되어 있다.



이해하기 쉽게 클래스 다이어그램으로 그려보면 다음과 같다.





이대로 사용하는 것도 괜찮지만 TemperatureHolder 클래스가 좀 더 수정에 닫혀 있도록 구현하는 방법이 있다.  TemperatureHolder 클래스는 코드의 다음과 같은 변경에 취약한 문제점을 가지고 있다.


1. ITemperature 하위 클래스가 늘어나면 TemperatureHolder 클래스가 생성해야 하는 클래스의 개수도 늘어난다.

2. 이와 함께 getTemperature() 함수도 변경된다. VERY_HOT이나 VERY_COLD가 추가될 경우를 생각해 보자. 이들은 단지 ITemperature 인터페이스의 하위 클래스일 뿐인데 이 클래스가 늘어난다고 해서 생성 및 전달의 책임만 가지고 있는 TemperatureHolder 클래스가 변경된다는 것은 문제가 있다.


이 두 가지 문제를 해결해 보도록 하자.


이 중에서 2번이 더 쉬운 문제니까 2번을 먼저 해결해보자. 알아 두어야 할 것은 두가지 문제를 모두 해결해야만 완벽한 해결이 된다. 따라서 2번만 해결하는 코드는 중간 산출물 정도로 이해해 주기 바란다.


getTemperature() 메소드 문제의 원인은 하위 객체들이 생성되는 조건을 TemperatureHolder가 알고 있다는 점이다. 하위 객체들은 특정 상태의 온도에 대한 정보를 가지고 있으므로 당연히 자기 온도 범위도 알고 있어야 한다. 이것을 객체가 가지고 있지 않기 때문에 TemperatureHolder 클래스가 가지고 있게 되고, 그것이 ITemperature 하위 객체가 변경될 때 TemperatureHolder 클래스도 변경되는 원인이다.


그래서 이를 해결하기 위해 ITemperature에 각 하위 객체의 온도 범위를 체크할 수 있는 인터페이스를 구현한다.


interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

    public boolean isInTemperatureRange(int temperature);

}



그리고 각 클래스들은 자신의 범위를 알려주도록 구현한다.


class HotTemperature implements ITemperature{

    String dbString = "HOT";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

    public boolean isInTemperatureRange(int temperature) { return temperature > 30; }

}

class NormalTemperature implements ITemperature{

    String dbString = "NORMAL";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){thermostat.stop();}

    public boolean isInTemperatureRange(int temperature) { return temperature <= 30 && temperature >= 0; }

}

class ColdTemperature implements ITemperature{

    String dbString = "COLD";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

    public boolean isInTemperatureRange(int temperature) { return temperature < 0; }

} 


무상태 프로그래밍을 지향하는 관점에서 상태 객체 생성시에 상태 객체를 생성하는 클래스 이외에 다른 클래스가boolean을 리턴하는 메소드를 구현하는 것은 상태 확인 메소드를 구현하는 것과 같지 않느냐고 질문할지 모르겠다. 변명이 통할지는 모르겠지만 일단 목적과 방법에 있어서 다른 점이 있어서 이야기를 하고 넘어가겠다.


1. 위의 isInTemperatureRange() 메소드는 자기 객체의 상태를 확인하는 것이 아니다. 오히려 인자로 들어온 temperature 값이 자기 기준에 맞는지를 확인해 주는 것이다.

2. isInTemperatureRange() 메소드가 어느 시점에 호출되든지 각 객체의 내부 상태에 대해 알 수 있는 정보는 하나도 없다.

3. 이 메소드는 TemperatureHolder 클래스에 의해 이용되는 것이다. 즉, 오직 생성 클래스에 의해서만 사용된다.

4. 이 메소드는 객체의 특성을 나타내주는 메소드이다. 따라서 객체 스스로 그 특성 정보를 가지고 있는 것이 타당하다.


일단 이렇게 수정한 후에는 TemperatureHolder 클래스를 수정해야 한다.


class TemperatureHolder{

    private static ITemperature HOT = new HotTemperature();

    private static ITemperature NORMAL = new NormalTemperature();

    private static ITemperature COLD = new ColdTemperature();

    private static List<ITemperaturetemperatures = new ArrayList<ITemperature>();

   

    static{

        temperatures.add(HOT);

        temperatures.add(NORMAL);

        temperatures.add(COLD);

    }

   

    public static ITemperature getTemperature(int temperature){

        for(ITemperature each : temperatures){

            if(each.isInTemperatureRange(temperature)) return each;

        }

   

        return null;

    }

} 


코드를 더 간결하게 만들어 보고자 약간의 무리수를 두었다. HOT, NORMAL, COLD가 모두 같은 타입인데 따로 떨어져서 사용되는 것을 방지하기 위해 List를 만든 것이다. TemperatureHolder가 생성되지 않고 사용될 수 있게 하기 위해서는 이 과정을 생성자가 아닌 static {} 구문 안에 넣어 주어야 한다. 그래야 실제 사용되기 전에 리스트에 객체들이 들어간 상태가 된다. 어쨌든 이 코드는 중간 결과물이라서 결국에는 모두 제거될 것이다.


자 이제 getTemperature() 메소드를 보자. 이전에는 if문들이 나열되어 있었는데 이제 단순히 각 객체에게 온도 범위를 물어보는 형태로 변경되었다. 적어도 이 상태라면 새로운 온도 상태 객체가 추가되더라도 getTemperature() 메소드가 변경되는 일은 없을 것이다.





이제 온도 상태 객체들의 생성 문제로 넘어가보자. TemperatureHolder 클래스는 Holder로서의 역할 때문에 각 하위 객체를 생성하여 가지고 있어야 한다. 이 점 때문에 새로운 온도 객체가 생성되면 TemperatureHolder 클래스도 변경이 되어야 한다. 이것을 자동화 할 수 있는 방법이 없을까?


1. 자 몇가지 특성을 먼저 이야기 해보겠다. 각 온도 상태 객체들은 타입이 ITemperature이다. 그리고 생성 이후 변경되지 않고 계속 유지되기 때문에 "static으로 선언"된다. 즉 소프트웨어가 실행되기 전에 모두 생성이 완료 된다.

2. 모두 ITemperature를 구현하고 있으므로 모두 "같은 상위 타입"을 가진다.

3. 온도 상태 객체는 "여러개" 이다.


위의 1~3에서 ""로 묶인 특성들을 잘 살펴보자. 어떤 프로그래밍 요소가 떠오르지 않는가? 바로 enum 타입이다. enum 타입은 enum 상위 클래스와 하위 클래스의 집합이다. 또한 static으로 선언되고, 여러 하위 클래스가 나열된다. 따라서 저 특성에 매우 적합한 형태이다. 자 enum을 가지고 각 상태 객체를 구현해 보자.


enum Temperature implements ITemperature{

    HOT{

        public String toDbString(){ return "HOT";}

        public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

        public boolean isInTemperatureRange(int temperature) { return temperature > 30; }

    },

    NORMAL{

        public String toDbString(){ return "NORMAL";}

        public void operateThermostat(Thermostat thermostat){thermostat.stop();}

        public boolean isInTemperatureRange(int temperature) { 

            return temperature <= 30 && temperature >= 0; 

        }

    },

    COLD{

        public String toDbString(){ return "COLD";}

        public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

        public boolean isInTemperatureRange(int temperature) { return temperature < 0; }

    }

    ;

}


우선 이 enum 클래스는 ITemperature 인터페이스를 구현하고 있다. 따라서 외부에서는 자연스럽게 ITemperature로 사용이 가능하다. 또 enum 타입이므로 중복 생성되는 경우가 발생하지 않는다. 그리고 각 하위 클래스는 자신의 특성에 맞게 인터페이스를 구현하고 있다. 따라서 외부에서 사용할 때에는 이전에 개별적으로 구현되었던 방식과 전혀 다를 바 없이 사용이 가능하다.


그러면 우리가 목적했던 TemperatureHolder 클래스는 어떻게 바뀌는지 보자. 당연히 ITemperature 하위 객체를 생성하는 부분은 수정이 되었겠지만 우리가 선언한 enum 기반의 객체들이 어떻게 사용되는지 확인이 필요할 것이다.


class TemperatureHolder{

    public static ITemperature getTemperature(int temperature){

        for(ITemperature each : Temperature.values()){

            if(each.isInTemperatureRange(temperature)) return each;

        }

        return null;

    }

}


뭔가 구현을 덜 해놓은 느낌까지 준다. 하지만 있을 것은 다 있다. 이미 enum을 통해 하위 객체들의 생성은 완료된 상태이다.(enum은 static 이므로 별도의 생성이 필요하지 않다.) 그리고 이전 중간 단계에서 static {}으로 List를 만드는 부분도 없어졌다. 


왜 인지는 getTemperature() 메소드를 보면 알 수 있다. 이전에 리스트를 통해 for문을 돌던 것이 Temperature.values()를 통해 for문을 도는 것으로 변경되었다. enum의 values() 메소드는 enum 클래스 하위 객체들을 배열로 만들어 제공하는 메소드이다. 따라서 for문에 사용될 수 있다. 그리고 이 점이 특히 enum을 사용할 때 좋은 점인데, 만약 새로운 하위 객체가 생성되었다 하더라도 values() 메소드는 새로 추가된 하위 객체를 포함한 배열을 리턴해 준다. 따라서 TemperatureHolder 객체와 같이 enum.values() 메소드를 사용하는 곳에서는 코드를 수정할 필요가 없어진다.


이로써 TemperatureHolder 클래스가 가지고 있던 설계적인 문제들이 모두 해결 되었다.


'5.디자인패턴' 카테고리의 다른 글

Singleton 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Adapter 패턴  (0) 2016.08.23
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
Pluggable Selector 패턴  (1) 2016.08.21
Posted by 이세영2
,