Strategy 패턴

5.디자인패턴 2016. 8. 15. 17:51

참고

- 상태와 행위의 결별


Strategy 패턴은 작업을 수행하는 대상 객체에 (변수 대신) 다른 객체를 인자로 넣어 줌으로써 대상 객체의 행위를 변경하는 패턴이다.


해결하고자 하는 문제

- 객체의 동작을 동적으로 변경하고자 한다.
- 객체의 동작이 다양하고 확장될 가능성이 있다.
- 상태 변수에 의해 조건문 중첩이 너무 많이 발생한다.

문제 코드

public static final int ADD_STATE = 0;

public static final int SUB_STATE = 1;

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

    if(state == ADD_STATE){

        return 5 + 10;

    }

    else if(state == SUB_STATE){

        return 5 - 10;

    }

    return 0;

}

public static void main(String[] args) {

    int result = 0;

    result = calculate(ADD_STATE, 5, 10);

    result = calculate(SUB_STATE, 5, 10);

}

위의 코드는 state 변수의 값에 따라서 간접적으로 calculate() 함수의 동작을 제어하도록 되어 있다. 상태 변수를 통해 행위를 변경시키는 코드는 좋은 코드가 아니다. 불필요한 상태 변수가 선언되고, if 문이나 switch 문과 같은 불필요한 제어문이 생성되기 때문이다. 가장 좋은 방법은 변경시키고자 하는 행위를 직접 넘겨주는 것이다.

우선 아래와 같이 변경하고자 하는 행위를 객체로 선언한다. 인자로 넘길 때 변경이 가능한 형태여야 하므로 동일한 인터페이스를 상속 받은 Add 클래스와 Sub 클래스를 선언해 준다.

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;

    }

}


이렇게 선언된 클래스를 calculate() 함수가 인자로 받을 수 있도록 한다. 이 때 state 변수는 이제 필요 없으므로 제거한다.

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

    return function.calculate(a, b);

}


public static void main(String[] args) {

    int result = 0;

    result = calculate(new Add(), 5, 10);

    result = calculate(new Sub(), 5, 10);

}


최종 결과

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;

    }

}

public class Strategy {

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

        return function.calculate(a, b);

    }

    public static void main(String[] args) {

        int result = 0;

        result = calculate(new Add(), 5, 10);

        result = calculate(new Sub(), 5, 10);

    }

}


상태를 나타내는 state 변수 대신에 행위를 구현한 객체를 직접 넣어 줌으로써 코드가 더 간결해지게 된다.

Strategy 클래스의 calculate() 함수는 IFunction 타입의 객체를 인자로 받도록 되어 있다. main() 함수에서는 calculate() 함수를 통해서 실행하고자 하는 행위에 따라서 Add 클래스 혹은 Sub 클래스를 바꿔 넣어주면 행위가 변경된다.  



Posted by 이세영2
,

*이 글을 보기 전에

- "모든 악의 근원 : 불완전성의 원리"




어떤 사람들은 완벽하려고 하고, 어떤 사람들은 위험을 먼저 본다. 그리고 어떤 사람들은 위험보다 기회가 먼저 보인다.


불완전성의 원리를 증명한 쿠르트 괴델은 완벽해지려고 노력했던 사람인 듯 하다. 그리고 당대의 많은 수학자들은 수학의 완전성이 무너지는 것을 보고 그 분야를 연구하기를 외면했다.


하지만 지금 우리가 컴퓨터를 최초로 만든 사람들로 알고 있는 일련의 사람들은 그곳에서 기회를 찾았다. 이들이 불완전성을 무시했던 것은 아니다. 이미 불완전성은 다양한 형태로 소프트웨어 분야에서 명제화 되어 있다. 하지만 괴델이 증명에 사용했던 알고리즘의 시초에서 어떤 이들은 컴퓨터와 소프트웨어라는 자동 계산 기계에 대한 기회를 발견했다.


