'조건문'에 해당되는 글 1건

  1. 2016.08.12 상태와 행위의 결별

이 글을 통해 알리려고 하는 내용은 사실 객체지향에 국한된 얘기는 아니다. 그래서 우선 시작은 구조적 언어(C언어)를 중심으로 이야기 하려고 한다. 하지만 객체지향에서 이 부분이 더욱 강화되고 원칙화 된 것이므로 당연히 객체지향으로의 확장도 다루도록 하겠다.


속성과 행위

결과물은 객체지향 쪽으로 흘러갈테니 용어도 객체지향으로 시작하겠다. 속성이란 기존 구조적 언어에서는 변수라고 부르는 것이고, 행위란 함수라고 부르는 것이다. 변수는 데이터를 저장할 수 있는 것, 변경되는 것이며 함수는 제어, 연산을 수행하는 것, 변경할 수 없는 것으로 이해한다. 일반적으로는 맞는 이야기이지만 이 이해에 매몰되면 좋은 소프트웨어를 만들기 어려운 것이 사실이다.


속성을 통한 행위의 변경

행위(함수)는 변경될 수 없다. 이것은 사실이다. 이미 코드 상에서 행위를 정의한 이후에 이것을 런타임에 변경하는 것은 불가능하다. 하지만 상황에 따라서 다른 행위를 해야 하는 경우는 비일비재 하다. 그래서 이러한 경우에 보통 값에 대한 변경이 가능한 변수를 활용한다. 지금 상황(state)에서는 덧셈을 해야 한다면 상황을 표현하는 변수를 만들고, 이 변수 값(ADD_STATE)을 통해서 덧셈을 하도록 행위를 유도한다. 반대로 뺄셈을 해야하는 상황이라면 변수 값(SUB_STATE)을 변경해서 뺄셈을 하도록 유도한다. 이것은 속성과 행위의 특성을 보면 당연하다. 변경이 가능한 것(변수)을 이용해서 변경이 불가능한 것(행위)을 변경하는 것이다. 그래서 프로그래머들은 상황을 알려 주기 위해서 변수를 선언하고 이를 상태(state)라고 보통 부르고(flag를 사용하기도 한다), 행위의 변경은 이 상태 변수가 어떤 값을 가지는지에 따라 조건문을 작성하고 그에 종속된 행위들을 나열한다.


말로 설명하는 것보다 코드를 보는 편이 더 빠르겠다. 자 다음과 같은 코드를 한번 보자. 일단 이 문제는 구조적 언어로부터 출발해 보도록 하겠다.


상태에 따른 동작 변경

#define ADD_STATE (0)

#define SUB_STATE (1)

int calculate(int state, int a, int b){

    if(state == ADD_STATE){

        return a + b;

    }

    else if(state == SUB_STATE){

        return a - b;

    }

    return 0;

}

int main(){

    int result = 0;

    result = calculate(ADD_STATE, 5, 10);

    result = calculate(SUB_STATE, 5, 10);

}


위의 예제는 state 변수의 값에 따라서 덧셈과 뺄셈을 조건적으로 수행하는 코드이다. 위 코드를 요구사항 관점에서 기술해 보면 다음과 같다. "상태가 덧셈 상태이면 결과에 덧셈 결과를 저장하고, 상태가 뺄셈 상태이면 결과에 뺄셈을 저장하라." "상황에 따라서 다른 동작을 수행해야 한다"는 관점에서 살펴 봤을 때 위의 코드에 어떤 문제점이 있겠는가? state 변수의 값을 if문으로 살펴보는 것은 상황에 따라 다르게 동작해야 한다는 조건을 만족시키는 일이므로 전혀 이상하지 않을 만한 코드이다. 그리고 셀 수 없이 많은 프로그래머들이 이런 형태로 프로그래밍을 하고 있다. 어찌보면 상황에 따라서 다른 동작을 하라는 요구사항을 만족시키는 방법은 위의 코드 밖에 없어 보인다.


자 문제에 대한 관점을 조금씩 바꿔 나가기 위해서 다음과 같이 코드를 수정해 보도록 하겠다.


int add(int a, int b) { return a + b; }

int sub(int a, int b) { return a - b; }


먼저 위와 같이 덧셈과 뺄셈을 수행하는 코드를 별도의 함수로 독립 시킨 다음, 조건문이 있는 곳을 다음과 같이 수정한다.


     if(state == ADD_STATE) result = add(5, 10);

else if(state == SUB_STATE) result = sub(5, 10);


논리적인 결과는 첫번째 소스와 전혀 차이가 없다. 단지 덧셈 구문과 뺄셈 구문을 함수로 독립시켰을 뿐이니까.


