이해를 돕기 위해서 예제를 사용해야 할 때가 온 것 같다.


일단 앞서서 객체의 세계와 외부 세계를 구분했다. 객체의 세계란 객체지향 언어를 통해 객체와 객체간의 관계로만 이루어진 소프트웨어 내부를 말한다. 외부 세계란 그 객체 세계와 소통하는 모든 다른 시스템들을 말한다.


이 글에서 나타내야 하는 부분이 바로 객체 세계와 외부 세계의 소통이므로 다음과 같은 예를 들어 보겠다.



Object World는 객체 세계를 말한다. 우리가 만드는 세계이다. 온도계와 DB는 외부 세계이다.

우선 온도계 값은 객체화 되어 있지 않다. 따라서 그냥 int 타입의 변수 값으로 온다고 가정한다.


DB는 온도 값도 저장하지만 온도의 상태도 저장한다고 가정한다. 즉, hot / normal / cold를 저장한다고 한다. 알기 쉽게 기준을 잡아 주자면 0도 미만은 cold, 30도 이상은 hot, 나머지는 normal이다.


그리고 우리의 객체 세계 내에서도 온도의 상태에 따라(즉 hot / normal / cold에 따라서) 여러가지 동작을 취한다고 가정한다. 객체 세계 내부에 온도 조절기(Thermostat)가 있어서 hot일 때는 cooling()이, cold일때는 warming()이 동작 해야 한다고 하자.


이런 상황에서 일반적인 프로그래밍의 방향을 이야기 해보자.


1. 일단 온도계의 온도를 받아야 할 것이다.

2. 온도에 따라서 다른 동작을 취할 수 있어야 한다.

3. DB에 저장할 때는 온도에 따라서 다른 상태 필드 값을 생성해 주어야 한다.


가장 손쉬운 방법으로 코딩을 해보자. 온도가 전달되어 들어 오면 온도조절기에 그 값을 전달해 주고 온도 조절기는 상태에 따라 온도를 조절한다. 그리고 그 값에 따라서 DB에 저장한다. 이것을 코드로 나타내기 위해 몇 개의 클래스를 정의하도록 하겠다.


1. ObjectWorld 클래스 : 시스템 전체를 담는 클래스.

2. Temperature 클래스 : 온도를 담고 있는 클래스

3. Thermostat 클래스 : 온도 조절기 클래스.

4. DataBase 클래스 : DB에 온도를 저장하는 클래스.


코드를 먼저 보기 전에 클래스 다이어그램을 보자.

ObjectWorld는 DataBase / Thermostat / Temperature 객체를 가지고 있다. ObjectWorld 객체가 온도값을 받으면 Temperature 객체의 setTemperature() 함수를 호출하여 온도를 갱신한다. 그리고 DataBase와 Thermostat에 이 객체를 전달해 주고 각자 필요한 동작을 수행하도록 한다. 이를 코드로 나타내 보면 다음과 같다.


Temperature 클래스

class Temperature{

    int temperature;

    public static final int HOT = 0;

    public static final int NORMAL = 1;

    public static final int COLD = 2;

    public void setTemperature(int temperature){

        this.temperature = temperature;

    }

    public int getState(){

        if(temperature > 30) return HOT;

        if(temperature >= 0) return NORMAL;

        else return COLD;

    }

}


온도를 저장하고, 온도의 상태를 계산하여 외부에 전달해준다. 온도 상태를 계산해서 주는 것은 매우 좋은 선택이다. 그렇지 않으면 각 객체들은 오직 temperature 값에 의존해서 상태를 계산해야 한다. 이것은 코드의 중복을 만들어 낸다.


DataBase 클래스

class DataBase{

    public void insertTemperature(Temperature temperature){

        switch(temperature.getState()){

        case Temperature.HOT :

            System.out.println("insert:HOT");

            break;

        case Temperature.NORMAL :

            System.out.println("insert:NORMAL");

            break;

        case Temperature.COLD :

            System.out.println("insert:COLD");

            break;

        }

    }

}

온도 상태에 따라서 데이터베이스에 저장한다.


Thermostat 클래스

class Thermostat{

    public void onTemperatureChanged(Temperature temperature){

        switch(temperature.getState()){

        case Temperature.HOT :

            cooling();

            break;

        case Temperature.NORMAL :

            stop();

            break;

        case Temperature.COLD :

            warming();

            break;

        }

    }

    private void cooling(){ System.out.println("Cooling"); }

    private void warming(){ System.out.println("Warming"); }

    private void stop(){ System.out.println("stop"); }

}


Temperature 객체를 받아서 온도 상태에 따라 온도를 조절한다.


ObjectWorld 클래스

public class ObjectWorld {

    Temperature temperature = new Temperature();

    Thermostat thermostat = new Thermostat();

    DataBase dataBase = new DataBase();