당시는 한창 2차 세계 대전이 진행 중이었고, 전쟁에서 이기기 위해 암호 해독, 무기 개발 등 다양한 연구를 수행하고 있었다. 연구를 위해서는 수학, 물리학 등 다양한 분야에서 당대 최고의 학자들을 모아 놓을 필요가 있었는데, 이들 중에 현대 컴퓨터 구조의 아버지라 불리는 폰 노이만이 있다.


그는 당대의 모든 학자들 중에서도 단연 최고의 천재라 불릴 만큼 대단한 사람이었다. 그에 대한 수많은 일화가 있고, 그것이 사실인지는 확인이 필요하지만 소프트웨어라는 것을 어떻게 생각했는지를 알 수 있는 일화가 하나 있다. 


컴퓨터는 만들어졌고, 컴퓨터를 구동시킬 알고리즘을 구현해야 했던 상황에서 당시에 활용할 수 있는 것은 0과 1로만 이루어진 기계어 뿐이었다. 폰 노이만의 제자들 중에서 기계어를 가지고 알고리즘을 구현하는 것이 무척 어려움을 느끼고 그보다 더 고급 언어(아마 어셈블리어 쯤 되지 않을까 한다)를 만들려고 하고 있었다. 고급 언어를 만들기 위해서는 언어를 정의하고, 정의된 형식대로 프로그램을 작성하고, 작성된 프로그램을 컴퓨터에 입력하고, 컴퓨터에 입력된 프로그램을 다시 기계어로 바꾸는 작업이 필요하다. 이러한 일련의 과정에는 컴파일러라는 프로그램이 필요하고 컴파일러는 컴퓨터의 컴퓨팅 능력이 없으면 동작하지 않는다.


요컨데 인간이 이해하기 쉬운 컴퓨터 언어를 만들어 사용하려면 컴퓨터의 연산 능력을 활용해야 한다는 의미이다. 폰 노이만이 이에 대해 한 말은 '완벽한 신의 언어가 있는데, 그걸 놔두고 저런 조잡한 걸 만들려고 하느냐?(위키)'였다.


개인적으로 저 일화가 진짜인지 확인하기는 어렵지만 일화가 알려주는 몇가지 중요한 문제들이 있어 짚어보고자 한다.


코드의 명료성

그렇다. 기계어는 인간이 이해하기 너무 어렵다. 얼마나 어렵냐면, 기계어로는 너무나 어려워서 기계어로 컴파일러를 만들고 조금 쉬운 언어를 정의해서(언어가 '조금' 더 쉬운 것이지 언어를 정의하는 일이나 새로 정의된 언어가 쉬운건 아니다) 그 언어를 컴퓨터에 입력할 수 있는 기계어 프로그램을 만들 생각을 하고 실천해야 했을 만큼 어렵다. 당시의 소프트웨어는 현대의 소프트웨어만큼 비 결정적이고 변덕스럽지 않다.(이 말의 의미는 잘 알고 있을 것이다. 현대의 소프트웨어게 사람들은 미래의 기대도 만족 시킬 수 있을 정도의 유연성을 요구한다. 미래의 기대는 아직 나타나지도 않았는데 말이다.) 폰 노이만과 같은 천재가 아니라면 기계가 바로 동작 시킬 수 있는 기계어로 현대의 소프트웨어를 만든다는 것은 불가능한 일이다.


이 일화에서 끄집어 내고 싶었던 부분은 범용 컴퓨터가 만들어진 시절부터 이미 코드의 이해 측면, 즉 인간이 이해할 수 있는 언어를 만들겠다는 생각이 존재했다는 점이다. 이것은 중요한 관점인데, 현대에도 잘 짜여진 코드의 중요한 덕목이 명료성이기 때문이다. 명료성이란 한마디로 말하자면 인간이 이해하기 편해야 한다는 특성이다. 어떻게 만들든지 기계어는 명료하지 않다. 현대의 언어로 만들어진 코드라고 해도 명료성을 추구하지 않으면 코드의 품질은 이루 말할 수 없이 저하된다. 하물며 기계어를 계속 다루었던 사람들이라면 (폰 노이만을 제외하고) 보다 고급 언어를 정의하여 사용하고 싶다는 욕구가 지금보다 더 강했을 것을 것이라 생각해 볼 수 있다.


