앞서 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
,