    public void receiveTemperatureValue(int temperatureValue){

        temperature.setTemperature(temperatureValue);

        thermostat.onTemperatureChanged(temperature);

        dataBase.insertTemperature(temperature);

    }

    public static void main(String[] args) { // 테스트 코드

        ObjectWorld world = new ObjectWorld();

        world.receiveTemperatureValue(50);

    }

}

온도 값이 새로 들어 오면 온도 객체를 업데이트 하고, DataBase 객체와 Thermostat 객체에 넣어 준다.




객체들의 책임이나 역할에 별 문제가 있어 보이진 않는다. 다만 상태를 확인하는 코드가 여럿 보인다. 이런 상태의 코드를 개선할 수 있을까?


여기서 무상태 프로그래밍을 위해서 문제가 되는 부분은 상태 확인 메소드인 Temperature.getState() 함수이다. 이 함수가 생긴 이유는 무엇일까? 애초에 상태를 없애고 싶다면 각 상태를 객체화 했어야 했다. 그리고 그러기 위해서 ObjectWorld 객체의 receiveTemperature() 함수는 값을 받기만 하지 않고 객체를 생성하는 Factory Method를 이용해야 했다.


변경의 방향은 이렇다. Temperature 객체는 상위 타입으로만 사용한다. Temperature 객체의 하위 타입으로 Hot, Normal, Cold를 나타내는 클래스를 정의한다. 온도값이 들어오면 값에 맞는 객체를 생성해낸다. 그리고 DataBase나 Thermostat에서는 상태 확인 함수를 동작시키지 않고 바로 자신의 동작을 수행한다. 이를 위해서는 Thermostat 객체와 Temperature와의 관계를 변경할 필요가 있다. 온도 스스로가 Thermostat을 동작 시키도록 말이다.


우선 클래스 다이어그램을 다시 그려 보자. 



우선 각 온도 타입의 상위 타입인 ITemperature가 생겼다. 그리고 온도 값에 따라서 개별 Temperature 객체를 리턴해 줄 TemperatureHolder를 만들었다. 마지막으로 ITemperature가 Thermostat에게 의존하는 형태로 변경이 되었다.


클래스 다이어그램만으로는 원하는 내용을 알 수 없으므로 코드를 보도록 하겠다.


ITemperature 인터페이스

interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

}

인터페이스에는 각 상태별 객체가 해야 할 일들이 정의되어 있다. 즉, DB에 대해서는 자신의 상태를 DB에서 저장할 수 있는 String 타입을 리턴할 수 있도록 해준다. Thermostat에 대해서는 각 상태별 객체가 수행해야 할 일을 직접 하도록 한다.


ITemperature 인터페이스를 구현한 상태별 클래스

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

}

각 상태별 클래스는 오직 자기 상태에 해야할 일들만 수행한다. DB를 만났을 때, Thermostat을 입력 받았을 때 해야 할 일들이 정의되어 있다.


TemperatureHolder 클래스

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;

    }

}


이 클래스가 무상태 프로그래밍을 위한 핵심 클래스라고 할 수 있다. 우선 이 클래스는 유일하게 조건문이 들어가는 클래스이다. 이 클래스가 상태에 따라서 동작하는 유일한 클래스가 된다. 그리고 상태에 따라서 동작을 수행하는 것이 아니라 객체를 전달한다. 즉, 각 상태를 객체화한 Hot/Normal/ColdTemperature 객체를 전달해 줌으로써 상태별 동작을 객체별 동작으로 변경한다. 이후 상태별 객체를 사용하는 객체들은 실제 어떤 상태 객체가 있는지 알지도 못한다. 오직 ITemperature 인터페이스만 알 수 있을 뿐이다. 이 객체는 상태 값을 상태 객체로 바꾸고 그 상태 객체의 타입에 대한 정보까지도 감춰 버리기 때문에 이후 프로그래밍에서 "상태에 따른", "상태별" 동작은 전혀 발생하지 않는다.


일반적으로 상태의 변경이 일어났을 경우에는 그 상태에 맞는 클래스를 생성하는 방식으로 대응한다. 따라서 보통은 TemperatureHolder가 아니라 TemperatureFactory가 사용된다. 하지만 이 예제에서는 굳이 객체 생성을 중복해서 할 필요가 없었기 때문에 홀더 형태로 구현하여 부하를 줄였다.


DataBase 클래스와 Thermostat 클래스

class DataBase{

    public void insertTemperature(ITemperature temperature){

        System.out.println("insert:" + temperature.toDbString());

    }

}

class Thermostat{

    public void cooling(){ System.out.println("Cooling"); }

    public void warming(){ System.out.println("Warming"); }

    public void stop(){ System.out.println("Stop"); }

} 