현대의 언어를 접하면서 우리가 염두해 두어야 할 것은 언어의 문법을 이해하는 것보다 언어의 관점을 이해하는 것이 더 중요하다는 점이다. 문법은 우리에게 가능성을 열어준다. 문법이 정의되어 있지 않으면 우리는 어떤 생각을 코드로 표현할 수 없다. C언어에는 객체지향 문법이 없다. 따라서 C언어로 객체지향을 표현하기가 어렵다. 따라서 문법을 잘 알지 못하면 표현하고 싶은 것이 있어도 표현하지 못한다. 따라서 문법을 공부하는 것은 당연히 중요하다. 문제는 많은 이들이 문법을 공부하고 깊이 이해하면 이해할 수록 문법이 주는 자유에 빠져들어 간다는 것이다. 어떤 이들은 문법이 "허용된 자유의 범위"라고 생각한다. 마치 문법을 문자 그대로 "법"이라고 생각하는 것 같다. 그래서 법으로 정해진 범위 내에서 모든 것이 가능하다고 생각한다. 이러한 생각을 표현한 말이 "돌아가기만 하면 된다"는 말이다. 문법이 "법"으로써 강제하는 정도는 그리 심하지 않다. 개수를 나타내는 변수 이름을 count라고 쓰지 않는다고 해서 컴파일러가 문법 오류를 발생시키지 않는다. 그래서 어떤 사람들은 이것을 "자유"라고 칭하고 변수 이름을 a라고 짓는다. 이것은 인간의 인지 능력에 대한 테러에 가깝다.


문법을 "자유"라고 생각하는 것은 관점을 무시한 생각이다. 관점이란 언어를 디자인한 목적을 말한다. 현대의 많은 언어들이 객체지향 언어이다. 객체지향 언어는 문법도 상당히 어렵다. [상속에 대한 올바른 이해]에서도 이야기 했듯이 객체지향 언어를 처음 접한 이들은 문제 해결과 직접 연관이 없는 문법들을 공부하느라 머리가 아파진다. 하지만 그러한 문법이 왜 생겼는지에 대한 생각, 즉 언어가 드러내고자 하는 관점을 이해하지 않으면 문법도 이해하기 힘들다. 문법이 정해져 있다는 것은 일반 "법"과 마찬가지로 제약이 정해진다는 것이다. 제약이 정해졌다는 것은 "법"과 마찬가지로 해악을 바로잡겠다는 의지가 담긴 것이다. 인간의 의지가 담겨 있을 정도로 언어적 제약이 중요했기 때문이다.


그러면 문법에 어떤 의지가 담겨 있는 것일까? 딱 한가지 의지, 즉 관점만 이야기 해보자면 바로 "아무렇게나 한다고 모두 코드가 아니다"라는 관점이다. 어셈블리어를 정의한 이유는 기계어가 가졌던 완벽히 기계어적인 자유를 박탈해야 했기 때문이다. 0과 1로만 이루어진 코드가 가진 자유는 인간에게 허용되기에는 너무나 큰 자유다. 정확히 어떤 자유냐면 "인간이 이해하기 힘들 만큼 어려운 자유, 인간이 다루기에는 너무 이해하기 힘들 만큼의 자유"다. 불완전성의 원리에서 이야기 했듯이 불완전성을 다룰 수 있는 유일한 도구는 바로 인간의 두뇌다. 따라서 인간의 두뇌가 가진 인지 능력을 자꾸 벗어나려고 하는 코드는 제약을 가해 바로 잡을 필요가 있다. 어셈블리어의 관점이란 그것을 표현한 것이다.


