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
,

앞서 Object World 예제에서 나왔던 TemperatureHolder에 관한 내용이다.


우선 소스를 먼저 보고 이야기를 시작하자.


interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

}


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(); }

}


class TemperatureHolder{

    public static ITemperature HOT = new HotTemperature();

    public static ITemperature NORMAL = new NormalTemperature();

    public static ITemperature COLD = new ColdTemperature();

    public static ITemperature getTemperature(int temperature){

        if(temperature > 30) return HOT;

        if(temperature >= 0) return NORMAL;

        else return COLD;

    }

}


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

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


FactoryMethod 패턴은 객체의 생성을 책임지는 패턴이다. 위와 같이 상위 타입(클래스 다이어그램과 소스 상에서는 ITemperature)이 같은 여러 하위 객체들의 생성을 담당하는 패턴이다. 그리고 매우 중요한 부분이 있는데 이 FactoryMethod 패턴을 통해 생성된 하위 객체의 종류나 그 존재까지도 외부에서는 알지 못하게 해야 한다는 점이다. FactoryMethod 패턴이 구현된 클래스에서 생성된 객체가 외부로 나가는 순간, 외부에서는 그 객체의 구체 클래스가 무엇인지는 알지 못하고 오직 ITemperature로만 알려지게 된다. 이것은 코드의 중복과 강한 결합 등 객체지향에서 지양하는 여러 문제들이 발생하지 않도록 방지해 준다.


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


다른 점이 딱 한가지 있다. Factory Method 패턴은 외부에서 객체를 요구하면 매번 생성해서 전달해 주는데 비해 Holder 패턴은 하위 객체 각각에 대하여 딱 한번씩만 생성한다는 점이다. TemperatureHolder 클래스에서 보면 HOT/NORMAL/COLD 라는 필드명으로 된 3가지 객체를 하나씩 가지고 있다.


이 글에서는 저 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<ITemperature> temperatures = 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 클래스가 가지고 있던 설계적인 문제들이 모두 해결 되었다.

Posted by 이세영2
,

객체지향으로 만들어진 객체는 능동적인가? 수동적인가?


어떤 책에서는 객체지향을 매우 능동적으로 서술하고 있다. 마치 객체들이 살아서 움직이면서 유기적으로 행동하는 것으로 묘사한다. 어떤 특정한 상태에서는 그것이 맞는 말이긴 하다. 하지만 내가 보는 객체지향의 세계는 상당히 수동적이다.


객체지향을 능동적으로 보는 관점에서 항상 시작은 "어느 객체가 무슨 할 일이 있을 경우"이다. 즉, 할 일이 있을 때 자기가 스스로 해야 할지, 남에게 시켜야 할지(위임)를 결정한다. 만약 해야 할 일이 크다면 위임될 가능성도 크다. 따라서 참조하는 객체에게 자신이 해야 할 일 일부 또는 전부를 시킨다. 따라서 해야 하는 일의 관점과 그 일을 하는 객체의 관점에서 보면 객체는 능동적인 것이 맞다.


하지만 객체지향 전체 세계를 놓고 보면 객체지향의 세계는 매우 수동적이다. 그 이유는 어떤 "이벤트"에 의해 객체가 동작하기 때문이다. 우리는 이 "이벤트"를 주로 메시지, 공개 함수, 공개 멤버 함수, 메소드, 인터페이스, API 등 다양한 이름으로 부른다. 절차지향 또는 구조적 언어에서 객체지향으로 넘어오면서 수동적인 것에서 능동적인 것으로 변화했다고 묘사하는 경우가 종종 있지만 결국 누군가가 시키지 않으면 일하지 않는 것이 기본이다. 맴버 함수 호출이 없는 경우 객체는 아무 일도 하지 않는다. 이것이 기본이다. 이벤트 없이 객체는 움직이지 않는다.


그러면 그 이벤트의 시작은 어디일까? 몇가지로 구분해보자.


1. 인터럽트

OS 레벨에서의 이벤트를 말한다. 통신, UI 조작(키보드나 마우스 이벤트), 타이머 등이 이에 해당된다. 이들은 여러 루트를 통해 이벤트를 객체에게 전달한다. 이러한 이벤트가 객체로 흘러 들어오는 순간 객체는 동작하게 된다. 그리고 이런 이벤트들은 아까 이야기 했던 공개 맴버 함수를 호출하는 형태로 이루어진다. 결국 객체는 시스템의 관점에서 보면 수동적인 것이다.


2. 쓰레드

쓰레드는 유일하게 객체가 능동적으로 보일 수 있는 부분이다. 쓰레드가 하는 일을 크게 나눠보면 두가지가 있는데, 하나는 이벤트를 기다리는 것, 하나는 주기적인 동작이다. 이벤트를 기다리는 것은 1번의 인터럽트와 크게 다를 바가 없다. 주기적인 동작은 일련의 동작들을 반복적으로 수행한다는 개념이다. 이것 정도가 그나마 능동적이라고 할 수 있겠다.


이 이야기를 꺼낸 이유는 간단하다. 무상태 프로그래밍을 수행하기 위해서 상태를 발생시키는 원인이 어디에 있는가를 알아보기 위해서다. 1번의 인터럽트의 경우 당연히 외부적인 요인이므로 상태를 발생시킨다. 그리고 우리가 다루었던 것처럼 외부적인 상태는 FactoryMethod 패턴, Holder 패턴 등을 이용하여 상태를 객체화 시킴으로써 해결할 수 있다. 


남은 것은 쓰레드인데 쓰레드가 어떤 상태를 계속 만들어 낸다고 해서 그것이 객체 세계 전체에 영향을 미치게 할 필요가 있을까? 이미 외부적인 상태에 대해 해결하는 방법을 알려 주었다. 그 방법을 쓰레드가 생성하는 상태에 적용하지 못할 이유가 있을까?


당연히 그렇게 하지 못할 이유는 전혀 없다. 어떻게 상태가 생성되었든 간에 그 상태가 발생한 객체는 여러 방법들을 이용해서 상태를 객체화 시킬 수 있다. 그러고 나면 모든 일은 객체와 객체간의 관계 문제로 바뀐다. 그리고 이 문제를 해결하는 해결책 역시 나와 있다. 상태 패턴, 전략 패턴 등이 바로 그것이다.


결국 객체 세계 내부에서는 (쓰레드를 제외하고는) 상태가 발생하지 않아야 한다.

Posted by 이세영2
,