그런 다음 한가지 생각을 떠올려 보자. state가 변수로서 하는 일은 오직 add()와 sub()함수를 바꿔서 실행 시킬 수 있도록 하는 것이다. 그렇다면 state 변수 대신 add() 함수와 sub 함수를 바꿔 넣어도 되지 않을까?


행위에 대한 직접 변경

int add(int a, int b) { return a + b; }

int sub(int a, int b) { return a - b; }

int calculate(int (*func)(int, int), int a, int b){

    return func(a, b);

}

int main(){

    int result = 0;

    result = calculate(add, 5, 10);

    result = calculate(sub, 5, 10);

}


생각하는 방식이 바뀐 것을 간단히 정리 해 보면 다음과 같다.


기존 : 상태 변수를 선언하고, 상태 변수에 상태 값을 할당하고, 상태 값을 체크하고 상태 값에 따라 덧셈과 뺄셈을 바꿔 계산한다.

변경 : 하고 싶은 행위에 따라 덧셈과 뺄셈을 하는 함수를 집어 넣는다.


간접적으로 행위를 변경하던 것을 직접 행위를 변경하는 것으로 바꾼 것이다. 이것이 일면 별 것 아닌 것처럼 보이지만 효과는 상당하다. 효과를 정리해 보면 다음과 같다.


1. 조건문이 사라졌다. 부수적으로 코드가 단순해졌다.

2. 이해하기가 쉬워졌다. 계산이 덧셈 뺄셈만이 아니라 곱셈 나눗셈 등 온갖 계산들이 나열되었다면 저 한가지 변화의 크기를 직감할 수 있을 것이다.

3. calculate 함수가 조건 체크에 실패 했을 때 return 0을 수행하던 것이 사라졌다. 이는 의도하지 않은 결과로 소프트웨어가 계속 동작하는 것을 방지해 준다.


생각보다 강력한 결과

이 방법은 상태 변수를 사용하는 모든 경우에 적용이 가능하다. 그 말은 상태 대신 행위를 직접 바꾸면 모든 상태 변수를 없앨 수 있고, 모든 조건문을 없앨 수 있다는 말이다. 이 결과로 유추해 보면 100 라인의 코드든 1000라인의 코드든 이론적으로는 조건문 없이 구현할 수 있다는 얘기다. 실제로 몇 달 전에 작성한 차트 그리기 프로그램(자바)은 5000라인(공백 제외)이었는데 조건문을 조사해 보니 60개 정도였다. 약 100라인에 if 문 혹은 switch 문이 한 개 있는 비율이다. 이것이 상태 변수를 없애고 행위를 직접 변경한 결과이다.


<바 차트, 라인 차트, 레전드 위치 변경, 레이블, 폰트 변경, 바 컬러 변경, 값에 따른 바 컬러 변경, 차트 흐름 고정, 테마 변경 등에 이르는 수많은 옵션들을 지원하도록 만들어졌지만 조건문은 단 60개 뿐이다>


상태에 종속적인 행위 변경

나는 상태를 선언하고, 상태에 대해 조건문을 작성하고 조건에 따른 행위를 수행하는 과정을 "상태 종속적 행위 변경"이라고 부른다. 지금도 많은 프로그래머들이 이러한 안티 패턴을 통해서 소프트웨어를 만들고 있다. 상태 변수를 선언하고, 상태에 따른 조건문을 작성하고 조건에 따라 다른 코드들을 집어 넣는다면 모두 "상태에 종속적인 행위 변경"이라는 안티 패턴을 사용하고 있는 것이다.


객체 지향에서의 행위 변경

객체지향 언어에는 상태 종속적 행위 변경 문제를 해결하는 디자인 패턴이 이미 정의 되어 있다. 전략 패턴과 상태 패턴이 바로 그것이다. 그리고 null 객체 패턴 + Special Case 패턴처럼 조금 덜 유명한 디자인 패턴도 상태 문제를 행위 문제로 바꿔주는 패턴들이다. 

전략 패턴에서는 A라는 객체가 B라는 객체에게 일을 시킬 때 일부 변경하고자 하는 행위가 있다면 이 행위를 객체화 하여 B에게 인자로 넘겨 준다. 이러한 방식으로 행위를 상태 변수 없이 변경한다. 맨 처음 예제가 사실 전략 패턴의 C언어 버전이라고 할 수 있다.


전략 패턴 예제

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}

class Calculator{

    public int calculate(int a, int b, IFunction function){

        return function.calculate(a, b);

    }

} 

< 맨 처음 예제를 Java로 전략 패턴 형태로 바꾸어 본 것이다. 함수 포인터 대신 객체를 넣는다는 점 만 다를 뿐이다>