그러면 C언어의 관점은 무엇일까? C언어는 구조적 언어의 관점을 포함하고 있다. 소프트웨어가 점점 커지고 복잡해질수록 거대한 소프트웨어를 부분으로 나누어 개발해야 한다는 생각이 자리 잡기 시작했다. 구조적 언어를 달리 표현하는 말이 "절차지향 언어"라는 말인데, 이 말은 구조적으로 분리된 부분들을 순서에 맞춰 수행시키겠다는 의미이다. 이것은 이전 언어인 어셈블리어가 가지지 못한 관점이다. 어셈블리어는 이해 측면에서는 기계어보다 나았을지 몰라도 역시 비 구조적 언어의 특성을 함께 가지고 있었다. 따라서 전체 소프트웨어가 부분 부분으로 분리되어 있지 않았고, 전체 소프트웨어의 절차, 즉 어떤 순서로 실행되는 것인지 이해하기가 어려웠다. 이러한 문제점을 보고 해결하려고 했던 노력이 담긴 것이 C언어이다. C언어는 문제를 함수라는 부분으로 나누고, 부분으로 나뉜 함수들을 어떤 순서로 실행할지를 정할 수 있다. 왜 이렇게 만들었을까? 언어가 발전해 온 이유, 즉 무엇인가를 왜 만들었을까에 대한 답은 항상 한가지이다. 인간이 이해하는데는 그렇게 만드는 것이 더 낫기 때문이다.


자 이제 말하고 싶었던 내용을 정리해 보면 이렇다. 소프트웨어를 만드는 프로그래밍 언어는 점점 더 인간이 이해하기 좋은 형태로 발전되어 왔다. 그 이유는 인간이 이해하지 않으면 소프트웨어를 관리하는 것이 불가능해지기 때문이다. 그래서 다시 언어의 관점을 정의해 보면 다음과 같다. 언어의 관점은 인간의 인지 능력을 보다 효율적으로 사용할 수 있도록 해야 한다는 것이다. 이것이 어셈블리어 같은 자연어 언어를 만들어 내고, C언어와 같은 구조적 언어를 만들어 내고, 객체지향 언어를 만들어 냈다. 함수형 언어나 스크립트 언어는 뭔가 특별한가? 아니다. 그것도 그들 나름대로 인간의 인지 능력에 최적화하려고 노력한 언어들이다. 어떤 부분에서는 인간이 신경쓰지 않아도 될 일들을 적절한 수준에서 감추었고, 어떤 부분에서는 기존의 언어들이 주지 못했던 유연성을 보충해 주었다. 이렇게 함으로써 인간이 해야 할 일과 인간이 하지 않아도 될 일들을 구분해 주었다. 인간이 해야 하는 일은 소프트웨어가 불완전성에 의해 오동작 하는 것을 방지하는 일이다. 인간이 하지 않아도 되는 일은 무의미한 반복, 비즈니스 로직과 상관 없는 코드의 갑작스런 출현 등이다.


비 구조적 언어

그렇다면 왜 처음부터 인간이 이해하기에 좋은 언어를 만들어 내지 못했을까?


폰 노이만은 괴델이 불완전성의 원리를 발표했던 장소에 있었다고 한다. 그리고는 "모든게 다 끝장 났다"고 말했다는 일화도 있다. 이 일화는 폰 노이만이 불완전성의 원리를 이해하고 있었다는 의미이다. 하지만 컴퓨터를 만들 생각을 한 걸 보면 그는 역시 기회를 더 많이 본 것이 아닐까 생각한다.


