상태를 전달하는 것은 프로그래밍에서 매우 조심해야 할 행위이다. 이미 만들어진 상태 조차도 상태를 객체화 하는 과정을 거쳐 상태를 보지 않아도 동작이 가능한 코드를 만들려고 노력하는데 상태를 전달한다는 것은 더더욱 피해야 할 일이다.


상태를 매개변수로 다른 객체에 전달하는 것은 다른 객체에게도 상태에 따른 동작을 강요하는 행위이다. 이것이 반복되다 보면 모든 코드들이 상태에 기반하여 동작하게 될 것이다. 이곳 저곳에서 조건문이나 제어문이 남발되고, 코드는 점점 이해할 수 없을 수준의 복잡성을 가지게 될 것이다.


개인적으로는 인정하기 어렵지만, 만약 객체의 동작 중에 새로운 상태가 발생했다면, 이것은 역시 객체화를 통해 해결할 수 있다. 이미 3. 무상태 프로그래밍 : 객체의 세계 외부와의 소통에서 보여준 것처럼 상태 값을 상태별 객체로 만들고, 이것을 상태 값 대신 넘겨주는 것이다.


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

    }

}


밑줄 친 부분은 데이터베이스 쪽에 상태별 객체를 넣어주는 코드이다. 수정 이전의 코드에서는 상태별 객체 대신 온도 객체가 그대로 들어갔었다. 상태별 객체가 들어가고 나면 이미 데이터베이스에서 필요한 정보, 즉 현재 상태를 나타내는 문자열 정보는 상태별 객체가 가지고 있으므로 기능을 수행하는데 아무런 지장이 없다. 상태 변수가 발생했다고 해서 굳이 상태 변수를 다른 객체에 까지 전파시킬 이유가 없다.


상태 전파

만약 다른 사람이 만든 코드의 인터페이스가 갖가지 상태 값을 받게 되어 있고, 혹시라도 그 사람과 사이가 좋지 않다면 이것은 그 사람을 야근에 빠뜨릴 좋은 기회가 될 것이다. 만약 그 사이 좋지 않은 사람이 바로 자신이라면 야근의 늪에 빠지지 않기 위해서 인터페이스를 변경하는 편이 좋다.


개인적으로 느끼기에 상태 전파는 객체지향에서 나쁜 코드로 인식하는 거의 모든 요소를 모두 가지고 있다. 잦은 조건문 사용을 유발하여 코드의 가독성을 떨어 뜨린다. 매우 간결하고 쉬웠어야 할 코드를 조건 체크로 인하여 복잡하게 만든다. 멀티 쓰레드 환경에서라면 상태 값이 모든 객체에 동일하다고 보장할 수 없으므로 안정성에도 영향을 미친다. 상태 전파는 상태 전파를 낳게 되고, 이로 인하여 중복 코드들이 나타나게 된다. 전파된 상태를 수정하는 일은 너무나 힘든 일이다. 


나는 외부와의 소통을 담당하는 코드가 아닌데도 조건문들이 코드에 등장하는 것은 다형성의 위배나 객체지향에 대한 몰이해의 산물이라고 보는 편이다. 만약 조건문 중첩이 일어났다면 거의 확실하다. 그리고 이러한 문제가 발생하는 원인 중에서 가장 큰 것은 바로 상태 전파 때문이다. 이것을 피하고 싶다면 상태를 상태별 객체로 변경해야 한다.

Posted by 이세영2
,

무상태 프로그래밍을 위해서 객체가 갖추어야 할 것은 두가지이다.


1. 비밀을 유지하라

2. 독립적이 되어라


비밀을 유지하라

간단하게 말하면 상태 확인 메소드를 제공하지 않아야 한다. getter 함수를 제공하지 말라는 얘기가 아니다. 물론 getter 함수도 최대한 자제해야 하는 것이긴 하지만 그보다 더 큰 문제를 발생시키는 것은 상태를 알려주는 메소드이다. 상태를 알려주면 상대 객체는 그 객체 상태에 종속적인 행위를 하게 된다. 이것을 막으려면 상대가 상태를 몰라도 자신에게 책임을 맡길 수 있도록 디자인 해야 한다.


가장 먼저 생각해야 하는 부분은 변수를 선언하는 것이다. 변수를 선언하는 이유를 곰곰히 생각해 봐야 한다. 그 변수가 꼭 필요한 것인지, 꼭 필요하다면 단순히 값을 저장하려는 목적인지, 아니면 상태를 표시하려는 목적인지 생각해 봐야 한다. 만약 조금이라도 값이 상태 처럼 사용된다면, 즉 변수의 값에 따라 동작을 바꿔야 하고, 그러기 위해서 조건문을 써야 한다면 바로 객체로 변환할 방법을 생각해 봐야 한다.


두번째는 그 변수값을 어떻게 사용할 것인지이다. 만약 getter를 제공해서 외부로 내보내야 한다면 외부 객체들은 그 변수를 어떤 용도로 사용하게 될지를 생각해야 한다. 만약 getter를 제공해서 외부에 있는 객체들이 자신의 상황을 알게 해야 하는 것이라면 객체의 설계에 대해서 다시 생각해 봐야 한다.