상태 패턴은 상태가 필요한 객체가 각 상태를 객체화시킨다. 그리고 필요한 상태에 따라 상태 객체를 변경시킨다. 그러면 상태 객체의 메소드를 호출할 때마다 상태에 따라 행위가 바뀌게 된다.

null 객체 패턴은 인자로 넘어온 객체가 null인지 체크(null 상태인지 아닌지를 체크하는 것이다) 하는 대신 아무 동작도 하지 않는 Null 객체를 만들어 넘긴다. 그려면 객체를 받는 쪽에서는 넘어온 객체에 대해 null 인지를 체크하지 않아도 된다. 실행해도 아무 동작도 하지 않을 뿐이다.


객체지향 언어는 무 조건문/무 상태 언어이다

객체지향 언어를 사용하면 switch 문의 사용이 줄어든다는 얘기가 있다. switch - case 문에서 체크  되는 변수 대신 상위 클래스에 대한 레퍼런스를 선언하고, 각각 실행되는 문장들을 개별 객체화하여 레퍼런스에 할당해주면 switch-case문이 없어지기 때문이다.(이것이 디자인 패턴에서 말하는 상태 패턴이다) 여기서 조금 더 나가보면 이런 상태 변수들은 모두 객체 레퍼런스로 대체될 수 있고, 변수에 의해 바뀌는 행위, 즉 조건문 안의 행위들은 모두 개별 객체로 대체될 수 있다. 그러면 조건문들이 모두 사라진다.


객체지향을 이야기 할 때 다형성은 빼 놓고 이야기 할 수 있는 특징이다. 다형성은 여러 객체가 있지만 외부적 관점(즉 "타입" 관점)에서는 동일한 객체로 취급하겠다는 것을 의미한다. 동일하다는 것은 구분하지 않는다는 것이고, 구분하지 않는다는 것은 조건을 체크할 필요가 없다는 것이다. 그것은 상태에 따른 조건문 체크가 없는 것과 같다.


좀 더 나가보면 근본적으로 객체지향이 추구하는 방향은 상태를 보지 않겠다는 것이다. 오직 상대를 행위 대상으로만 바라본다. 인터페이스에는 변수가 없다. 즉 상태를 보지 않아도 된다는 의미다. 어느 객체가 동작 불가능한 상태에 있다고 가정하자. 이것을 외부에 알려줄 필요가 있을까? 객체가 동작 불가능한 상태라고 판단하면 그 객체는 실제 동작들을 객체화 해 두었다가 동작 객체를 동작 불가능한 객체로 바꾸기만 하면 된다. 그러면 외부에 자기 상태를 알려줄 필요가 없다.


객체와 객체 간에는 공유하는 변수가 없다. 상태는 변수의 부분집합이므로 상태도 공유되지 않는다는 의미이다. 인터페이스가 보여주는 것처럼 다른 객체의 상태를 몰라도 사용할 수 있다. 외부적 관점에서 상대방 객체는 상태가 없다. 그러면 조건문 없이 사용할 수 있다. 내부적으로는 상태를 객체로 대체한다. 그러면 상태 없이 구현될 수 있다. 그러면 조건문 없이 사용될 수 있다.


따라서 객체지향 언어는 무 조건문 / 무 상태 언어이다.


(구조적 언어도 함수 포인터라는 무 상태 언어적 특성을 가지고 있다. 하지만 이러한 관점이 널리 보급되지는 못했다. 아무래도 객체지향 언어에서 처럼 상태 제거를 위한 함수 포인터를 다중 중첩해서 사용하기가 어려운 문제가 있었을 것이다. 함수 포인터를 많이 사용한 코드는 사실 이해하기가 어렵다. 함수 포인터는 근본적으로 변수이고, 변수적인 타입만 체크 할 뿐 객체지향 언어의 "타입"처럼 특별한 제약이 없기 때문에 문제가 발생할 가능성이 많다.)


가장 단순한 적용 방법

우선 변수를 선언할 때 생각을 해보자. 변수가 상태를 나타낼 것이고, 조건문에 사용될 것인가? 그렇다면 그 변수는 필요 없다. 객체로 대체하라.


조건문을 만들기 시작했다. 조건문에 사용되는 변수가 하나 인가? 그러면 그 변수는 필요 없다. 객체로 대체하라.

조건문에 들어가는 변수가 여러개인가? 여러 변수를 하나의 변수로 바꿔라.


이 과정을 따르면 반복문을 제외하고는 단 한 줄의 코드도 들여쓰여지지 않은 코드가 만들어질 것이다.

Posted by 이세영2
,