하지만 안타깝게도 그가 설계한 컴퓨터가 이해한 언어, 즉 기계어는 그 이후에 출현한 어셈블리어와 마찬가지로 비 구조적인 언어이다. 비 구조적 언어라는 것은 "구조가 없는 소프트웨어를 만들어 내는 언어", 즉 소프트웨어가 일정한 부분들로 분리될 수 없고 소프트웨어 전체가 하나의 단위로 만들어지는 언어라는 말이다. 현대에 소프트웨어를 공부하는 사람들이 이해하기 쉽게 설명하자면, 최근의 소프트웨어는 적어도 전체 소프트웨어를 다수의 변수와 다수의 함수로 구분시킬 수 있다. 객체지향 언어라면 객체 단위로도 구분지을 수 있다. 비 구조적 언어를 현대 언어로 개발하는 방식에 비유해 보자면 몇만 라인짜리 소프트웨어를 단 하나의 함수에 구현한 것이라고 볼 수 있다.(지금도 이렇게 하는 사람이 많지만...... 이렇게 만든 것이 쓸모 있다면 그를 폰 노이만으로 부르겠다) 지금은 몇 만 라인 코드가 흔하지만 당시에 언어는 기계어나 어셈블리어이다. 만만치 않은 일이었을 것이다. 그것도 함수 하나에다 구현하는 것은 더더욱 그렇다.


이렇게 된 데에는 두가지 원인이 있다. 


하나는 최초 괴델의 증명에 사용되었던 알고리즘의 시초는 구조적인 형태가 아니었다. 그것에 영감을 받아 만들어진 것이 폰 노이만의 컴퓨터이다. 그래서 소프트웨어를 구조적으로 만들어야겠다는 생각을 먼저 하지는 못했을 것이다. 최 우선으로 생각해야 할 것은 괴델이 만든 알고리즘이 동작 가능하도록 하는 것이었다. 그런 상황에서 소프트웨어의 머나먼 미래를 내다 보고 구조적 언어로 기계어를 설계한다는 것은 불가능했다.


두번째 원인은 폰 노이만의 천재성 때문이다. 앞서의 일화가 사실이라면 그에게는 기계어를 다루는 것이 현대의 고급 언어들을 다루는 일보다 쉬운 일이었을 것이다. 그런 그가 알고리즘을 구조적으로 구현할 필요성을 느끼지 못했을 것은 자명하다. 기계어로도 그런 일을 충분히 할 수 있는데 굳이 컴퓨터의 연산 능력을 사용해 가면서 컴파일이라는 비 생산적인 일을 해야 할 이유를 못 느꼈을 것이다. 어쩌면 컴퓨터의 구조는 단순한 것이 좋고, 인간이 이해하고 작성하기 편한 소프트웨어를 만드는 것은 컴퓨터의 연산 능력을 이용하는 편이 좋다고 생각했을지도 모른다. 하지만 아무래도 폰 노이만에게는 그런 생각은 없었을 것 같다.


사실 위의 두가지 원인은 결과를 놓고 유추해 본 것에 불과하다. 폰 노이만은 천재이면서도 인간에 대한 이해가 높은 사람이었다. 컴퓨터가 유용해지고 더 많은 사람들이 컴퓨터를 접하는 시기가 오면 소프트웨어도 보통의 인간들이 다룰 수 있는 수준이 되어야 할 것이라는 생각을 못하지는 않았을 것 같다. 그래서 다른 하나의 원인을 생각해 보자면 인간이 이해하기 쉬운 언어를 만들어 내는 일보다는 다른 일이 더 중요했고 시간이 부족했지 않을까 생각한다. 천재라고 해서 모든 일을 다 할 수는 없는 것이니 말이다.


소프트웨어의 예견된 위기

컴퓨터는 폰 노이만과 같은 천재들에 의해 만들어졌다. 그리고 암호 해독이나 무기 개발 등 다양한 분야에 필요한 계산 기능을 수행함으로써 그 유용성이 증명되었다. 그리고 컴퓨터가 계산한 결과로 만들어진 무기들이 정확하게 작동하는 것을 확인하면서 이제 사람들은 컴퓨터의 연산 능력을 계속 향상시켜서 더 많은 계산을 수행하게 만들어야겠다는 생각을 하게 된다. 폰 노이만 이외에도 수많은 천재 수학자, 물리학자들이 같은 일을 하고 있었지만, 그들 같은 천재는 세상에 그렇게 많지 않다. 