가장 드라마틱한 변화를 가져온 것이 이 부분일 것이다. 상태를 체크하던 코드들이 모두 사라졌다. 너무 단순해져서 두 클래스를 묶어 보여줘도 괜찮을 정도다. DataBase에서 실행되는 코드는 실제로 단 한줄 밖에 없다. Thermostat의 변경은 조금 설명이 필요하다. 이전에는 Temperature 객체를 받아 동작했었다. 하지만 이제는 ITemperature 인터페이스에 매개변수로 입력되게 된다. ITemperature 인터페이스를 상속하는 상태별 온도 클래스들이 이 Thermostat 객체를 받게 되는데, 이 때 이미 각 상태별 객체들이 어떤 동작을 해야 할지는 정해져 있기 때문에 조건문이 전혀 필요하지 않다. DataBase 클래스나 Thermostat 클래스나 모두 자기가 할 일만 구현하면 되도록 변경되었다.


ObjectWorld 클래스

public class ObjectWorld {

    ITemperature temperature;

    Thermostat thermostat = new Thermostat();

    DataBase dataBase = new DataBase();

    public void receiveTemperatureValue(int temperatureValue){

        temperature = TemperatureHolder.getTemperature(temperatureValue);

        temperature.operateThermostat(thermostat);

        dataBase.insertTemperature(temperature);

    }

    public static void main(String[] args) {

        ObjectWorld world = new ObjectWorld();

        world.receiveTemperatureValue(50);

    }

}


대표적으로 수정된 부분은 ITemperature 객체를 얻어오기 위해서 TemperatureHolder 클래스를 이용하는 부분이다. TemperatureHolder 클래스로부터 상태별 객체를 받아오는 과정을 통해 이후에 동작하는 모든 코드에서 상태 체크 부분이 사라지게 된다.


또 한가지 부분은 기존에 Thermostat 객체에 Temperature 객체를 넣어 동작을 수행하던 것을 이제는 반대로 ITemperature 인터페이스 객체에 Thermostat 객체를 넣는 형태로 바뀌었다는 점이다. 이렇게 해야 상태를 체크하지 않고 Thermostat이 동작할 수 있다.


종합적인 변화

자 변화를 모두 종합해 보면 다음과 같다.


상태 체크를 위한 조건문, 제어문이 단 한 곳에서만 사용하도록 변경되었다. TemperatureHolder 객체 이외에 모든 객체는 무상태가 되었다. 더 이상 어느 객체가 다른 객체의 상태를 체크하지 않아도 된다. 따라서 상태에 따른 동작이 더 이상 확장되지도 않는다.


상태가 추가되거나 삭제되는 변화에 의해 수정되는 곳이 단 한 곳으로 줄어 들었다. 만일 새로운 상태(very hot / very cold)가 추가되었다면, 이전의 코드에서는 DataBase, Thermostat도 동시에 변경 되어야 했을 것이다. 하지만 switch - case 문의 단점은 변경사항이 발생한 경우 어디에 switch - case 문이 있는지를 알 수 있는 방법이 없다는 것이다. 이것은 잠재적인 버그의 원인이 된다. 새로 변경된 구조에서는 새로운 상태가 추가 되더라도 다른 코드에 영향을 전혀 미치지 않는다. TemperatureHolder 클래스를 제외한 다른 코드들에서는 상태 객체의 종류 조차 알지 못하기 때문이다.


이후 온도에 기반한 동작을 구현하기가 용이해졌다. 만약 UI가 추가되어 온도를 숫자로 나타내야 한다면 ITempterature 인터페이스에 온도를 숫자로 리턴하도록만 구현해 주면 된다. 만일 상태별 객체로 생성되지 않았었다면 상태를 체크하는 코드가 또 추가되었을 것이다.


가독성이 향상되었다. 조건문과 제어문은 코드의 길이를 길게 만들기도 할 뿐더러 쉽게 읽기 어렵게 만든다. 혹시 온도 뿐 아니라 습도와 같이 유사하고 같이 쓰일 법한 상태 변수가 또 들어온다면 코드의 지저분함은 이루 말할 수 없을 정도로 커지게 될 것이다.


결론

객체의 세계와 외부와의 소통은 단일한 곳에서 이루어져야 한다. 일반적으로 이 소통은 FactoryMethod 패턴, 또는 Holder 패턴에 의해 이루어지게 될 것이다. 그리고 그 소통이 끝난 이후에 외부의 상태는 객체화 되어야 하고 일단 객체화 된 이후에는 어떤 다른 객체들도 그 상태에 대해서는 알 필요가 없어야 한다. 이런 방식으로 프로그래밍을 하면 상태 전파 문제, 조건문 생성 문제가 모두 해결된다.


(외부와 소통하는 객체를 제외하고) 모든 영역에서 조건문과 제어문을 볼 수 없는 코드를 상상해보기 바란다. 우리가 얼마나 많은 시간 동안 if문, switch-case 문의 조건을 계산하는데 시간을 소비하고 있는지를 생각해 본다면 이 프로그래밍 방식의 이점을 충분히 이해할 수 있을 것이다.

Posted by 이세영2
,