독립적이 되어라

상대의 상태를 보고 행위하려고 하지 말아야 한다. 뭔가를 해야 한다면 바로 해야 한다. 상대 객체의 상태를 보려고 하다보면 조건문이 생기고 코드는 복잡해진다.




둘 중 어느 것이 중요하냐고 물어본다면 당연히 비밀을 유지하는 쪽이다. 아무것도 제공하지 않는데 사용할 수는 없다. 중요한 부분은 비밀을 유지하면서도 기능을 구현할 수 있는 방법은 무엇인가 이다.


이미 한가지 예는 알려 주었다. 아래 코드는 온도에 관한 코드이다.


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;

    }

}


이 코드는 자기 상태를 외부에 자꾸 알려 주도록 되어 있었다.(getState() 함수) 이로 인해 다른 객체들은 이 객체의 상태를 확인하지 않고서는 동작할 수 없었다. 하지만 이 Temperature 객체를 상태별로 분리한 후 전체 소스에서 이 객체의 상태를 확인하지 않고도 동작이 가능하게 변경되었다. 이것을 디자인 패턴에서 상태 패턴이라고 부른다.

문제점을 해결한 아래의 소스는 전형적인 상태 패턴이다.


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

}


이 상태별 객체는 이용할 수도 있고 거꾸로 상태별 객체에 의해 다른 객체가 이용될 수도 있다. 상태별 객체를 이용하는 경우는 상태별 객체에 있는 메소드가 제공하는 정보를 이용하는 것만으로도 목표가 달성되는 경우이다. 전형적으로 toDbString() 객체가 그러하다. DataBase 클래스에서는 이 객체의 메소드 중에서 toDbString() 함수만 호출해서 그 결과를 얻으면 끝이다. 이 결과값을 가지고 다시 상태를 확인하려고 하지 않기 때문에 그것 만으로 충분하다.


약간 변경이 필요했던 부분은 Thermostat 부분이다. Thermostat은 상태를 값으로 이용하려 하기 보다 자신의 행위를 변경하려고 했었다. 자신의 행위 변경을 위해서 다른 객체의 상태가 필요하다는 것은 다른 객체의 상태에 종속적이라는 뜻이다. 따라서 다른 객체의 상태를 가지고 자기가 무언가를 하기 보다는 종속적인 방식, 즉 Thermostat 객체가 상태별 객체에 매개 변수로 넘겨지는 편이 더 자연스럽다. 따라서 operateThermostat() 메소드에 의해 Thermostat 객체의 행위가 호출되도록 하면 코드가 더 간결해진다.


무상태 프로그래밍에서는 다른 객체의 상태에 관심이 없다. 단지 자기가 할 일에 집중하면 된다. 만약 다른 객체의 상태를 꼭 확인해야 하는 경우라면 다른 객체에 종속적이라는 의미이다. 따라서 하고자 하는 일을 직접 하지 말고, 상대 객체에 메소드를 정의하고 매개 변수로 객체를 넘겨야 한다.

Posted by 이세영2
,

null을 리턴한다는 것은 리턴 타입이 객체라는 의미이다. null은 객체가 아니다. null과 객체는 같지 않으므로 객체로 취급하면 에러가 발생한다. 그렇기 때문에 null은 "예외적인 처리"를 해 주어야 한다. 결국 null을 리턴할 수 있다는 것은 받는 쪽에서는 null인지 아닌지를 체크해야 한다는 것이다. 이것은 전형적인 상태의 모습이다.


자 취급하기 좋은 예를 들어 보자. 우리는 종종 List에서 특정 조건에 맞는 객체를 꺼내오고 싶을 때가 있다. 그래서 다음과 같이 프로그래밍을 한다.


List<Person> persons = new ArrayList<Person>();

public List<Person> getByName(String name){

    if(persons.size() == 0) return null;

    List<Person> result = new ArrayList<Person>();

    for(Person each : persons){

        if(each.getName().equals(name)) result.add(each);

    }

    return result;

}


여기서 눈여겨 볼 부분은 if(persons.size() == 0) return null 이다. 이 코드는 원하는 객체가 없을 경우 List 객체를 만들지 않고 바로 리턴하는 코드이다. 어떤 사람들은 최대한 연산량을 줄이고 List 객체를 새로 생성하는 것을 방지한다는 면에서 좋다고 생각할지도 모른다. 하지만 이 코드는 코드 자체로도 그다지 큰 효과를 내지 않을 뿐더러 null을 리턴함으로써 메소드를 사용하는 입장에서 null을 한번 더 체크하게 만든다는 문제가 있다. 만약 새로운 객체를 생성하지 않는 것이 그렇게 중요하다면 다음과 같이 하면 된다.


if(persons.size() == 0) return Collections.emptyList();


이미 Java에서는 위와 같이 빈 List를 가지고 있다. 저렇게 리턴하면 메소드를 사용하는 입장에서는 null 체크를 따로 하지 않아도 된다.


Posted by 이세영2
,