폰 노이만은 당시의 컴퓨터나 계산기들보다 더 빠른 계산을 할 수 있었다고 한다. 하지만 계산을 항상 천재들에게 의존하는 것은 효율적이지 못하다. 우선 인간은, 특히 그런 천재들은 돈만 준다고 해서 일하는 사람들이 아니다. 그들 나름대로의 지적 탐구 방향과 주어진 일이 맞아 떨어져야만 가능한 일이다. 그들도 의지를 가진 인간이기 때문이다. 하지만 컴퓨터에게는 그런류의 불확실성이 없다. 컴퓨터를 이용해서 더 많은 계산을 시키겠다는 것은 합리적인 생각이다. 비록 당대의 천재보다는 못했다 하더라도 기술을 계속 발전시키면 그렇게 되지 못할 것도 없었고, 실제로도 그렇게 되었다.


이처럼 컴퓨터의 유용성에 눈 뜬 과정은 상당히 짧았으나, 그와 함께 탄생했던 소프트웨어 기술의 중요성에 눈을 뜨기 시작한 것은 그보다 한참 후였다. 그 원인을 좀 생각해 보면 다음과 같다.


우선 초기 컴퓨터는 성능이 떨어졌기 때문에 지금처럼 거대한 소프트웨어를 만들 필요가 없었다. 컴퓨터를 이용하는 목적은 일반적인 사람들보다 더 빠른 공학적, 수학적 계산이다. 목적이 명확한 상태에서 성능이 부족한 컴퓨터에 작은 소프트웨어를 개발하는 것이다. 아마도 많은 소프트웨어가 단 한 사람의 개발자의 손에서 개발될 수 있었을 것이다. 이러한 환경에서는 굳이 컴파일러는 만들고 프로그래밍 언어에 대한 이론을 만들 필요가 없었다. 


두번째 이유는 초기 컴퓨터를 개발하고 소프트웨어를 개발하던 사람들이 당대 최고의 지성인들이었다는 점이다. 초창기 소프트웨어 개발자들의 전공은 다른 분야였겠지만 많은 사람들이 수학자나 물리학자, 공학자들이었고, 수학과 같은 논리적인 사고에 익숙했던 사람들이었다. 게다가 당시는 전시이다. 그들 중에서도 뛰어난 사람들만 끌어 모을 수 있었을 만큼 자본은 충분했다. 이런 이들이 모여 만든 소프트웨어다. 장담하건데 당시에는 소프트웨어에 작은 버그 하나만 나와도 수많은 천재들 앞에서 조롱을 당했을 것 같다. 이런 분위기라면 대부분의 소프트웨어가 완벽한 상태였을 것이고, 그런 상황에서 소프트웨어 기술을 개발한다는 것은 생각하기 힘들다.


하지만 시간이 지날수록, 즉 컴퓨터의 성능은 점차 좋아지고, 상업적인 이용 가치가 부각되면서 컴퓨터와 소프트웨어를 연구하는 사람들은 점점 더 많아지고, 이와 더불어 경쟁이 치열해 지면서 시간적 여유는 부족해지게 되었다. 특히 상업적으로 발전하게 된 상황을 보면 소프트웨어가 위기에 빠지게 된 이유를 이해할 수 있을 것이다.


최초의 컴퓨터가 군사적 목적이었다는 점은 자원이 풍부했다는 것을 의미한다. 소프트웨어에서 자원이 의미하는 것은 곧바로 인적 자원을 말한다. 즉 논리적 사고에 특화된 고급 인력들이 소프트웨어를 만들어 왔다. 하지만 컴퓨터와 소프트웨어 개발이 상업화 되면서 금전적 이득을 추구하게 되면서 초창기 우수한 인력들에 비해 낮은 비용으로 채용된 사람들이 그들의 자리에 들어 앉게 되었다. 이것은 그들과 현대의 수많은 소프트웨어 개발자들을 비하하고자 해서 하는 말이 아니다. 전체적으로 보면 소프트웨어 개발자 집단은 예전이나 지금이나 상당한 수준의 창의성과 지성을 갖춘 인재들이다. 하지만 상업적인 목적을 위해서 어느 정도 규모 이상의 인력을 확충하다 보면 아무래도 군사적 목적으로 끌어들인 최고의 인력들과 같은 수준의 비용을 지불하기 어렵고, 그러면 그렇게 뛰어난 사람을 모두 잡기는 어려웠을 것이다. 이 상황에서 나름대로 선발 기준을 잘 정립한다 해도 개중에는 질이 떨어지는 사람들이 끼어 들어오는 것을 막기가 힘들다. 그리고 그때나 지금이나 마찬가지로 소프트웨어는 개개인의 생산성을 측정하기가 상당히 어려운 분야이다. 대다수의 사람들이 창의성과 지적인 능력을 가지고 성실하게 일을 했다고 해도 문제의 대다수는 소수 인원의 무지에 의해서 발생한다. 그리고 이것이 상업적 목적, 즉 이윤 추구와 만나면서 개발 기간이 자주 축소되고, 요구사항이 자주 변경되며, 개개인의 사정에 의해 개발 인력이 자주 바뀌는 상황과 맞물리게 되면 아무리 뛰어난 인력이라고 해도 고전하는 것은 당연하다.


이렇게 고전할만한 요소들이 갖춰지자 이전에는 간과되었던 부분들이 중요한 부분으로 떠오르게 되었다. 소프트웨어도 하드웨어 개발에 준하는 개발 프로세스가 필요하다는 것, 소위 말하는 '통짜' 프로그램을 개발하는 것이 아니라 구조적인 개발, 즉 설계가 필요하다는 것, 그리고 설계의 의도를 반영할 수 있고, 이전의 비 구조적 언어의 관행을 돌아가지 않게 해 줄 단단한 문법을 갖춘 언어가 필요하다는 것이다.

Posted by 이세영2
,

enum의 활용법

4.JAVA 2016. 8. 13. 18:34

C언어에서 enum은 단순히 상수형 변수 역할에 지나지 않았다. 하지만 Java에서는 매우 다른 특성들을 지니고 있다. 이 특성들 중에는 특별한 것들도 있어서 기존과는 다른 여러 방식으로 enum을 활용할 수 있다.


먼저 enum의 실제 타입부터 알아보자.


enum의 실제 타입

enum Type{ // abstract class

    ADD,    // public static final class ADD extends Type{}

    SUB;    // public static final class SUB extends Type{}

}


이처럼 기본적으로 enum은 추상 클래스이다. 그리고 그 하위에 선언된 각 열거형 변수는 실제로는 변수가 아니고 enum의 타입을 상속 받은 하위 클래스이다. 이 하위 클래스는 외부에 노출되어 있고 생성할 필요가 없으며 런타임에 교체가 불가능하므로 public static final 타입을 갖는다.




enum은 기본적으로 추상 클래스이기는 하나 다른 클래스로부터 상속을 받지는 못한다. 하지만 interface는 상속을 받을 수 있다. 따라서 다음과 같은 형태로 구현이 가능하다.


enum에 인터페이스 상속 받기

interface Interface{

    public void api();

}

enum Type implements Interface{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

}


이 인터페이스 상속 방법은 생각보다 강력하다. 이 특성을 이용해서 디자인 패턴에 나오는 수많은 패턴들을 enum을 통해 구현할 수 있다. enum이 public static final 이라는 점은 Singleton과 유사한 특성을 지니고 있다. 따라서 중복 생성이 안되면서도 일반 클래스 기능을 할 수도 있고, 필요한 때에는 enum의 특성을 활용할 수도 있다. 이 강력한 특성 때문에 Singleton 패턴, Abstract Factory 패턴, Factory Method 패턴, State 패턴, Strategy 패턴, Visitor 등 다양한 패턴의 enum 버전이 있다. 이들에 대해서는 디자인 패턴 항목에서 다룰 예정이다.




추상 클래스라면 함수를 선언할 수도 있어야 한다. 아래와 같이 함수 선언이 가능하다.


enum에서 함수 선언하기

enum Type{

    ADD,

    SUB;

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

}




추상 클래스이기 때문에 추상 메소드의 선언도 가능하다. interface를 상속 받을 수 있다는 점에서도 이 점은 유추해 낼 수 있다.


enum에서 추상 메소드 선언하기

enum Type{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

    abstract public void api();

}




같은 맥락에서 static 메소드 역시 가능하다. 실용적인 예제를 위해서 하위 타입의 개수를 알아내는 함수로 해보자.


enum에서 static 메소드 선언하기(하위 타입의 개수 알아내기)

public static int size(){ return values().length; }


보통은 Type.values().length를 통해 꺼내오기도 하지만 가독성 면에서 Type.size()를 호출하는 것이 더 나아 보인다.




클래스라면 생성자도 선언할 수 있어야 할 것이다. 생성자 선언은 아래와 같다.


enum에서 생성자 선언하기(enum 클래스를 인덱스로 활용하기)

enum Type{

    ADD(0),// 생성자에게 필요한 인자는 () 안에 넣는다.

    SUB(1); //

    int value;

    private Type(int value){ this.value = value; }   // 이것이 생성자.

    public int value(){ return value; }

}


역시 활용성을 위해서 enum 클래스를 인덱스로 활용할 수 있도록 value 라는 int 형 값을 생성시에 인자로 받도록 했다. Java의 enum은 클래스이기 때문에 C언어에서 처럼 값으로 활용할 수가 없다. 대신에 이처럼 생성자를 통해 인자로 받은 값을 가지고 있다가 값이 필요한 경우 value() 함수를 호출함으로써 값으로도 사용이 가능하다. 이 활용법은 "Effective Java" 라는 책에 수록된 내용이다. 생성자가 private인 것에 주목해야 한다. enum은 외부에서 생성이 불가능하기 때문에 생성자는 항상 private으로 선언해 주어야 한다.




다음은 문자열로 enum을 알아내는 함수이다. enum이 클래스라는 것은 이미 이야기 하였다. 따라서 하위 타입들도 모두 toString() 함수를 가지고 있는데, 그 결과 값은 기본적으로 자신의 선언된 이름과 같다. ADD.toString()은 "ADD" 값이 결과값이다. 이러한 특성은 매우 유용한데, 특히 데이터베이스에 저장할 때 그렇다. DB의 가독성 측면에서 문자열을 활용한 경우에 문자열을 입력받아 타입을 리턴하도록 하면 코드 상에서 문자열 대신 enum을 활용할 수 있으므로 코딩이 편리해진다.


enum 에서 문자열로 enum의 타입을 알아내기

public static Type getTypeByString(String str){

    for(Type each : values()){

        if(each.toString().equals(str)) return each;

    }

    return null;

}



enum 타입이 제공하는 기본 함수로 enum의 순서를 알 수 있는 함수가 있다.


public int ordinal();


위 ordinal() 이라는 함수인데 이 함수는 선언된 enum의 하위 타입이 몇 번째 순서로 선언되었는지를 알 수 있다.(순서의 시작 값은 0이다.) 가령 위에서 선언한 ADD 타입의 경우 0이 리턴되고, SUB의 경우 1이 리턴된다. 이 특성을 이용하면 enum을 인덱스로도 활용이 가능하다.



Java에서의 enum은 열거형의 특성과 클래스의 특성을 함께 가지고 있다는 장점이 있다. toString() 함수를 가지고 있다는 것만으로도 디버깅을 얼마나 쉽게 만들어 주는지 모른다. 그 밖에도 데이터 베이스와의 연동, switch-case 문에 대한 활용, 인터페이스 상속을 활용한 디자인 패턴 등 다양한 곳에 활용할 수 있다.

Posted by 이세영2
,