다시 앞서의 예로 돌아가보자.

class Task{

    public void method(){}

}


class Client{

    Task task = new Task();    // 1

    public void work(){

        task.method();         // 2

    }

}

책임의 종류를 따르자면 Task 클래스는 구현의 책임을 가지고 있다. 현재 명시적인 기능을 구현하지는 않았지만, Client 입장에서 Task는 어떤 기능을 수행하길 바라는 대상이다. Client 클래스는 1에 의해서 Task 객체를 생성하는 책임을 가지고 있다. 그러면서 2와 같이 Task 객체의 메소드를 이용함으로써 의존하고 있다.

만일 만들고자 하는 소프트웨어가 간단히 구구단 정도 출력하는 일이라면 지금의 구조로도 충분히 훌륭한 소프트웨어를 작성할 수 있다. Task 객체에 구구단 출력 기능을 구현하고, 구현된 구구단 출력 메소드를 Client 객체가 호출해 주면 되기 때문이다. 그리고 위의 예에서는 다른 어떤 보호 장치도 없이 Task 객체에 Client가 직접 의존하고 있다. 이것은 마치 Task 객체가 절대 사라지지도, 변경되지도 않을 것임을 확신하고 있는 것이나 다름 없다. 아무래도 Task 객체는 Client 객체와 매우 밀접한 관계에 있는 듯 하다. 그렇지 않고서야 서로 어떤 안전장치도 없이 의존할 수 있겠는가?

이와 같은 상황을 조금 더 이해하기 쉽게 하기 위해서 현실 세계의 예를 하나 도입해 보도록 하겠다. 잠시 스스로가 작은 음식점 주인이 되었다고 가정해보자. 적은 자본을 투자한 관계로 우선 혼자서 가게를 내기로 한다. 혼자 가게를 낸 상황이라면 가게의 모든 일을 주인인 내가 다 해야 하는 상황이다. 위의 예에서 보면 Client가 Task 객체에 의존하지 않고 혼자서 일을 모두 처리하는 상황이다. 시간이 조금 지나서 가게를 찾는 사람도 늘고, 아무리 음식점이라도 전문성이 중요하기 때문에 나보다 더 나은 주방장을 구해 일을 해보기로 한다. 그러면 주인인 나는 고용된 주방장에게 일을 맡기는 형태로 가게를 운영하게 될 것이다. 위의 예에서 Task 객체를 생성하고 메소드를 호출하는 과정과 유사하다. 하지만 뭔가 좀 위태로워 보인다. 주방장을 고용하는데 있어서 어떠한 안전장치도 없다. 음식점의 예에서 보면 취직한 주방장이 가게를 갑작스레 그만 둔다거나, 성실성이 부족하여 종종 가게에 나타나지 않는다거나, 음식 솜씨가 좋다고 해서 고용했는데 맛이 형편 없다거나 하는 위험한 상황이 있을 수도 있는데 말이다. 이런 상황에서 주인인 내가 안전하게 주방장을 고용할 수 있는 방법은 무엇일까? 바로 주방장과 계약을 하는 것이다. 가게 주인인 나는 주방장을 고용하기 위한 일련의 계약서를 작성해 둔다. 출퇴근 시간, 월급여, 경력 사항, 업무 내역과 같이 이슈가 될만한 사항들을 고용의 조건으로 만들어 두는 것이다. 이렇게 되면 좋은 점이 두가지 생긴다. 하나는 고용 조건에 합의한 주방장과 업무 시 직접 주방장이 할 수 있는 일을 물어보고 일하지 않아도 된다. 단지 계약서에 나열된 업무를 중심으로 일을 시키기만 해도 계약에 합의한 주방장은 당연히 그 일을 할 수 있을 것이다. 또 만일 기존의 주방장이 일을 그만두고 다른 주방장을 고용하게 되더라도 계약서에 합의 하기만 한다면 실제 업무 지시 방식을 바꾸지 않아도 새로운 주방장과 일하는데 문제가 없을 것이다.

객체지향으로 넘어와서 음식점 주인과 주방장의 관계 사이에 있던 "계약서"라는 개념이 무엇일까? 바로 interface이다. 이와 관련해서는 정확한 개념이 필요하기 때문에 잠시 정확한 개념을 설명하고자 한다.
앞서 지휘자와 개별 연주자들 간의 관계에서 "연주자"라는 개념을 도입했었다. 연주자는 개별 연주자의 차이점을 배제하고 공통점만 취한 추상적인 개념이다. 이와 상응하는 개념이 객체지향 언어에서의 interface이다. 그리고 가게 주인과 주방장 사이에 "계약서" 개념을 도입했는데 이 역시 interface를 염두해 둔 개념이다. 이들 두 예와 interface는 어떤 관계가 있을까? 우선 "연주자" 개념 역시 "계약서" 개념과 동일하다는 점을 설명할 필요가 있다. 지휘자 - 개별 연주자 간에는 직접적인 계약 관계가 필요하지는 않지만, 적어도 연주가 성공할 수 있으려면 "연주자" 개념은 강제성을 띄어야 한다. 연주하세요라고 말했을 때 연주를 시작하고, 연주를 멈추세요라고 말했을 때 멈추지 않으면 연주는 아름답게 이뤄지지 않을 것이다. 그런 강제성을 직접적으로 명시해야 할 필요가 있다면 그것이 "계약서"가 되는 것이다. 자 그럼 interface와 계약서의 관계를 이야기해 보자.
interface는 추상 공개 메소드(abstract public method)들의 집합으로 이루어진 추상 클래스(abstract class)이다. 만약 Java처럼 interface라는 명시적인 대상이 없는 언어를 다루고 있다면 추상 공개 메소드만 선언된 추상 클래스를 떠올리면 된다. 추상 클래스란 인스턴스화(객체화)가 불가능한 클래스다. interface는 추상 클래스이므로 interface 역시 객체화시킬 수 없다. 추상 공개 메소드는 구현부가 없는 메소드이다. 구현부가 없으므로 당연히 동작시킬 수 없다. 자기 자신은 구현부가 없지만 만일 interface를 상속한 경우(Java에서는 이를 상속이라 부르지 않고 구현(implements)이라고 부른다. 앞으로는 Java의 개념에 따라서 구현이라고 부르도록 하겠다.) 추상 공개 메소드의 구현부를 구현해야 할 의무가 생긴다. 추상 공개 메소드를 구현하고 싶지 않은 하위 클래스는 추상 클래스여야만 한다.
이러한 개념들에 비추어 보면, interface는 이를를 구현하는 하위 클래스들에게 정해진 공개 메소드를 꼭 갖추도록 강제하는 역할을 한다는 점을 알 수 있다. interface 스스로는 직접적인 구현을 제공하지 않는다.
이런 interface의 특징은 두가지 특성을 가져다 준다. 우선 interface를 구현하는 구체적인 객체에게는 강제성을 주면서도 자율성을 보장한다. 강제성은 interface가 선언한 추상 공개 메소드를 꼭 구현하도록 한다는 점에서 그렇다. 대신 interface는 어떤 형태로도 구현을 가이드 하지 않기 때문에 구체적인 객체는 어떤 방식으로든 자유롭게 구현할 수 있다. 이는 연주자가 연주하세요와 멈추세요에 반응해야 하는 것은 강제이지만, 개별 연주자들의 실제 연주 내용(구현)은 서로의 악기에 맞게 자율적으로 할 수 있다는 것과 같다. 이를 통해서 얻을 수 있는 이점은 개별 연주자의 범위를 넓힐 수 있다는 것이다. 단지 연주하세요와 멈추세요에만 응답할 수 있다면 서양의 악기든 동양의 악기든 상관하지 않고 모든 악기 연주자들을 구현해도 좋다. 이는 다양한 요구사항을 반영할 수 있는 역할을 한다. interface가 주는 또 하나의 특성은 구체적인 구현 객체들을 모르더라도 interface를 구현한 객체들이라면 모두 사용할 수 있다는 것이다. interface를 구현한 모든 객체들은 interface가 제공하는 공개 메소드를 구현해야만 한다. 이것은 강제적이다. 따라서 사용하는 측에서는 interface만 보고 객체를 다루더라도 전혀 지장이 없다. 사용하는 측에서 매우 다양해질 수 있는 구체적인 구현 객체를 모르고도 그 객체들을 다룰 수 있다는 점은 유연성에 매우 큰 이득이다. 

계약 책임의 도입

이제 우리의 예제에 계약 책임을 도입해볼 차례다. 계약 책임은 interface가 가지고 있다. 따라서 interface를 선언할 필요가 있다. 그리고 구현 책임을 가진 Task 객체는 그 interface를 구현해야 한다. 마지막으로 사용자인 Client 객체는 Task 객체를 직접 사용하지 않고 interface를 이용해서 Task 객체를 다루어야 한다. 이와 같은 형태로 코드를 수정하면 아래와 같아진다.

interface Contract{

    public void method();

}

class Task implements Contract{

    public void method(){}

}

class Client{

    Contract task = new Task();    // 1

    public void function(){

        task.method();             // 2

    }

}


interface가 계약이라는 점을 명시하기 위해서 interface의 명칭(symbol)은 Contract라고 붙였다. 그리고 이 계약서에는 method()라는 공개 추상 메소드를 구현하도록 명시되어 있다. 따라서 Contract interface를 구현하는 클래스는 항상 method()도 구현해 주어야 한다. 위의 예제에서는 Task 클래스가 Contract interface를 구현하고 있다. 따라서 Task 클래스는 method()를 역시 구현하고 있다. Task 객체를 사용하고자 하는 클래스인 Client 클래스는 "계약"에 따라서 Task를 사용해야 한다. 따라서 기존에는 Task 참조 변수를 선언하였으나 이제는 Contract 참조 변수로 대체되었다.(1번 라인) 하지만 실제 사용할 객체는 Task 타입이므로 Task 객체를 생성하여 Contract 참조 변수로 참조하도록 되어 있다. 2번 라인의 메소드 호출은 일면 이전의 코드와 같아 보인다. 하지만 task 참조 변수는 그 타입이 Contract로 변경되어 있다. 따라서 2번 라인의 호출은 직접 Task 객체의 메소드를 호출하는 것이 아니라 Contract 타입의 method()를 호출하는 것이다. 이와 같은 관계를 이해하기 위해서 다음의 예제도 한번 살펴보자.

interface Contract{

    public void method();

}

class Task2 implements Contract{

    public void method(){ /* something else */ }

}

class Client{

    Contract task = new Task2();

    public void function(){

        task.method();            // 메소드 호출

    }

}


위의 예제에서는 Task 클래스 대신 Task2 클래스를 선언하고 사용하고 있다. Task2 클래스 역시 Contract interface를 구현하고 있으므로 method()도 동일하게 구현하고 있다. 하지만 실제 동작은 기존의 Task 클래스와는 다르게 선언했다고 가정해 보자.
이제 Task2를 사용하기 위해서 Client 클래스에서는 Task2 객체를 생성하여 Contract 타입의 참조 변수인 task에 할당한다. 그리고 메소드 호출 부분으로 넘어가보자. 이전의 예에서와는 다르게 task 참조변수에는 Task2 객체가 참조되어 있다. 따라서 메소드 호출 부분에서 메소드를 호출한 경우 Task2 객체의 메소드가 호출된다. 즉, 이전의 예에서 할당된 객체는 Task 객체였고, 이번읜 Task2 객체이므로 실제 호출되는 메소드는 달라지지만 둘 다 같은 형태로 호출이 된다. 이는 참조를 직접 Task 객체나 Task2 객체로 하지 않고 Contract 타입으로 참조했기 때문에 가능한 일이다.
이를 통해 얻은 것은 무엇일까? 만일 필요에 의해서 Task 객체나 Task2 객체 또는 또 다른 Contract interface를 구현한 객체로 바꾸게 되더라도 function() 메소드가 수정될 필요가 없어졌다. 즉 우리가 바라던 유연성이 확보된 것이다. Contract 타입의 객체는 그 "계약"만 제대로 지킨다면 무수히 늘어날 수 있으므로 무수히 많은 요구사항을 반영할 수 있는 구조가 된 것이다. 

클래스 다이어그램을 통한 이해와 여전한 문제점

그러면 이제껏 진행했던 내용을 UML의 클래스 다이어그램을 통해 이해해보자. 클래스 다이어그램은 실제 동작시 객체간의 유기적인 협력 관계를 표현하기에는 적절하지 않지만(유기적인 협력 관계는 UML의 시퀀스 다이어그램을 이용하는 편이 좋다), 우리가 하고 있는 작업, 즉 의존 관계에 대한 관리를 하는데는 매우 유용한 다이어그램이다. 클래스 다이어그램을 단순화해서 보면 클래스(+ interface)와 관계(association, dependency, generalization)로 나누어 볼 수 있다. 관계는 연관, 의존, 일반화 등으로 나누어질 수 있지만, 단순하게 보자면 어떤 클래스와 다른 클래스가 관계로 연결되어 있다면 그 둘 중 적어도 하나에는 상대 클래스가 코드 상에 등장한다는 것을 의미한다. 그래서 우리가 하는 작업을 통해서 다른 클래스와의 관계가 잘 정리되었는지를 확인하는데 매우 유용하다.

위의 다이어그램은 이 글에서 작성한 코드를 클래스 다이어그램으로 나타낸 것이다. Client의 입장에서는 여러 개별 Task들을 직접 다루기보다는 Contract interface를 도입해서 간접적으로 다루도록 만들었다. 여기서 가장 좋은 것은 Client가 Task들과 아무런 관계를 가지지 않는 것이다. 하지만 위 다이어그램에서도 보듯이 Client는 Task들의 생성자 메소드를 호출함으로써 생성(create)을 수행하고 있다. Task들을 전혀 모르고도 사용할 수 있어야만 의존관계를 완벽하게 끊어낼 수 있다. 더 나은 구조를 만들어내기 위해서는 별도의 작업이 필요하다.


Posted by 이세영2
,

우리의 목표는 객체지향 언어를 이용해서 유연성 있는 소프트웨어를 개발하는 것이다. 소프트웨어가 유연하다는 것은 다양한 요구사항 변화에도 대처가 가능하다는 의미이다. 요구사항 변화는 소프트웨어에 추가, 제거, 변경과 같은 수정을 가져온다. 즉, 유연성은 소프트웨어가 얼마나 손쉽게 수정될 수 있느냐에 달려 있다. 이미 요구사항의 변화를 쉽게 반영할 수 있는 객체지향의 4대 특성에 대해서 이미 살펴봤다. 그리고 객체지향의 4대 특성이 반영된 일반적인 구조에 대해서도 살펴봤다. 지휘자 - 연주자 - 개별 연주자로 연결되는 구조를 통해서 객체지향 언어가 어떻게 다양한 요구사항을 동일하게 취급하는지 살펴 보았다. 하지만 이제 객체지향 언어를 이용하여 완성된 형태의 유연성을 제공하기 위해 알아 두어야 할 요소에 대해 설명할 시간이다.


의존(Dependency)과 책임(Responsibility)

의존과 책임은 객체지향 4대 특성을 통해 확보한 유연성을 더욱 강화시키기 위해 필수적으로 알아 두어야 할 요소이다. 그리고 의존과 책임은 별도로 설명하기 곤란할 정도로 밀접한 관계가 있는 개념들이다. 이 개념들을 실제 코드를 통해 이해해 보도록 하자.

class Task{

    public void method(){}

}


class Client{

    Task task = new Task();    // 1

    public void work(){

        task.method();         // 2

    }

} 

위의 코드에는 두 개의 클래스가 있다. 하나는 Task 클래스이고, 다른 하나는 Task 클래스를 사용하는 Client 클래스이다. Client 클래스에서는 1 처럼 Task 객체를 "생성"하고 있다. 그리고 2번 처럼 그 객체의 메소드를 호출하고 있다. 메소드 호출을 객체지향 용어로는 "메시징"이라고 부른다. 그리고 메소드 호출을 통해 다른 객체를 "사용하는" 것을 객체지향 용어로는 위임(Delegation)이라고 한다. 본래 Client 객체가 해야 할 일이지만 (다양한 이유로 인하여) Task 객체에게 맡긴다는 의미이다. 이 전체를 한마디로 표현하자면 Client 객체가 Task 객체를 생성하고, 일부 기능을 위임한 것이다. 이 구조에서 구체적으로 어떤 기능을 위임했는지는 중요하지 않다. 다만 기능의 종류와 상관 없이 일반적으로 만들어지는 구조라는 것이 중요하다.

또한 Client와 Task 객체간의 관계를 의존관계라고 부른다. 좀 더 정확하게 말하면 Client 객체가 Task 객체에 의존하는 것이다. 위의 코드에서 의존은 두 번 발생한다. 우선 Task 객체를 생성하는 1번 코드에 의해 의존한다. 또 2번의 method() 호출을 통해서도 의존한다. A가 B에 의존한다는 말은 간단하게 이야기 하면 A의 코드 상에 B의 요소가 등장한다는 말이다. 1에서는 Task 객체의 타입이 등장하고, 생성자 메소드 호출이 등장한다. 2에서는 method()라는 Task 객체의 메소드 호출이 등장한다. 이것이 의존으로 불리는 이유는 다른 객체의 요소가 등장하는 순간부터 그 객체가 없어지면 안되기 때문이다. 만일 의존 대상인 Task 클래스가 사라지면 Client 객체는 바로 컴파일이 불가능해지고 동작할 수 없는 상태가 된다.

이 의존 관계가 내포하는 문제를 조금 더 깊이 다뤄보자. 위의 코드처럼 Client가 Task에 의존하는 상태, 즉 Client 클래스 코드에 Task 클래스 코드가 등장하는 상황에서는 Task 클래스의 변경이 Client 코드에 영향을 미치게 된다. 앞서 Task 클래스가 사라지게 되는 경우도 이에 해당한다. 만약 의미를 명확하게 하기 위해서 Task 클래스의 명칭을 바꾼다고 하자. 그러면 Client 코드에서도 Task의 변경된 명칭을 사용해야만 한다. Task의 method()라는 이름의 메소드 역시 의미를 명확하게 하려는 이유로 변경될 수 있다. 그러면 2번 코드 처럼 해당 메소드를 호출하는 코드 역시 변경해 주어야 한다. 만약 Task의 기본 생성자 메소드를 대신하여 인자를 받는 생성자 메소드가 추가되었거나, 기본 생성자 메소드의 가시성을 낮춰서 외부에서는 Task 객체를 생성하지 못하도록 만들었다면 1번의 코드도 그에 맞춰 수정되어야 한다. 위의 예에서는 Task 클래스에 의존하는 곳이 단 두 곳 뿐이지만, 상황에 따라서는 코드들을 모두 변경하기 버거울 정도로 많은 곳에서 의존이 발생할 수도 있다.

이것은 소프트웨어 개발자들이 바라지 않는 일이다. 우리가 Task를 수정해야겠다고 느끼는 시기는 Task를 수정해야 할 이유가 있을 때이다. Task라는 클래스명의 의미가 모호하다고 생각될 경우, method()라는 메소드 이름이 모호하다고 생각될 경우, Task 객체가 해야 할 일이 변경될 경우, 새로운 메소드를 추가하거나 기존 메소드를 제거할 경우 등이 Task 클래스를 수정하게 되는 상황이다. Task 클래스를 수정하는 이유는 이를 사용하는 Client 클래스와는 전혀 상관 없는 일이다. 하지만 위의 코드에서 보듯이 Client의 코드가 Task의 코드에 의존하고 있을 경우 Client의 코드가 수정되는 것은 자명하다. 즉, 수정을 해야할 이유는 Task 코드에서 발생했는데 엉뚱하게 이를 사용하는 Client 코드가 수정되어야 하는 상황이 된 것이다. 

만약 사용되는 객체(또는 요소)로 인하여 사용하는 객체가 수정되는 이와 같은 일이 발생하지 않도록 하려면 어떻게 해야 할까? 단 한가지 답만 존재한다면 사용되는 대상이 수정될 가능성을 낮추는 것이다. 그렇다면 이 "수정될 가능성"을 어떻게 판단할 수 있을까? 이 답은 로버트 C. 마틴이 밝힌 객체지향 설계의 5원칙(SOLID)에 들어 있다. 


책임(Responsibility)의 정의

객체지향 설계의 5 원칙(SOLID) 중에서 첫번째 원칙은 단일 책임의 원칙이다. 단일 책임의 원칙(Single Responsibility Principle)은 객체지향의 구성요소가 단 하나의 책임만을 가진다는 원칙이다. 그러면 책임(Responsibility)이란 무엇인가? 단일 책임 원칙에서는 책임을 다음과 같이 정의 한다.


"변경하려는 이유"


즉 우리가 찾던 "사용하려는 대상이 수정될 가능성"을 "책임"이라고 부를 수 있다는 것이다. 어떤 객체가 다른 객체에 의존하는 이유는 무엇인가? 의존의 대상이 되는 객체에 어떤 "책임"이 부여 되어 있고, 그 책임을 이용하고 싶기 때문이다. 그래서 책임에 대해 덧붙이자면 기능이라고 생각할 수도 있다. 이용해야 할 이유, 즉 기능이 있어야 이용하고 싶기 때문이다. 그리고 기능이 있어야 그 기능을 수정하려는 이유도 생긴다. 아무 기능도 없다면 의존할 필요도 없고, 수정할 필요도 없다. 


그러면 실제 구현 속에서 책임을 찾아보자.

구현의 대상이 필드라면 필드의 가시성, 타입 및 명칭이 변경의 대상이 된다. 또한 필드의 초기값도 변경의 대상이 될 수 있다. 필드 중에서도 객체 참조의 경우에는 참조할 객체의 할당 방법도 변경의 대상이 된다. 예를 들어 필드의 선언과 함께 객체를 생성해서 할당한 경우라면 참조할 객체의 종류 역시 변경될 수 있다.

메소드는 메소드의 시그니쳐(signature)와 메소드 내부 구현이 변경될 수 있다. 메소드의 가시성, 리턴 타입, 메소드 명칭, 파라메터, 메소드의 내부가 변경의 대상이다.

클래스는 타입, 필드, 메소드를 가지고 있다. 따라서 클래스는 (가독성에 따라) 자신의 타입명이 변경될 수도 있고, 다른 클래스를 상속 받거나 interface를 구현하게 됨으로써 자신이 가질 수 있는 타입이 변경될 수도 있다. 또 클래스는 필드가 변경 될 수도, 메소드가 변경될 수도 있다. 

구현의 대상이 interface라면 interface에 선언된 메소드가 변경의 대상이 될 수 있다.


단일 책임 원칙에서 정의하는 책임을 그대로 따른다면 위에서 열거한 변경의 대상은 모두 다 책임이 된다. 이들을 모두 개별적인 책임으로 정의하고 각 책임에 맞는 대응책을 모색하기에는 너무 많은 양이다. 별로 중요하지 않은 책임을 드러내놓고 논의하는 것은 시간 낭비에 불과하다. 적어도 객체지향을 통한 설계나 구현을 위해 의미 있는 정보를 제공해 줄 수 있는 수준이 되어야 한다. 또 너무 유사한 것들을 일일이 나열하면서 설명하는 것도 의미가 없는 일이다. 비슷한 것들은 하나의 개념으로 통합 시킬 필요가 있다. 그래서 중요도, 유사성, 실용성 등을 고려하여 책임을 적절히 분류해 보고자 한다.

책임을 종류별로 분류하고자 하는 목적을 이야기 할 필요가 있다. 다시 말하지만 우리는 객체지향 언어를 통해서 유연한 소프트웨어를 만들고자 한다. 유연한 소프트웨어를 만들기 위해서 추상화된 대상을 통해서 다양한 요구사항의 구체적인 차이점을 감추도록 했다. 이것만으로도 훌륭하지만 구성요소간의 의존성이 여전히 문제로 남아 있다. 다른 객체를 이용하는 객체는 이용되는 객체의 변경에 영향을 받는다. 이를 해결하는 방법은 객체들이 수정될 이유가 적어지게 만드는 것이다. 객체들이 수정될 이유가 바로 책임이다. 만약 어떤 객체가 너무 많은 책임을 가지고 있다면, 이 책임을 분할할 필요가 있다. 책임이 적어진다는 것은 변경할 이유가 적어진다는 것을 의미한다. 의존하는 대상이 변경될 이유가 적어지면 사용하는 쪽의 코드가 수정될 가능성도 함께 적어진다.

그래도 한가지 의문이 더 남는다. 결국 많은 책임을 분할하여 여러 객체나 요소로 분산하자는 얘기인데, 그렇게 되면 결국 의존이 여러 대상으로 분산될 뿐, 전체적인 의존의 복잡성은 그대로 유지되는 것이 아니냐는 것이다. 물론 상황에 따라서는 의존성이 줄어들기보다 객체나 요소가 늘어나는 이유로 인하여 관계가 더 복잡해 보일 수도 있다. 하지만 책임을 나눌 경우 크게 두가지에서 이득을 얻을 수 있다. 

첫째로 책임이 적절하게 여러 대상으로 분할된 경우가 여러 책임이 하나의 대상에 밀집해 있는 경우보다 덜 수정된다는 점이다. 만일 여러 책임이 하나의 클래스에 몰려 있다고 가정해 보자. 이 경우에는 종종 여러 책임별로 메소드 단위, 속성 단위로 잘 구분되지 않는 경우가 많이 있다. 즉, 하나의 메소드가 두가지 책임을 위해서 사용될 경우도 있고, 두가지 이상의 책임이 하나의 메소드 흐름 속에 뒤죽박죽 섞여 있을 수도 있다. 책임을 면밀히 생각해 보지 않았기 때문에 이들이 섞여 있어도 그렇다는 것을 잘 모르는 경우가 많다. 이 때 어떤 이유로 대상을 수정하게 된다면 어떤 일이 벌어질까? 하나의 목적으로 수정을 시작했는데, 그 목적과는 관계없는 코드들이 중간중간에 섞여 있을 것이다. 이는 수정에 집중하기 어려운 상황을 만든다. 그리고 하나의 책임만을 수정했음에도 불구하고 다른 책임을 잘못 건드려서 다른 목적에도 맞지 않게 수정될 가능성도 있다. 이것은 단일 책임 원칙에 의해 책임이 잘 분할된 여러 요소들을 개별적으로 수정할 때에 비해 매우 비효율적일 수 밖에 없다.

둘째로 책임이 분할되어 있을 경우 중복 요소의 확인이 쉽고, 이를 통해 한차원 높은 추상화를 발휘할 수 있다는 점이다. 책임을 분할해 보면 책임이 다른 객체로 분할되거나 별도의 메소드로 분할되게 된다. 이렇게 대상들이 작아지면 전체를 이해하기가 수월해진다. 그리고 작은 단위들은 서로 유사성을 띄는 경우가 많다. 완성된 자동차를 종류별로 비교하면 동일성을 찾기가 어렵지만, 부품단위로 뜯어보면 같은 것을 발견할 가능성이 높아지는 것과 같은 이치이다. 설령 조금씩 다르다고 해도 부품들의 목적이 같다면 둘을 통일 시켜 하나의 요소로 만들기도 쉽다. 같은 세단이라도 완성차끼리 호환시키기는 어렵지만, 세단과 SUV 사이라고 해도 부품별로 놓고 보면 나사, 프레임, 바퀴, 선루프 등 부품별로는 얼마든지 동일성을 유지시킬 수 있는 여지가 있다. 이와 같은 이유로 더 많은 요소의 동일성을 확보하고, 이를 통해 더 많은 유연성을 확보하는 것이 가능해 진다.

책임을 잘 분류하면 다음과 같은 효과를 얻을 수 있다. 우선 단일 책임 원칙을 어떻게 위반하고 있는지를 확인할 수 있다. 어떤 구현 요소(interface, 클래스, 메소드 등)가 여러 종류의 책임을 함께 가지고 있음을 분석해낼 수 있고, 어떤 책임을 분리하여 단순화할지에 대한 전략을 세울 수 있다. 또 책임의 종류에 따라 어떤 형태로 책임의 분할을 수행할 수 있을지를 알 수 있다. 그리고 이런 분할 방식을 이용하여 어떤 소프트웨어를 작성하더라도 정형화 된 형태의 설계나 구현을 할 수 있게 된다. 이것은 개발의 속도를 향상 시키는데 매우 큰 도움이 된다. 마지막으로 객체지향 소프트웨어가 어떤 형태로 기능을 확장해 나가야 할지를 가이드 할 수 있다. 기능이 확장된다는 것은 책임이 증가한다는 것을 의미한다. 책임이 증가되면 단일 책임 원칙을 지키기 위해 분할이 필요하다. 이 때 책임의 종류를 명확하게 이해하고 있으면 쉽게 책임을 분할 할 수 있고, 단일 책임 원칙을 유지시킴으로써 좋은 소프트웨어 구조를 유지할 수 있다.


생성 책임

객체의 생성은 매우 중요한 작업 중의 하나이다. 객체를 생성하는 과정의 중요성 때문에 GoF의 디자인 패턴에도 5가지 종류의 생성 패턴이 존재한다. 또한 생성 과정에서 만나는 다양한 문제점들을 해결하기 위해서 많은 소프트웨어들에서 Factory Method를 이용한다.

이 생성 책임은 세부적으로 다음과 같은 작업들이 정상적으로 이루어지도록 만든다. 첫번째는 적절한 구체 객체를 생성하는 것이다. 생성 패턴이나 Factory Method에서는 사용자가 사용하는 생성자 객체 혹은 생성 메소드에 따라서, 또는 생성 메소드에 의해 전달되는 파라메터에 따라서 다른 객체를 생성하고 제공한다. 생성 책임을 지고 있는 모듈로부터 적절한 객체를 제공 받음으로써 어플리케이션이 적절한 동작을 취할 수 있도록 한다. 두번째로 객체의 타입을 감춘다. 구체 객체를 생성했다고 해도 그 구체 객체의 타입으로 사용하지 않는 편이 좋을 경우가 있다. 대표적으로 다형성을 통해 구현된 일련의 객체들을 받아 사용하는 경우이다. 이 때 생성 책임을 지는 모듈은 구체 객체의 타입을 감추고 추상 타입으로 리턴해 줌으로써 객체를 사용하려는 쪽과 생성된 구체 객체간의 관계가 생성되는 것을 막아준다. 세번째로 객체가 생성되고 나서 사용되기 전에 해줘야 할 작업들을 실행해준다. 대표적으로 초기화 작업이나 이벤트 수신을 위해 생성된 객체를 다른 객체에 등록하는 작업 등이다. 네번째로 객체의 생성에서 사용 전까지 있을 수 있는 동기화 문제를 방지해 주는 역할을 한다. 다섯번째로 객체가 생성되면서 가져야 할 초기 값들을 할당해준다.


조립 책임

현대 소프트웨어처럼 요구사항이 다양화 되기 전까지는 객체를 조립한다는 개념이 두드러지지 않았다. 프레임워크, 즉 어떤 어플리케이션으로 진화하든지 상관 없이 충분한 기능을 지원해 줄 수 있도록 설계된 소프트웨어 플랫폼들에서나 주로 고민하던 부분이다. 하지만 객체지향 언어에서 의존성 관리를 통한 유연성 확보 개념이 널리 퍼지고, 엔터프라이즈 애플리케이션이 더 많은 산업 분야에 적용되고, 같은 기능의 소프트웨어를 대량으로 판매하기 보다는 사용자의 요구에 맞춰 다른 기능들을 제공하는 방식을 더 많이 지원해야 하는 상황에서 이 조립 책임이 대두되었다. 아마도 앞으로 인공지능 분야가 더더욱 발전하고 소프트웨어가 더 많은 데이터를 손쉽게 수집할 수 있는 플랫폼을 개발하는 방향으로 나가게 된다면 이 조립 책임은 더욱더 비중이 커지게 될 것이다.

Spring 프레임워크의 의존성 주입(Dependency Injection), 넷빈즈의 룩업(Lookup) 서비스 객체, 서비스 로케이터(Service Locator) 등과 같이 객체의 조립이라는 과정은 독립적이고 핵심적인 요소이다.


구현 책임

구현의 책임은 구체 객체가 가진 책임이다. 즉 특정 요구사항에 맞게 실제 동작을 수행할 수 있는 메소드를 가지고 있는 것이다. 좀 더 구체적으로 보면 공개 메소드가 구현 책임의 대상이 된다. 이 공개 메소드는 interface나 상위 클래스를 상속함으로써 구현이 강제 되기도 하고, 자체적으로 공개 메소드로 설정하고 제공되기도 한다. 어쨌든 구체 객체는 이들 공개 메소드에 대하여 구현함으로써 실제 작업을 수행하는 책임을 가진다.


계약 책임

계약 책임은 interface에 대한 책임이다. 계약이라는 이야기가 매우 생소할 수도 있다. 하지만 어떤 interface를 구현하는 객체들이 구체적인 구현에 있어서 자율성을 가지고 있다는 사실은 이미 알고 있을 것이다. 객체를 사용하는 쪽에서는 구체적인 객체가 어떤 방식으로 동작하는지에 대해서는 관여하지 않는다. 이를 통해 다형성이 유지되고 유연한 구조의 소프트웨어가 탄생할 수 있다. 하지만 이와 함께 interface는 행위를 강제하는 역할을 한다. 이것을 현실 세계와 비교해 보면 "계약" 관계와 그 유사성을 찾을 수 있다. 계약서를 들이 미는 쪽은 사용자 측이고, 계약을 이행해야 하는 측은 구체적인 구현 객체이다. 그리고 계약서에 해당하는 것은 interface이다. 사용자 측에서는 계약서, 즉 interface를 구현하기만 한다면 구체적인 객체가 어떤 일을 하든지 상관하지 않는다. 대신 계약서(interface)는 반드시 지켜져야만 한다. 

이런 개념으로 interface를 이해하면 리스코프 치환의 원칙이 자연스럽게 지켜진다. 사용자 측에서는 언제나 interface를 통해서만 객체를 취급한다. 구체적인 객체들은 interface의 구현을 강제 당하는 셈이지만, 그 방법에 대해서는 최대한 자유를 누릴 수 있다. 이런 특성들을 개념적으로 잘 이해할 수 있도록 하기 위해서 interface에 계약 책임을 부여하고자 한다.


상태 책임

상태 책임은 필드와 필드의 가시성, 그리고 getter/setter에 대한 책임이다. 어떤 객체에 필드를 선언할 때는 메소드를 선언할 때보다 훨씬 신중해야 한다. 필드는 구현을 도와주는 도구가 아니다. 오히려 가능한 한 선언을 피해야 하고, 일단 선언 되어 있다면 가능한 한 가시성을 낮춤으로써 외부로 유출되는 것을 방지해야 한다. 일단 선언된 필드는 필드를 가지고 있는 객체에 책임을 뒤집어 씌운다. 필드는 필드 스스로 연산을 수행할 능력이 없기 때문이다. 다른 객체들은 특정 필드가 선언된 객체에게 해당 필드의 값을 요구하거나(getter), 어떤 필드 값을 저장하라고 시키거나(setter), 그 필드와 연관된 연산을 수행할 것을 요구할 수 있다. 그리고 특히 getter 메소드는 필드의 값을 외부로 누출시켜 필드에 의해 발생하는 로직들(특히 조건문과 같은)을 전파시키는 역할을 하게 된다. 따라서 단순히 필드를 노출시키는 것만 아니라 필드를 통해 다른 객체들이 하고자 하는 일들을 예상하고 그 일들을 미리 구현해야 한다.


이로써 책임의 분류가 끝났다. 이제 책임의 분류가 어떤 효과를 나타내는지 확인해 볼 차례다.

Posted by 이세영2
,

설계를 하다보면 종종 상위의 인터페이스나 상위 클래스는 외부에 public으로 노출을 시키되, 하위의 구체 클래스들은 외부에 노출시키지 않아야 하는 경우가 있다. 이는 정보 은닉을 위한 좋은 설계 방법 중 하나이다. 이를 위해서 보통 하위의 구체 클래스들을 package private class(JAVA에서 접근자 키워드가 없이 선언되는 클래스)로 선언한다. 이렇게 하면 같은 패키지 내에서는 해당 클래스에 접근할 수 있어도 다른 패키지에서는 package private 클래스를 접근할 수 없으므로 오직 인터페이스나 상위 클래스를 이용할 수 밖에 없다. 따라서 하위 클래스들의 변경이나 추가와 같은 유연성이 보장된다.

하지만 문제는 package private class에 대한 Unit Test를 작성하는 일은 쉽지 않다는 점이다. 일반적으로 제품 코드와 유닛 테스트 코드는 분리되는 것이 좋다. 제품 코드와 유닛 테스트 코드가 같은 곳, 즉 같은 패키지 내에 존재하면 제품의 출시를 위해서 유닛 테스트 코드를 골라내는 작업이 필요하다. 또 제품 코드를 이해하기 위해서 소스를 뒤적이다 보면 유닛 테스트 코드가 함께 섞여 있어서 불편함을 겪게 된다. 그래서 일반적으로 제품 코드와 유닛 테스트 코드를 분리하여 두는데, 이렇게 되면 제품 코드와 유닛 테스트 코드의 패키지가 분리되면서 package private인 요소들에 대해서는 유닛 테스트가 접근할 수 없게 된다. 이렇게 되면 간접적으로 해당 요소들을 테스트해야 하는데 이는 매우 번거로운 작업이 된다.

이클립스에서는 이 문제를 해결할 수 있는 좋은 솔루션을 제공해 준다. 바로 소스 폴더를 여러개 등록하는 것이다.

일단 해결하고자 하는 문제를 명확하게 보여주기 위해서 다음과 같은 예제를 작성해 보겠다.


Shape 및 ShapeFactory 소스 코드

// Shape.java

public interface Shape {

    public void draw();

}

class Triangle implements Shape{

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

}

class Circle implements Shape{

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

}

// ShapeFactory.java

public class ShapeFactory {

    public Shape createTriangle(){ return new Triangle(); }

    public Shape createCircle(){ return new Circle(); }

} 

우선 외부에 노출시키고자 하는 인터페이스인 Shape을 정의한다. 그리고 이 인터페이스는 외부에 노출될 것이므로 public interface로 선언하였다. 그리고 하위 클래스인 Triangle과 Circle 클래스들을 구현한다. 이들 클래스는 외부에 노출시키지 않을 것이다. 따라서 접근자가 없는 클래스, 즉 package private 클래스로 선언이 된다.

그리고 외부에 Triangle 클래스와 Circle 클래스를 노출시키지 않을 수 있으려면 생성자 호출을 대신할 Factory 클래스를 제공해 주어야 한다. ShapeFactory 클래스는 Shape 클래스와 같은 패키지 내에 있으면서 외부의 요청에 따라 Triangle 객체와 Circle 객체를 생성하여 제공해 준다. 이 때 구체적인 타입을 감출 수 있도록 Shape 타입으로 상위 타입 캐스팅을 한 후에 제공한다. 이를 통해 Triangle과 Circle 클래스가 외부로 노출되는 것이 차단된다.


이제 유닛 테스트를 구현해야 할 차례다. ShapeFactory가 각 API에 따라서 제대로 된 Triangle 객체와 Circle 객체를 생성하는지를 테스트 해보고자 한다. 그렇다면 아래와 같은 테스트 클래스를 구현해 볼 수 있다.


ShapeFactoryTest.java

public class ShapeFactoryTest {

    ShapeFactory sut;

    @Before

    public void setUp(){

        sut = new ShapeFactory();

    }

    @Test

    public void testCreateRectangle(){

        Shape shape = sut.createTriangle();

        assertTrue(shape instanceof Triangle); // --- 1

    }

    @Test

    public void testCreateCircle(){

        Shape shape = sut.createCircle();

        assertTrue(shape instanceof Circle);   // --- 2

    }

} 

1과 2에서와 같이 Triangle 객체와 Circle 객체가 제대로 생성되었는지를 확인할 필요가 있다. 그런데 Triangle과 Circle은 모두 package private 클래스들이다. 따라서 이 유닛 테스트 코드와 제품 코드가 분리되어 있더라도 이들에 접근 할 수 있는 방법이 필요하다.


제품코드와 테스트 코드의 소스 폴더 분리하기

Proeject Explorer / New / New JavaProject를 클릭하면 아래와 같은 다이얼로그가 나올 것이다. 


여기서 바로 finish를 누르지 말고 Configure default 버튼을 누른다. 그러면 아래와 같은 다이얼로그가 생성된다.

붉은색 박스처럼 src 대신 src/java/main을 입력하고 확인을 누른다. 그리고 나서 Next 버튼을 누르면 아래와 같이 화면이 바뀌게 된다.


여기서 오른쪽 클릭을 하고 New Source Folder 버튼을 누른다. 그러면 아래와 같은 다이얼로그가 생성된다.


여기에 Folder name에 src/test/main을 입력하고 확인을 누른다.

그리고 나서 Finish를 누르면 프로젝트 생성이 완료된다. 폴더 구성이 아래와 같다면 제대로 생성이 된 것이다.


일반적으로는 src라는 폴더 하나만 있는데 우리가 만든 프로젝트는 src/java/main과 src/test/main 폴더가 더 추가되어 있다. 그리고 이들 모두가 새로운 패키지를 생성하는 것이 가능하다. 이 두 폴더 중에서 src/java/main 폴더에는 제품 코드를 추가하고 src/test/main에는 유닛 테스트 코드를 추가하면 된다.

이렇게 소스 폴더를 둘로 나눠 놓은 이유는 물론 제품 코드와 유닛 테스트 코드를 분리시키기 위해서이다. 그런데 다른 방법으로도 둘을 분리할 수 있음에도 굳이 방법을 사용한 이유는 저 두 폴더에서 생성한 동일한 경로의 패키지는 같은 패키지로 취급되기 때문이다. 같은 패키지로 취급된다는 말은 위에서 package private 클래스에 대한 유닛 테스트와 Shape 및 ShapeFactory에 대한 소스 코드를 각각의 폴더에 분리하여 넣어도 둘의 패키지 경로만 같게 설정하여 주면 테스트가 정상 동작한다는 것을 의미한다.

이를 확인하기 위해서 두 폴더에 각각 com 이라는 경로의 패키지를 만들어 본다. 그러면 아래와 같이 될 것이다.


이제 위의 패키지에는 Shape.java와 ShapeFactory.java를 넣고, 아래 패키지에는 ShapeFactoryTest.java를 넣는다. 그리고 유닛 테스트를 실행해 보면 된다.

같은 테스트 코드라고 해도 다른 패키지 경로에 넣으면 Triangle 과 Circle 클래스가 등장하는 곳에서 에러가 발생한다.


사실 이러한 소스 폴더 분리 방식은 이미 Spring Framework에서도 적용하고 있는 방식이다. 만일 이와 같이 package private class들에 대한 테스트가 필요하다면(일반적으로 모든 프로젝트에서 그렇겠지만) 꼭 위와 같이 같은 패키지 경로들을 만들어 테스트 코드를 넣는 것이 좋다. 그래야만 보다 손쉽게 테스트 코드를 작성할 수 있다. 만일 Spring이 아닌 일반 프로젝트를 생성한다고 해도 유닛 테스트를 작성해야 하는 경우라면 미리 위와 같은 형태로 소스 폴더를 구성해 두는 편이 유닛 테스트를 작성하기에 용이할 것이다.

한가지만 더 이야기 하자면, protected 메소드 역시 같은 방식으로 테스트가 가능하다. protected 메소드는 package private과 같이 같은 패키지 내의 다른 클래스에서도 접근이 가능하다. 따라서 위와 같은 형태로 소스 경로를 분리한 후 테스트를 수행해 볼 수 있다.



Posted by 이세영2
,

객체지향 개념들간의 관계 UML


간략히 설명하자면 다음과 같다.

- OOP : Object Oriented Programming

- OOP는 유연성(Flexibility)을 가진다.

- 유연성은 캡슐화와 추상화, 다형성을 이용해서 달성된다.

- 상속은 캡슐화를 활용하고, 다형성은 상속을 이용해서 만들어진다.

- 캡슐화를 통해서 가시성(Visibility) 개념이 만들어지고, 클래스 개념이 만들어진다.

- 클래스는 타입과 필드, 그리고 메소드를 가지고 있다.

- 클래스 개념은 인터페이스(Interface), 추상 클래스(Abstract Class), 구체 클래스(Concrete Class) 개념을 파생시킨다.

- 객체(Object)는 구체 클래스(ConcreteClass)가 가진 개념을 포함한다.

- 객체는 Identity를 가지고 있다.(구체클래스를 통해 클래스가 가진 개념도 가지게 되므로 Method, Field, Type도 가지게 된다.)

- 메소드는 프로토타입을 가지며, 추상 메소드는 메소드의 일종이다.

- 필드는 Reference와 Primitive로 나뉜다.

- 클래스와 메소드, 필드는 가시성을 가진다.

- 클래스는 상속 가능(Inheritable)하다.

- 메소드는 재정의 가능(Overridable)하고, 오버로딩 가능(Overloadable)하다.

Posted by 이세영2
,

중간 정리

이쯤 해서 왜 "객체지향의 올바른 이해"라는 포스팅을 만들게 되었는지 그 동기를 설명하고 넘어가는 편이 전체적인 이해에 도움이 될 것이라고 생각한다.

이전 포스팅들에서 객체지향 4대 원칙이 나온 배경을 유연성의 예를 통해 설명하였다. 개인적으로 여러 책을 읽어보고 검색을 해 봤지만 이와 같은 방식으로 객체지향을 설명한 글은 없었다. 하지만 포스팅의 내용을 이해했다면 현실 세계에서 나온 개념을 프로그래밍 언어에서 차용했다고 보는 편이 더 타당하다는 점을 알 수 있을 것이다. 개인적으로 이 부분은 나름대로 확신하는 편이다. 하지만 그렇다고 해서 굳이 이미 사람들이 객체지향을 이해하고 있는 방식을 바꿔서 설명해야 할 필요가 있겠는지 하는 의문은 생길 것이다. 나는 그것이 필요하다고 봤다.


객체지향을 설명하는 일반적인 방식은 이렇다.

추상화(Abstraction)에 대해서는 공통된 속성과 행위를 추출하는 것으로 주로 설명한다. 이 설명이 틀린 것은 아니다. 하지만 공통된 속성과 행위를 추출하는 과정의 목적이 명확하지 않다. 유연성을 확보하기 위한 목적으로 설명하지 않기 때문에 추상화 과정이 필요한 이유를 설명하기가 어렵다. 그래서 어떻게든 가져다 붙이는 설명이 "속성과 행위를 중복 구현하지 않기 위해서"라고 말한다. 만약 추상화의 목적이 중복 구현의 방지, 즉 재사용이라면 일반적인 언어에서 말하는 작은 규모의 재사용, 즉 라이브러리와 무엇이 다른지를 명확히 해야 한다. 하지만 애초에 목표가 제시되지 않은 상태에서 추상화의 목적을 설명하기 때문에 그 본질적인 용도가 불명확해지는 것이다.

애초에 설명이 이렇게 시작하다보니 다른 개념들도 점차 모호해지기 시작한다. 캡슐화는 속성과 행위의 묶음이고, 이들 중에서 일부는 외부에 노출시키지 않는 것, 소위 정보 은닉을 통해 객체 내부 변경이 외부에 영향을 적게 미치도록 만든다고 말한다. 그래서 객체지향이 다른 언어보다 좋다고 이야기 한다. 캡슐화의 가장 큰 이점은 추상화를 통해 만들어진 객체(추상 객체라고 하자)가 그 고유한 타입을 가지게 되고, 이 추상화된 객체의 타입(추상 타입이라고 하자)을 통해서 여러 개별 객체가 공통으로 다루어 질 수 있음으로써 유연성이 확보된다는 점이다. 이 과정에서 캡슐화의 의미가 "공통의 속성과 행위에 타입을 부여하여 다루어질 수 있는 대상으로 만드는 것"이라는 정의를 성립시킬 수 있다. 따라서 캡슐화에서 가장 강조되어야 하는 부분은 타입의 부여이다. 하지만 캡슐화를 단순한 속성과 행위의 묶음 또는 일부 속성이나 행위를 외부에 노출시키지 않는 것이라고 정의한 덕분에 다른 개념들의 설명은 더욱 모호해진다.

상속에 대해서는 다른 객체의 속성과 행위 "만"을 가져 오는 것으로 보통 설명한다. 앞서 강조했듯이 캡슐화된 객체에 있어서 가장 중요한 것은 타입이다. 상속은 타입을 상속 받음으로써 그 타입으로 다뤄질 수 있도록 만드는 것이 핵심이다. 그래야 구체적인 타입이 아닌 상위 타입, 즉 추상 타입으로 다뤄지면서 유연성을 확보할 수 있게 된다.

다형성(Polymorphism)의 경우 역시 왜곡이 심하다. 다형성을 메소드 오버로딩과 혼돈하는 경우가 많고, 기껏해야 상위 타입으로 호출된 메소드를 하위 타입의 메소드로 대체되어 호출되는 것 정도로 말하는 경우가 대부분이다. 다형성은 하나의 타입이 서로 다른 타입으로 지칭될 수 있는 특성이다. 이 다형성은 객체지향 언어에서 상속을 통해 확보된다. 상속이라는 특성은 속성과 행위 뿐 아니라 타입도 상속시킨다. 이를 통해 상속을 받은 클래스는 자기 고유 타입 뿐 아니라 상위 타입도 가지게 된다. 이러면 가지고 있는 타입이 두 개가 되는데 이것이 다형성(multiple type)이다. 따라서 다형성이라는 단어만으로는 유연성을 이해하기 어렵다.


많은 소프트웨어 개발자들은 새로운 언어를 빨리 배우는 것이 자신의 능력과 연결된다고 생각한다. 그것 자체는 틀린 것이 아니다. 하지만 C언어와 같은 절차지향 언어를 쓰다가 Java와 같은 객체지향 언어를 배우게 되는 경우에도 이런 생각에 사로잡힌다. 특히 개발자들은 자신이 하는 분야에 대한 자부심이 크기 때문에 다른 분야 업무를 경시하는 경향이 있다. C언어와 객체지향 언어는 사용되는 분야가 많이 다르다. 언어를 빨리 배워야 뛰어난 개발자라는 생각과 다른 분야에 대한 경시가 객체지향 언어에 대한 이해 부족으로 나타난다. 빨리 배워서 기능 하나 빠르게 개발해서 보여주고 "쉽네!" 라고 빨리 말할 수 있어야 한다는 강박관념에서 언어를 배우는 것이다. 일반적인 형식의 객체지향 설명이 추구하는 방향이 딱 이런 정도다.

객체지향 언어는 애초에 프로그래밍을 시작할 단계에서부터 유연성을 염두해 두고 시작해야 하는 언어다. 프로그램을 구성해야 하는 요소(즉 객체)가 둘이 되었을 때부터 둘 간의 관계에 대해서 생각해보고, 이들의 관계가 확장될 소지가 있는지, 즉 유연성을 확보해야 할 필요가 있는지를 고려해야 한다. 또 이미 구현되어 있는 프로그램이라고 해도 서로 유사성이 있는 요소는 없는지, 그리고 유사한 요소들을 함께 다룰 수 있도록 함으로써 유연하고 간결한 프로그램으로 만들 수는 없는지를 생각해야 한다. 이런 과정에서 유연성을 확보하는 방법에 대한 일종의 청사진을 가지고 있다면 그 일이 매우 수월해질 것이다. 이것이 현실 세계에서 오케스트라 지휘자의 문제를 보여주고, 이를 객체지향 4대 원칙과 비교하면서 설명하게 된 계기이다.


이것을 통해 얻은 것은 무엇인가? 

우선 객체지향 언어를 배우는 과정에서 많이 간과되거나 잘못 설명되었던 개념들을 정확하게 정립할 수 있었다. 특히 객체지향 언어에서 "타입"이 얼마나 중요한지를 재발견할 수 있게 해주었다. 이제껏 일반적인 객체지향 도서들이나 블로그 들에서는 타입에 대해 거의 언급하지 않았거나 단편적으로만 설명했을 뿐이다. 하지만 이 포스팅을 통해서 객체가 속성과 행위의 집합이 아닌 타입과 속성과 행위의 집합으로 이해될 수 있었다. 그리고 캡슐화가 타입을 부여하는 과정이라는 점, 상속은 속성과 행위만이 아니라 타입까지도 물려 준다는 점, 마지막으로 다형성의 의미가 여러 타입을 가질 수 있는 특성이라는 점까지 이해할 수 있게 되었다.

객체지향 언어의 4대 특성이 왜 도입되었는지를 알 수 있었다. 객체지향이 실제 세계의 반영이라는 점은 많은 글들에서 설명하고 있다. 그러나 단순히 실세계를 반영하기 위해서 4대 특성을 도입했다는 것은 설득력이 부족하다. 앞서서도 이야기 했듯이 객체지향의 개념은 절차지향에 비해 이해하기가 어렵다. 굳이 더 어려운 개념을 도입해서 언어를 개발하고, 그렇게 개발된 언어가 지금처럼 널리 쓰일 수 있게 된 이유가 있어야만 한다. 유연성이라는 목적은 그 이유에 부합한다. 그리고 이해하기 어려운 개념임에도 불구하고 유연성이라는 목적을 명확히 이해하고 사용하면 객체지향을 빠르게 익히는데 많은 도움이 된다.

객체지향 언어의 목적이 유연성이라는 점을 이해하고 있고, 이 목적에 맞게 4대 특성을 이해하고 있으면 이제 디자인 패턴과 같은 설계적인 특성에도 빨리 다가갈 수 있다. GoF의 디자인 패턴 23가지 중 약 10가지는 그 목적이 유연성 확보에 있고, 앞서 설명한 유연성 확보 방식을 기반으로 구현된다. 나머지 패턴들 중 일부는 일부 특수한 목적을 위한 것이거나 객체지향 4대 특성의 일부를 활용한 것들이다.


Posted by 이세영2
,

이제 현실세계에서 객체지향의 세계로 넘어갈 차례다. 아직까지도 왜 객체지향 언어가 다른 언어에 비해 더 유연한 언어인가에 대한 정확한 답을 얻지는 못했을 것이다. 객체지향 언어가 유연한 이유를 간단히 말하자면 현실세계의 유연성을 그대로 프로그래밍 언어에 접목시켰기 때문이다. 객체지향 언어가 가진 고유한 특성들을 이제부터 이야기 할텐데, 이 특성들과 유연성은 완벽하게 매치된다. 이제껏 객체지향의 특성들을 일단 공부하고 외우고 나서 어느 정도 객체지향 개념을 이해하게 된 사람들이나 이제 막 객체지향 언어에 발을 들여 놓은 사람들 모두 현실 세계의 유연성을 일단 이해하고 객체지향 개념을 접하게 된다면 그 개념이 왜 필요한지, 그리고 그것이 어떻게 유연성을 만들어 내는지를 정확히 이해할 수 있게 될 것이다.


객체지향 4대 특성

객체지향 언어는 다른 언어들과 차별화된 특성을 가지고 있다. 그리고 이러한 특성들 중에서 중요한 것을을 객체지향 4대 특성이라고 부른다. 


추상화(Abstraction)

유연성을 확보하는 각 단계 중에서 공통점을 추출하고 차이점을 감추는 작업에 대해서 이야기 했다. 이 단계에서 했던 일을 요약하자면 다음과 같다. 우리는 여러 요구사항들로부터

    1. 공통점을 추출하였고,

    2. 불필요한 공통점을 제거하였다.

오케스트라의 비유에서 우리는 연주한다는 공통점을 추출해냈고, 같은 국적이라는 공통점을 추출해 냈지만 국적은 연주와 상관없는 정보이기 때문에 제거하였다. 객체지향 언어에서 추상화(Abstraction)라고 부르는 특성은 우리가 유연성을 확보하기 위해 했던 과정과 동일하다. 추상화라는 말은 구체적인 것을 제거한다는 말이다. 이것은 여러 요구사항에 대한 공통 요약본을 만드는 과정이다. 따라서 무턱대고 추상화를 할 수는 없다. 우선 제거되는 대상은 공통되지 않은 것이어야 한다. 그리고 우리가 만드는 것은 요약본이기 때문에 공통점들 중에서도 목적한 바와 맞지 않는 것들은 다시 제거해야 한다. 이러한 과정을 통해서 문제와 밀접한 관계가 있고, 모든 요구사항들이 공통적으로 가지고 있는 부분들이 추출된다.



추상화(Abstraction)의 과정



캡슐화(Encapsulation)

캡슐화는 "캡슐로 만들다"라는 의미이다. 우리는 이미 "대상화" 한다는 개념을 통해 이 특성을 익혔다. 캡슐화의 의미는 크게 3가지로 나눌 수 있다.

1. 공통점들의 묶음에 명칭을 부여한다. 이 "묶음"은 다룰 수 있는 "대상"이 된다.

2. 속성과 행위를 하나로 묶는다.

3. 실제 구현 내용 일부를 은닉한다.


일반적으로 캡슐화라고 하면 2번과 3번을 떠올린다. 그리고 그것 만으로도 어느 정도 이해하는데 도움이 되기는 한다. 하지만 정작 중요한 것은 1번이다.

우리는 유연성에 대한 설명 중 이상화 시인의 시 "꽃"에 대한 이야기를 했었다. 2번처럼 속성과 행위를 하나로 묶었다고 하더라도 거기에 "명칭"이 부여되지 않으면 다룰 수 있는 "대상"이 되지 못한다. 일반적으로 객체지향에서 이 "명칭" 이라는 것은 타입(Type)이라고 말하고, "대상"은 객체를 말한다. 즉, 다룰 수 있는 대상인 객체가 되려면 타입을 가져야 한다는 말이다. 아래 코드는 이 캡슐화의 예이다.


class Musician{ // 명칭을 부여했다.

    public void play(){ /* nothing to do */ }

    public void stop(){ /* nothing to do */ }

} 

Musician은 이제 우리가 추출한 공통점들의 명칭이 된다. 그리고 중괄호({})의 시작과 끝에 의해서 그 명칭이 의미하는 내용이 묶여진다. 중괄호를 통해 공통점들이 묶이고 Musician이라는 명칭이 부여된 것이다. 명칭, 즉 타입(Type)이 중요한 이유는 이후 다른 객체지향 특성을 알고 나면 매우 명확해지게 될 것이다. 일단 아래의 코드를 보고 한가지만 이야기 하고 넘어가겠다.

Musician musician = new Musician();

위의 코드 한 줄은 다음과 같은 의미를 지닌다.

    1. 등호( = ) 오른쪽은 Musician 타입의 객체를 생성하는 코드이다. 

    2. 등호 왼쪽은 musician이라는 참조를 선언하는데 그 타입은 Musician 타입이다.

    3. 이 한 줄의 코드를 통해 Musician 타입의 객체가 생성되고, 이 객체는 Musician 타입의 참조 객체를 통해 다루어진다.

객체를 생성하고 객체와 타입의 참조를 통해 객체를 다룬다는 말이다. 같은 타입이니 당연히 객체를 다루는 것은 문제가 없다. 이제 다음에 나올 개념들을 이해할 차례다.


상속(Inheritance)

이제 우리는 공통점에 대해 해야 할 일들을 마쳤다. 그러면 이제 다양한 요구사항들의 차이점들에 집중할 때가 되었다. 오케스트라의 비유에서 각 악기 연주자들은 서로 다른 악기를 다룬다. 따라서 이들 악기로부터 나오는 소리는 모두 다 다를 것이다. 하지만 이들은 모두 연주한다(play()), 멈춘다(stop())는 공통점을 가지고 있어야 한다. 특히 유연성의 관점에서 보면 각 악기 연주자들은 연주한다와 멈춘다를 통해서 다루어져야 한다.

이것은 이미 만들어져 있는 공통점을 개별 악기 연주자가 이용할 수 있어야 함을 의미한다. 객체지향 언어에서 이미 만들어져 있는 타입을 다른 객체가 물려 받아 이용하는 것을 상속(Inheritance)이라고 한다.

상속을 통해 다른 객체로부터 물려 받는 것은 세가지가 있다.

    1. 타입(Type)

    2. 필드(field, = 속성 = 변수)

    3. 메소드(method, = 행위, 함수)

이 중에서 가장 중요한 것을 고르라면 당연히 타입이다. 일반적으로 많은 객체지향 관련 자료들이 놓치는 것이 상속을 통해 타입을 상속받는다는 사실이다. 이 타입 상속에 대한 이해가 없이는 객체지향을 온전히 이해할 수 없다. 타입 상속은 다음에 나올 다형성의 중요한 기반이 된다.

일단 상속이 어떤 방식으로 이루어지는지부터 알아보자. 우리는 공통점을 추출해 낸 Musician이라는 타입을 이미 가지고 있다. 그리고 이제 우리는 구체적인 요구사항, 즉 개별 악기 연주자들을 정의해야 한다. 그리고 개별 악기 연주자는 이미 정의된 Musician으로도 다루어 질 수 있어야 한다. 따라서 Musician이라는 타입을 "상속" 받아야만 한다. 일단 하나만 해보도록 하자.

class FluteMusician extends Musician{

    public void play(){

        System.out.println("playing flute");

    }

    public void stop(){

        System.out.println("stop flute");

    }

} 

FluteMusician 클래스는 Musician 클래스를 상속 받는다. 이를 통해 FluteMusician 클래스는 FluteMusician이라는 자신의 타입도 가지면서 동시에 Musician이라는 타입도 가지게 된다. 또한 Musician 클래스가 가지고 있던 공통점들, 즉 연주하다(play())와 멈추다(stop()) 역시 Musician 클래스로부터 상속을 받는다.(예제에서는 일단 이들 공통점들을 "재정의" 하고 있다.)

여기서 주목할 것은 타입과 함께 공통점들을 물려 받는 것이 왜 중요한가 하는 부분이다. 


다형성(Polymorphism)

다형성은 객체지향의 꽃이라고 불린다. 다형성이란 다(많은) + 형(타입) + 성(특성)이 합쳐진 말로, 하나의 대상이 여러 개의 타입을 가진다는 것을 의미한다. 하나의 대상이 여러개의 타입을 가진다는 것이 무슨 의미일까?

우리는 이미 다형성을 만들어내는 기법을 알고 있다. 바로 상속이다. 상속은 상속을 받는 클래스에게 자신의 타입과 필드와 메소드를 전달해 주는 역할을 한다. 앞서서 이 세가지 중에 가장 중요한 것이 타입이라고 말했다. 이 타입을 물려 줌으로써 물려 받은 클래스는 다형성을 가진 클래스가 되기 때문이다.

위의 예제에서 FluteMusician은 자기 자신의 타입을 가진다. 그와 함께 Musician을 상속 받음으로써 Musician 타입으로도 취급이 될 수 있다. 아래 코드를 보자.

FluteMusician fm = new FluteMusician();

위의 코드는 FluteMusician 클래스를 객체화 하고 FluteMusician 타입으로 다루겠다는 의미를 지닌다. 아래 코드를 한 번 더 보자.

Musician m = new FluteMusician();

위의 코드는 똑같이 FluteMisician 클래스를 객체화 하지만 Musician 타입으로 다루겠다는 것을 의미한다. 이처럼 똑같은 객체가 여러 타입으로 취급될 수 있는 특성이 다형성이다.


그렇다면 왜 다형성이 객체지향의 꽃이라고 불리는가?

우리는 유연성에 대한 예로부터 Musician 객체를 추출해냈다. 그리고 각각의 클래스가 가진 차이점을 나타내는 클래스들 중에서 FluteMusician이라는 클래스를 만들어 보았다. 이 FluteMusician 클래스는 Musician 클래스를 상속 받도록 되어 있다. 이제 하프 연주자, 피아노 연주자도 같은 방식으로 만들어 볼 수 있다.

class HarpMusician extends Musician{

    public void play(){

        System.out.println("playing harp");

    }

    public void stop(){

        System.out.println("stop harp");

    }

}

class PianoMusician extends Musician{

    public void play(){

        System.out.println("playing piano");

    }

    public void stop(){

        System.out.println("stop piano");

    }

}

이제 3명의 연주자로 구성된 오케스트라(좀 빈약 하지만)를 지휘한다고 생각해보자. 만약 다형성이 없었다면, 즉 FluteMusician은 오직 FluteMusician이라는 타입만 가질 수 있고, Musician 타입을 가질 수 없다면, 그리고 다른 클래스들도 이와 같다면 연주는 다음과 같이 이루어질 수 밖에 없다.

public void playAll(){

    FluteMusician fm = new FluteMusician();

    HarpMusician hm = new HarpMusician();

    PianoMusician pm = new PianoMusician();

    // ......

    fm.play();

    hm.play();

    pm.play();

    // ......

    fm.stop();

    hm.stop();

    pm.stop();

    // ......

}

개별 악기 연주자들은 서로 공통점으로 묶어 다룰 수 없다. 다형성이 없을 경우 각각의 클래스는 자기 고유 타입 한가지만 가질 수 있기 때문이다. 따라서 각각의 클래스를 통해 만들어진 객체들은 한번에 취급될 수 없고 각각 다뤄질 수 밖에 없다. 마치 플루트 연주하세요 / 하프 연주하세요 / 피아노 연주하세요 라고 지휘자가 따로따로 외치는 것과 같다. 객체지향에서 다형성이 없다는 것은 현실 세계에서는 마치 개별 연주자들을 공통된 명칭, 즉 연주자라고 부를 수 없는 것과 같다. 플루트 연주자는 현실 세계에서 플루트 연주자이면서 동시에 그냥 연주자라고 불릴 수 도 있다. 하프 연주자도 하프 연주자로 불릴 수 있지만 그냥 연주자라고 불릴 수도 있다. 따라서 현실 세계에서는 연주자 여러분 연주하세요라고 말하면 자연스럽게 동시에 모든 악기 연주자들이 연주를 시작할 수 있다.


객체지향에서는 다형성을 이용하여 현실 세계에서의 동시 연주 효과를 낼 수 있다. 다형성을 통해 개별 연주자 객체들은 Musician이라는 공통 타입을 통해 다루어질 수 있다.

public void playAll(){

    List<Musician> musicians = new ArrayList<Musician>();

    musicians.add(new FluteMusician());

    musicians.add(new HarpMusician());

    musicians.add(new PianoMusician());

    // ......

    musicians.forEach(each -> each.play());

    musicians.forEach(each -> each.stop());

}


개별 연주자들은 각자 고유한 타입을 가지면서도 Musician이라는 타입으로 다뤄질 수 있다. 따라서 Musician 타입 객체를 넣을 수 있는 musicians라는 List에 함께 집어 넣을 수 있다. 그리고 forEach와 같은 순회 메소드를 이용하여 Musician 클래스가 가진 play() 메소드나 stop() 메소드를 동시에 호출함으로써 동시 연주와 같은 효과를 낸다.


유연성과 객체지향 특성

자 이제 다시 한번 정리를 해보자. 우리는 현실세계에서 여러가지 방식으로 유연성을 확보하고 있다. 이미 익숙한 방식을 재활용하는 것은 훌륭한 전략이다. 따라서 그리고 객체지향 언어는 현실 세계에서의 유연성 전략을 그대로 따르고 있다.

현실 세계에서의 유연성은 4가지 단계로 나뉘어진다. 그리고 이 단계들은 객체지향의 4대 특성과 1:1로 매칭된다.

일단 다루어야 할 여러 요구사항의 공통점을 찾고 문제 해결과 관계가 없는 공통점들을 제거한다. 이 과정을 객체지향 언어에서는 추상화(Abstraction)라고 한다.

그런 다음에는 공통점을 묶어 대상화 한다. 객체지향에서는 이를 캡슐화(Encapsulation) 라고 한다.

이 다음에는 개별적인 차이점들을 구현해 주어야 한다. 이 때 이미 뽑아 놓은 공통점은 모든 차이점들에게도 포함을 시켜 줘야 하는데, 이 과정을 객체지향에서는 상속(Inheritance)이라고 한다.

이제 차이점 대신 공통점을 통해 대상을 다루어야 한다. 공통점을 통해 대상을 다루려면 각각의 요구사항들이 공통점을 통해 다뤄 질 수 있어야 하는데, 객체지향에서는 이를 다형성(Polymorphism)을 통해 지원한다.

Posted by 이세영2
,

소프트웨어 분야에서 유연성은 다양한 요구사항을 수용할 수 있는 소프트웨어의 구조적 특성이라고 정의할 수 있다. 소프트웨어는 다른 어떤 컴퓨팅 요소보다도 유연하다. 그리고 잘 만들어진 소프트웨어일 수록 더 유연하게 요구사항을 수용할 수 있다. 소프트웨어의 적용 분야가 확대되고, 소프트웨어 개발 업체들 간에 경쟁이 치열해지고, 더 많은 사람들이 소프트웨어를 사용하게 되고, 더 많은 기기들이 연결되고 더 많은 정보를 수집하게 되면서 유연성에 대한 요구는 점점 더 증가하고 있다. 이러한 요구사항을 반영하기 위해서 소프트웨어는 유연성을 다른 어느 요소보다도 높은 가치로 가지고 갈 수 밖에 없다. 이것이 현대에 객체지향 언어가 대두된 이유이다.

그렇다면 객체지향 언어는 어떤 방식으로 더욱 유연한 구조를 가질 수 있게 되었을까? 여기에 대한 답 역시 현실에서 찾아볼 수 있다.


어린 왕자의 유연성 : 양이 든 상자

소프트웨어의 유연성을 떠올리면 항상 어린 왕자의 일화가 떠오른다. 어린 왕자는 사막에 불시착한 조종사(화자)에게 양을 그려줄 것을 부탁한다. 조종사는 양을 그려본 적이 없기 때문에 거친 솜씨로 이런 저런 양을 그려 준다. 하지만 어린 왕자는 어떤 것은 병들어 보인다, 어떤 것은 숫양이다, 어떤 것은 너무 늙어 보인다는 이유로 퇴짜를 놓는다. 그러면서도 계속해서 양을 다시 그려달라고 말한다.(이쯤에서는 까다로운 요구사항을 늘어놓는 고객이 떠오르게 된다.) 그러자 조종사는 아무렇게나 쓱쓱 그린 상자를 주면서 네가 갖고 싶어하는 양은 그 안에 들어 있다고 말한다. 그러자 어린 왕자의 얼굴은 환하게 밝아진다. 어린 왕자가 어떤 양을 상상하든 그 양은 그 상자 안에 들어 있다. 그래서 어린 왕자는 상자에 만족하게 된다.

우리가 현실세계에서 말하는 유연성이란 이런 상자와도 같다. 구체적인 사항들을 세세하게 늘어 놓으면 정확하긴 해도 다른 것으로 대체하기는 힘들다. 좀 엉성하고 덜 구체적인 것은 이리저리 가져다 붙이면 이것도 되고 저것도 된다. 어린 왕자의 상자는 그런 엉성하고 덜 구체적인 것의 결정판 정도라고 할 수 있다. 엄밀히 말해서 상자는 양과는 너무 동떨어져 있다고 생각될 정도로 매우 추상적이다. 하지만 그렇기 때문에 어린 왕자는 그 상자에서 자신이 원하는 양의 모습을 마음껏 상상해 볼 수 있었다.

이와 비슷한 예를 하나 더 들어보자. 수잔 서덜랜드는 그의 저서 "밈"에서 다음과 같은 타로 카드 점괘를 이야기 한다.

"당신은 남들의 애정과 인정을 받아야만 하는 타입이면서도 스스로에 대해서는 엄격하네요. 겉으로는 절도 있고 자신을 잘 통제하는 것처럼 보이지만, 속으로는 걱정이 많고 불안감도 많이 느끼고요. 가끔은 내가 제대로 된 결정을 내렸을까 하고 심각하게 고민하는군요......"

이 점괘는 사실 트릭인데 이는 "바넘 효과"라는 것을 이용한 것이다. 바넘 효과는 거의 누구나 스스로에 대해서는 참이라고 생각하지만 남에 대해서는 그렇게 생각하지 않는 진술을 던지는 것이다. 이것은 심리적인 유연성에 해당한다. 즉, 대부분의 사람들이 자신에게 맞다고 생각하는 점괘를 말해 줌으로써 그 점괘를 믿도록 만든다. 이러한 점괘에서는 당연히 매우 구체적인 날짜나 시간, 구체적인 명칭이나 사건들에 대해서는 언급을 피하는 것이 상책이다. 그래야만 더 많은 사람들에게 들어 맞는 유연한 점괘가 될 것이기 때문이다.


율리우스 카이사르의 유연성 : 추천 편지


율리우스 카이사르는 기원전 1세기에 살았던 로마의 지도자이다. 율리우스 카이사르는 인물 사전에 보면 로마의 정치가, 장군, 작가라고 나온다. 대단히 다재다능한 사람으로써 생애 전체가 유연성의 표본이 아닌가 싶기도 하다. 그가 지휘해서 이긴 전투들은 대부분 너무 독창적이어서 안타깝게도 사관학교 교재로 쓰이지 못할 정도이고(사관학교에서 교육을 하려면 어느 정도 정형화되고 누가 하더라도 같은 효과를 낼 수 있는 전투 방식이어야 하는데 카이사르의 그것은 너무나도 창의적이었다는 의미이다.), 이런 능력은 여자를 홀리는데도 사용되어 유명한 난봉꾼으로 통하기도 했다.

어쨌든 카이사르의 이러한 재능의 원천은 철저히 합리적인 방식을 통해서 얻어진 것으로 생각된다. 그는 발명에도 일가견이 있었는데, 긴 두루마리에 글을 적고 읽었던 당시에 종이를 낱장으로 잘라 한쪽 면을 이어 붙이는 "책"을 발명하였다. 이것은 두루마리가 이전에 글을 읽었던 위치를 알기에는 적합하지만 원하는 글이 적힌 임의의 위치를 빠르게 찾는데는 불편하다는 점에 착안한 것이다. 또한 당시까지 사용되던 부정확한 로마력을 대신하여 율리우스력이라는 당시로서는 매우 정확한 달력을 사용하도록 했다. 이 달력은 기원전 1세기에 사용되기 시작한 이후로 1582년 그레고리력으로 바뀌기 전까지 사용될 정도로 매우 정확했다.

이런 합리적인 사고 방식은 그가 권력을 유지하기 위해서 지방의 선거에 자신이 원하는 인물들을 추천한 방식에도 나타난다. 카이사르는 다음과 같은 문장이 쓰인 편지들을 미리 만들어 두었다.

"카이사르는 선거구 (       )의 유권자 여러분이 던지는 표로써 후보자 (     )가 그들이 바라는 관직에 당선될 수 있기를 희망한다."

그리고 필요한 시기에 이 편지에다 선거구 이름과 후보자 이름만 적어 넣음으로써 필요한 모든 선거구에서 자신이 원하는 후보를 지지해 주도록 추천할 수 있었다. 카이사르의 합리적인 성격 덕분에 매우 유연성 있는 추천 편지가 만들어지게 된 것이다.

이러한 유연성의 예는 현대에도 자주 찾아 볼 수 있다. 이 글을 읽고 있는 사람들 중에서 이미 결혼을 한 사람이 있다면 결혼을 위해서 청첩장을 만들어 전달했을 것이다. 그리고 그 청첩장의 내용은 그 청첩장을 받는 사람들마다 모두 달랐을 것이다...... 라고 짐작한다면 아무래도 유연성이 떨어진 생각이 아닐 수 없다. 거의 모든 사람들은 청첩장의 내용물은 모두 같고, 단지 전달할 상대에 따라서 겉포장에 이름만 다르게 적어 보냈을 것이다. 이것이 인간미는 조금 떨어질지 모르지만 바쁜 현대인들에게는 상당히 합리적인 선택이라고 할 수 있다.


어린왕자의 상자, 유연한 점괘, 카이사르의 추천 편지, 청첩장의 예를 통해서 우리가 현실 세계에서 유연성이라는 것을 어떻게 이용하고 있는지 확인해 볼 수 있었다. 일단 여러 요구사항들(어린왕자가 원하는 그 양, 점을 보러 오는 다양한 사람들, 다양한 선거구에 다양한 후보)을 모두 만족시키려면 모든 요구사항이 포함하는 공통된 부분들을 먼저 알아야 한다. 그러기 위해서 서로 다른 점들을 제거하고 공통된 부분을 모아 필요한 대상을 미리 만들어 둔다. 그리고 나서 개별적인 요구사항들에 대해서는 차이점만 조금씩 더해서 대응한다. 이렇게 함으로써 여러 요구사항을 개별적으로 대응하는데 들어가는 수고를 최대한 덜어낼 수 있다. 요컨데 똑같이 할 수 있는 부분을 뽑아 내어 한번만 하고 다른 부분들에 대해서는 최소한으로 대응하는 방식이다.


유연성 확보 전략 : 오케스트라의 비유

이제 조금 더 객체지향에 가까운 유연성 비유를 이야기 해보도록 하겠다. 이번에는 오케스트라에 비유해 보도록 하겠다. 오케스트라는 매우 다양한 악기를 연주하는 연주자들과 지휘자가 함께 음악을 연주한다. 악기는 바이올린, 피아노, 플루트, 하프, 비올라, 바순, 트럼펫, 트럼본, 첼로, 콘트라베 등 매우 다양하다.


이제 우리는 오케스트라 지휘자의 행동을 유추해 보자. 일반적으로 지휘자는 특별히 말을 하지 않고 지휘봉을 이용하겠지만 목적한 바를 명확하게 보여주기 위해서 지휘자가 말로 지휘를 한다고 생각해 보겠다. 지휘자는 이제 연주가 시작되었으면 좋겠다고 생각한다. 그래서 이제 각각의 연주자들에게 연주를 부탁해야 한다. 이 상황에서 지휘자가 이렇게 이야기한다.

"바이올린 연주하세요. 피아노 연주하세요. 플루트 연주하세요. 하프 연주하세요......"

이렇게 각각의 연주자들에게 개별적으로 연주를 부탁한다면 어떻게 되겠는가? 일단 연주를 시작하는 시점이 다르기 때문에 아름다운 연주가 되지도 않을 뿐더러 연주를 다 시작하게 될 때까지 시간이 매우 많이 걸리게 될 것이다. 이것은 일반적이고 합리적인 방식이라고 할 수 없다.

그러면 이제 다음의 방법을 생각해 보자.


이런 난감한? 상황을 타개하기 위해서 지휘자는 기지를 발휘한다. 지휘자는 각각의 악기를 다루는 사람들을 한꺼번에 다룰 수 있는 방법을 모색한다. 바이올린을 연주하는 연주자, 피아노를 연주하는 연주자, 플루트를 연주하는 연주자...... 이들은 모두 다른 악기를 연주하지만 연주자라는 공통점을 가지고 있다. 따라서 각각의 악기 이름으로 대응하기 보다는 공통된 부분, 즉 연주자라는 호칭으로 연주를 부탁하면 된다.

"연주자 여러분, 연주하세요."

이렇게 되면 각 연주자들은 동시에 연주를 시작할 수 있고, 지휘자는 단 한번만 요청하면 모든 악기가 연주 되도록 할 수 있다.

이 과정에서 지휘자는 매우 구체적인 대상, 즉 바이올린 연주자, 피아노 연주자 등을 대신하여 조금은 추상적인 대상, 즉 연주자라는 개념을 만들고 활용하였다. 이는 앞서 살펴본 현실 세계의 예들을 대표하는 방식이다. 실제적이면서 개별적인 대상 대신에 공통적이고 조금은 추상적인 개념을 대상화하고 이를 이용함으로써 개별적으로 대응하는 방식에 비해 훨씬 합리적이고 손쉽게 요청을 전달할 수 있었다.


우리는 이제 현실 세계에서 서로 다른 다양한 요구사항들을 어떤 방식으로 합리적이고 유연하게 대처하는지를 알게 되었다. 여러가지 요구사항들을 수집(각 악기 연주자들)하고, 구체적인 요구사항에 비해 다소 추상적인 공통점(연주자)을 찾아내고, 이를 대상화(연주자라고 부를 수 있도록)하고, 이들 공통점에 차이점을 조금 더하고(어떤 연주자는 바이올린을 연주한다), 공통점을 중심으로 대상을 다룬다(연주자 여러분 연주하세요). 이런 방식으로 유연성을 확보하는 것은 조금만 관심있게 찾아보면 늘상 있는 있는 일이라는 점도 알 수 있다. 동사무소에 준비된 각종 서류들은 이름과 서명란, 주소 등의 몇가지 구체적인 정보만 놔 둔체 이미 작성되어 있다. 여기에 자기의 정보만 적어 넣으면 곧바로 훌륭한 신청서가 된다. 보험을 가입할 때에도 약관은 이미 작성되어 있고, 가입자가 개인 정보를 기록하고 사인만 하면 가입이 완료된다. 이런 예를 더 찾아보자면 무수히 많을 것이다.

그러면 이제 우리가 현실 세계에서 유연성을 확보하는 과정을 단계별로 살펴볼 시간이다.


공통점을 추출한다.( = 차이점을 감춘다.)

현실 세계에는 마치 아날로그와도 같은 수준의 서로 다른 요구사항들이 있다. 아날로그 수준이라는 말은 요구사항이 하나하나 떨어져 있지 않고 서로 연속적으로 이어진 것처럼 다양하다는 의미이다. 연속적으로 이어진 요구사항들은 그 사이사이에 또 무수히 많고 다양한 요구사항들을 품고 있다. 이렇게 생각해야 하는 이유는 이미 밝혀진 요구사항들보다도 더 많은 요구사항이 앞으로 생겨날 수 있기 때문이다. 사람들은 종종 한가지를 보면 다른 한 가지를 떠올린다. 요구사항 하나는 그와 유사한 생각을 만들어 내고 이것은 다시 구체적인 요구사항이 되어 돌아오기도 한다. 소프트웨어 분야에서 유독 웹 개발자들이 힘들어지는 이유가 바로 이것 때문이다. 소프트웨어 시스템 내부가 어떻게 동작하는지는 쉽게 알 수 없지만 브라우저 상에서 표현되는 그림이나 글자는 쉽게 눈에 보이고, 눈에 보이는 순간 생각을 하게 되고, 생각이 다시 요구사항으로 돌아 오면서 이미 만들어 놓은 것을 고치게 하기 때문이다.

공통점을 추출하는 과정은 이 아날로그 수준의 서로 다른 요구사항들을 모두 수용할 수 있는 공통된 특징을 모으는 과정이다. 이 과정은 당연히 서로 다른 부분은 제거한다. 오케스트라의 비유에서 각 악기 연주자들은 서로 다른 악기를 가지고 있다. 서로 다른 악기를 가지고 있다는 점을 포함하고 있으면 모든 연주자들을 지칭할 수 있는 공통점을 만들어 낼 수 없다. 따라서 개별적인 악기들은 공통점에서 제거한다.

이와 함께 제거 되어야 하는 것은 또 있다. 각 악기 연주자들은 서로 다른 개인이기도 하다. 거주하는 곳이 다르고, 식성이 다르고, 평소 즐겨 입는 옷도 다르다. 이름, 전화번호, 주민번호, 나이, 헤어스타일 등 다른 점을 찾아보자면 무수하게 많다. 또 공통점도 있을 수 있다. 모두다 한국 사람일 수도 있고, 모두 다 악보를 볼 두 눈을 가지고 있을 수 있다. 이렇게 나열하다 보면 공통점과 차이점은 무수하게 많아지게 될 것이다. 그래서 공통점만 모으는 과정에서 너무 많은 공통점이 모일 수 있다. 그렇기 때문에 공통점을 추출하는 과정에서는 목적에 맞지 않는 불필요한 공통점은 제거되어야 한다. 오케스트라 지휘자는 현실 세계에서 연주자들과 친하게 지내기 위해서 이런 저런 정보들을 알아야 할 수도 있다. 하지만 지금은 지휘에 집중해야 할 때이다. 따라서 지휘하는데 꼭 필요한 공통점만을 추출해 내면 된다. 국적이나 악보를 볼 수 있는 눈 따위는 이 기준에 의해서 제거 되어야 한다.

이처럼 차이점들을 제거하고, 공통된 부분들 중에서 우리의 목적과 상관이 없는 정보들 역시 제거되고 나면 이제 공통점만 모은 어떤 것이 된다.


"대상화" 한다.

어떤 공통점이 모였을까? 우리는 연주를 할 것이기 때문에 각 악기 연주자들은 연주를 할 수 있어야 한다. 그리고 필요한 시점에 연주를 멈춰 줄 줄도 알아야 한다. 더 많은 공통점이 필요할 수도 있지만 이 정도만 모였다고 가정하자. 

그러면 이제 "대상화"를 할 시간이다. 대상화란 모아 놓은 공통점을 다룰 수 있게 만드는 과정이다.


(이상화)

  내가 그의 이름을 불러주기 전에는 
  그는 다만 
  하나의 몸짓에 지나지 않았다


 
내가 그의 이름을 불러주었을 때 
  그는 나에게로 와서 
  꽃이 되었다

  ......
 
 
우리들은 모두 
  무엇이 되고 싶다

 
나는 너에게 너는 나에게 
  잊혀지지 않는 하나의 의미가 되고 싶다
.


이 대상화를 떠올리면 항상 이상화 시인의 꽃이라는 시가 떠오른다. 이 시의 아름다움도 좋지만 대상화라는 과정이 어떤 것인지를 이해하는데도 도움이 된다. 우리가 어떤 대상에 대한 개념을 모았다고 하자. 하지만 이것에 정확한 명칭을 부여하여 다룰 수 있도록 만들기 전에는 그것은 "하나의 몸짓"에 지나지 않는다. 하지만 공통점들을 명료하게 만들고 명칭을 부여하면 그것은 "꽃"이 된다. 오케스트라의 비유에서 "꽃"은 바로 "연주자"라는 개념이다. 이 연주자라는 개념은 구체적이지 않고 추상적인 개념이다. 따라서 구체적인 대상을 현실 세계에서 찾을 수는 없다. 하지만 반대로 연주자라는 개념을 다룰 수는 있다. 연주자는 연주를 할 수 있어야 해, 연주자는 필요할 때 연주를 멈출 수 있어야 해 와 같은 방식으로 다소 추상적인 방식이긴 하지만 다룰 수 있는 대상이다. "대상화"의 개념을 명쾌하게 이해하지 못하겠다면 일단 "명칭"을 부여했다는 정도만이라도 이해하면 되겠다.


차이점을 더한다.

이제 남은 것은 차이점이다. 차이점은 공통점을 추출하는 과정에서 남은 것들이다. 그렇다고 남은 모든 것들은 아니다. 우리의 목적은 오케스트라를 지휘하는 것이다. 차이점을 더하는 과정도 목적에 맞는 것에만 해당한다. 여기서 각 악기 연주자들의 개인정보나 취향은 제거된다. 대신 오케스트라의 목적에 맞게 악기는 다들 가지고 있어야 하겠다. 오케스트라 지휘에 앞서 필요한 악기들이 모두 준비 되었는지를 확인하는 과정도 필요할 것이다. 따라서 차이점에는 악기가 포함된다. 이제 우리는 연주자라는 개념에 개별적인 차이점이 더해진 개별적인 악기 연주자를 설정한다. 대략 플루트 연주자, 하프 연주자, 피아노 연주자 등이 되겠다.

이 차이점을 더하여 개별적인 악기 연주자를 만드는 과정은 한가지 암시적인 가정을 포함한다. 플루트 연주자는 매우 구체적이다. 하지만 동시에 좀 더 추상적인 개념인 연주자와 동일시 될 수도 있다. 즉 플루트 연주자는 다른 연주자들과 구분되는 플루트라는 악기 속성을 가지면서도 동시에 연주자가 가진 연주하는 속성, 연주를 멈추는 속성도 가지고 있어야 한다. 이렇게 되어야만 다음 단계가 가능해진다.

현실에서라면 이와 같은 과정을 겪어야 할 것이다. 아래는 지휘자가 플루트 연주자에게 하는 가상의 대화이다.

"플루트 연주자님, 플루트 연주자님은 다른 악기 연주자들과 다르게 플루트를 가지고 계셔야 합니다. 그리고 저는 연주를 효율적으로 하기 위해서 플루트 연주자님을 그냥 연주자라고 부를 것입니다. 연주자란 연주하세요 하면 자기가 가진 악기를 연주하고, 멈추세요 하면 연주를 멈추는 사람입니다. 이해되셨죠?"

현실 세계에서는 이미 각 악기 연주자가 연주자 개념을 알고 있기 때문에 이런 일이 일어나진 않는다.


공통점을 통해 대상을 다룬다.

이제 지휘자가 오케스트라 지휘를 시작할 시점이다. 앞서의 지휘자와 플루트 연주자와의 대화와 유사한 대화들이 각 악기 연주자들과도 오고 갔다고 가정하자. 그러면 이제 지휘자는 연주를 시작할 수 있는 상태가 된 것이다. 각 악기 연주자들은 우리가 정의한 "연주자"의 개념을 알고 있다. 따라서 각 악기 연주자들에게 개별적으로 연주를 부탁하지 않아도 된다. 단지 다음과 같이 말하기만 하면 된다.

"연주자 여러분, 연주하세요"

그러면 플루트 연주자는 자기가 가진 악기인 플루트를, 하프 연주자는 하프를, 피아노 연주자는 피아노를 연주하게 될 것이다. 이렇게 열심히 공통점을 대상화하고 이용할 수 있도록 만들어 놓고서도 각각의 악기 연주자에게 일일이 연주를 지시하는 것은 비 합리적인 일이다.


자 이제 현실세계에서의 유연성이라는 것은 무엇이고, 이것이 수많은 요구사항들을 어떻게 수용하는지 알 수 있게 되었으며, 유연성을 확보하는 전략을 단계별로 이해할 수 있게 되었을 것이다. 이제 현실 세계에서의 유연성 확보 방법이 우리가 알고자 하는 객체지향 언어와 어떻게 연관되는지 살펴볼 차례다.

Posted by 이세영2
,

현실과 소프트웨어 세계에서의 객체

우리는 현실에서는 객체의 세계, 소프트웨어 세계에서는 객체지향의 세계에서 살고 있다고 해도 과언이 아니다.

현실에서의 객체란 독립적이면서 (분리되지 않고) 하나로 취급되는 인식의 단위를 말한다. 인간은 대상을 정확히 인식하기 위해서 목적한 대상과 다른 대상들과의 개념적인 차이들을 필요로 하고, 그 차이들이 명확해지고 나면 그 대상에 대해 명칭을 부여한다. 이 과정을 통해 불분명했던 대상은 정확한 명칭을 지닌 객체가 된다. 우리의 인식 속에서 일단 객체로 자리잡은 대상은 고유한 인식적 위치를 점유하게 되고 이는 다른 대상을 판별하는데 이용됨으로써 인식의 세계가 점점 더 넓어지는데 일조하게 된다. 대상을 다른 대상들과 분리하여 인식하는 이와 같은 방식은 현실에서의 인간이 살아가는데 큰 도움을 준다.

근본적으로 프로그래밍 언어로서의 객체지향 개념은 위에서 본 현실에서의 객체에 대한 인식과 크게 다르지 않다. 프로그래밍 언어의 세계는 관념적인 세계이다. 인간은 현실을 객체 단위로 이해하고 있지만 실제 세계는 객체적인 세계가 아니다. 개와 고양이는 (관념적으로는) 같은 동물이긴 하지만 개와 고양이는 같은 개념으로 묶을 수 있는 단위가 아니다. 하지만 우리의 두뇌는 현실을 관념적으로 파악하기 때문에 거리낌 없이 개와 고양이를 묶어 동물이라고 부른다. 그리고 아이러니하게도 이렇게 비현실적인 관념으로 세계를 파악하는 방식이 전체를 분리하지 않은 체 이해하는 방식보다 거의 언제나 더 유용하다. 유용하다는 말은 더 빠르고 정확하게 이해할 수 있다는 말이다. 따라서 프로그래밍 언어에 객체지향이라는 개념이 도입된 이유는 인간이 객체를 중심으로 프로그래밍을 하는 편이 더 빠르고 정확하게 이해하면서 구현하는 것이 가능하기 때문이다. 이 점은 매우 중요한데, 프로그래밍의 도구는 오직 두뇌 뿐이고, 두뇌가 더 빠르고 정확하게 문제를 이해하는 것은 프로그래밍에 그만큼 유리하게 작용하기 때문이다.

2016년 6월 기준으로 Java, C++, Python, C#, PHP, Javascript, Ruby, Objective-C 등과 같이 굵직한 언어들 중에서 객체지향을 온전히, 혹은 상당 부분 지원하는 언어의 비중을 따져보면 약 44.5%를 차지했다. 만약 하드웨어적인 제약 사항을 만족시키기 위한 언어(C, Assembly)나 제한된 목적을 위한 언어(Cobol, Matlab, Delphi, Visual Basic 등)를 제외한다면 범용적이고 일반적인 어플리케이션의 대부분은 객체지향 언어로 만들어진다고 볼 수 있을 것이다.


프로그래밍 언어의 역사와 객체지향

우리가 현실 세계를 객체로 이해하고 있다는 점을 이해한다면 객체지향 언어가 프로그래밍 언어 영역에 도입된 시기는 매우 빨랐을 것으로 추측할 수 있다. 실제로도 그렇다. 이반 서덜랜드(1938~)는 1963년도에 "스케치패드 : 사람과 기계와의 그래픽 대화 시스템"이라는 논문을 통해 객체지향 개념을 기반으로 구현한 그래픽 프로그램을 발표한다.("누가 소프트웨어의 심장을 만들었는가") 이후 시뮬라67 언어가 만들어지고(1967), 앨런 케이에 의해 스몰 토크라는 언어가 만들어짐으로써 온전한 객체지향 언어가 탄생하게 된다. 시기적으로 보면 현대에 가장 많이 사용되고 있는 비 객체지향 언어인 C 언어가 1972년에 만들어졌으니 객체지향 언어는 완성된 비 객체지향 언어보다도 개념적으로 더 오랜 역사를 가지고 있는 셈이다.

하지만 이율배반적이게도 많은 사람들이 객체지향 언어를 접한 시기는 상대적으로 최근의 일이다. 여기에는 여러가지 이유가 있다. 첫째 객체지향 언어는 컴퓨팅 파워가 뒤쳐졌던 70년대에는 다른 언어들에 비해 상대적으로 비인기 언어였다. 낮은 하드웨어 성능을 최대한 끌어낼 수 있는 절차지향 언어가 먼저 인기를 끌었다. 둘째, 우리에게는 객체를 중심으로 현실을 이해하는 것 만큼 익숙한 이해의 방식이 하나 더 있기 때문이다. 이것은 어떤 문제(또는 일)를 시간과 인과 관계의 순서로 이해하는 방식이다. "처음엔 A였다가 B라는 이유로 C가 되었다가 결국엔 D로 끝났어"와 같은 서술 방식은 시간과 공간 속에서 살아가는 인간에게 매우 익숙한 상황 인식 및 설명의 방식이다. 이러한 방식을 프로그래밍 언어에 도입한 것이 절차지향 언어이다. 첫번째 이유로 인해 기피되던 객체지향 프로그래밍 방식이 초반부터 매우 강력한 경쟁자를 만난 셈이다.

세번째 이유는 객체지향 언어를 이해하는 것이 절차지향 언어에 비해 상대적으로 어렵다는 점이다. 객체를 중심으로 이해하는 것이 현실을 이해하는데 유용하다는 점에서 볼 때는 매우 아이러니한 일이 아닐 수 없다. 우리는 매우 자연스럽게 세계를 객체 중심으로 이해한다. 이 과정은 너무나도 자연스럽기 때문에 그 메커니즘에 대해서는 잘 인지하지 못한다. 우리는 개와 고양이를 개와 고양이라고 부르면서 구분하고 있고, 동시에 동물이라는 카테고리 안에 이 둘을 자연스럽게 포함시킨다. 또 동물과 식물을 구분하고 있으면서 동시에 생물이라는 카테고리 안에 이 둘을 포함시킨다. 이것을 그림으로 그리면 트리 구조가 될 것이다. 하지만 우리는 이러한 구분을 트리 구조를 통해서 이해하고 있지 않다. 그냥 자연스럽게 그 인식 과정을 받아들인다. 하지만 이러한 메커니즘을 모사하여 프로그래밍 언어에 도입하는 과정은 그다지 순탄하지 않다. 개와 고양이를 구분하려면 개를 개념적으로 명확하게 정의하고 고양이를 개념적으로 명확하게 정의할 수 있어야 한다. 그리고 이들 둘을 동물이라는 같은 개념으로 묶을 수 있으려면 두 동물이 가진 개념 중에서 공통점을 추출할 수 있어야 한다. 또한 동물의 특성을 개와 고양이가 가질 수 있게 하려면 상속이라는 개념을 도입해야 하고, 동물이라는 개념으로 개와 고양이를 다룰 수 있으려면 다형성이라는 개념을 도입해야 한다. 요컨데 인간의 두뇌가 현실을 이해하는데는 엄밀한 정의도 필요하지 않고, 전체적인 논리적 완결성을 추구할 필요가 없지만 이를 매우 논리적인 완결성을 요구하는 프로그래밍 언어에 도입하는 데에는 다양한 개념의 정립이 필요하다.


객체지향이 대두된 이유

그렇다면 절차지향 언어가 계속 우위에 설 수도 있었을텐데 객체지향 언어는 왜 다시 대두되었고, 현재와 같은 주류 언어가 될 수 있었을까?

기초적이고 단편적인 이유들을 먼저 설명해 보자. 첫째로 컴퓨터의 연산 능력이 좋아졌다는 점이다. 객체지향 언어가 생겨난 70년대 이후 수십년에 걸쳐 하드웨어는 비약적인 발전을 거듭해 왔다. 그리고 하드웨어적/소프트웨어적 발전으로 인하여 하드웨어는 단일한 플랫폼을 넘어서 병렬화되면서 성능은 더더욱 좋아지게 되었다. 이것은 상대적으로 연산량을 많이 필요로 하는 객체지향 언어가 활동할 수 있게 된 배경이 된다. 둘째로 절차지향 언어의 한계점이 대두되었기 때문이다. 앞서 말했듯이 절차지향 언어는 시간과 공간 상에서의 인과 관계를 중심으로 현실을 이해하는 인간의 인식 방식을 모방한 것이기 때문에 이해하기 쉽고 다루기 쉬운 프로그래밍 언어이다. 하지만 절차지향 언어로 개발된 소프트웨어는 어느 일부분을 떼어 내어 다른 소프트웨어를 만드는 것이 어렵다. 절차지향 언어가 함수라는 독립된 단위로 소프트웨어를 분할하고 있지만 각 함수는 연쇄적으로 호출되면서 동작하기 때문에 어떤 부분만을 별도로 나누어 다른 시스템에 적용시키기가 어렵다. 또한 절차 전체에서 메모리를 공유하거나, 특정 절차가 어떤 데이터와 연관되는지를 쉽게 알 수 없기 때문에 전체 동작을 이해하기 위해서는 각 절차들의 내부를 자주 들여다 봐야 되고, 이런 방식으로는 큰 소프트웨어를 작성하기가 어려워진다.

하지만 보다 근본적인 이유는 따로 있다. 1930년대 후반에 소위 "컴퓨터"라는 것이 발명된 직후, 컴퓨터의 용도는 주로 암호 해독이었다. 일련의 덧셈, 뺄셈, 곱셈, 나눗셈 등을 이용하여 인코딩된 정보를 디코딩하는 용도라고 할 수 있다. 1940년대에는 군사 분야쪽으로 용도가 넓어진다. 포탄 궤적용 표, 폭발 계산(원자폭탄 개발), 탄도 미사일 계산과 같이 군사 물리적인 용도가 주를 이루었다. 1950년대에는 FORTRAN 언어가 개발되면서 과학 연구 분야에 컴퓨터가 활용되기 시작한다. 초신성 폭발, 수치해석, 일기예보 등이 그 예이다. 1960년대에는 운영체제와 COBOL 언어가 등장함으로써 고등 제조 기술 분야와 비즈니스 분야에 소프트웨어가 활용된다. 항공기 설계(특히 엔진 설계), 우주선의 제작 및 우주선 궤도 계산 분야, 비즈니스 소프트웨어 분야에 활용되었다. 1970년대에 이르러서는 PC가 출현하고 유저 인터페이스 및 인터넷이 탄생한다. 이와 함께 PC용 소프트웨어가 개발되면서 소프트웨어는 산업 전반에 걸쳐 사용된다. 애플I이 개발된 것이 1976년의 일이다. 1980년대는 PC 시대가 활짝 열리게 된다. MS-DOS가 발명되고(1981), PC의 대중화를 주도한 맥킨토시(1984)가 발명된 것도 이 시기이다. 특히 맥킨토시는 최초의 GUI와 마우스를 채용함으로써 그래픽 기반의 소프트웨어 시대를 열었다. 1990년대는 뭐니뭐니해도 WWW, 즉 월드 와이드 웹의 시대이다. HTML과 웹 브라우저로 인하여 전 세계의 컴퓨터가 서로 정보를 주고 받을 수 있는 세상이 되었다. 이로 인하여 소프트웨어는 닫힌 공간을 위한 용도에서 연결된 공간을 위한 용도로 활용되게 된다. 2000년대는 소셜 네트워크 및 집단 지성의 시대이다. 이 시기에 구글이 기존과는 완전히 다른 개념의 검색 기술을 개발(2003)하였고, Wikipidia(2001)와 같이 집단 지성을 활용하는 시대가 되었다. Facebook(2004)은 SNS의 시대를 열었고, 아이폰(2007)은 이 연결을 통한 가치 창출을 모바일의 영역으로 확대하였다. 2010년대는 빅데이터와 인공지능의 시대이다. IBM은 왓슨(2011)을 통해 빅데이터 기반으로 훈련된 인공지능을 선보였고, 2016년에는 이세돌과 알파고의 대결을 통해 인간의 능력을 넘보는 인공지능의 시대가 시작되었다.

소프트웨어의 역사에서 가장 주목할만한 것은 소프트웨어 활용 영역이 지속적으로 확대 되었다는 점이다. 소프트웨어는 컴퓨터의 초기부터 지금까지 그 적용 분야가 계속 확대되어 왔다. 그러면 이제부터는 그 적용 분야가 점차 축소될 것인가? 인공지능 분야는 현재 제한적인 부분에서만 인간의 능력을 뛰어 넘는 수준에 머물러 있지만 앞으로 적어도 모든 분야에서 인간의 능력을 뛰어넘기 전까지는 계속 발전하게 될 것이다. 이를 위해서는 더 많은 정보가 수집되어 분석되어야 한다. 당연히 데이터는 계속 수집될 것이고, 인간의 능력을 뛰어넘는 분야는 점점 넓어질 것이다. 사람들이 가지고 있는 모든 지식과 컴퓨터 뿐만 아니라 모든 센서가 연결되고 그 데이터가 수집될 때까지 이 과정은 지속될지 모른다. 즉, 소프트웨어는 적용 분야, 크기, 연산량이 확대되는 방향으로 발전해 갈 수 밖에 없다.

이 과정에서 소프트웨어가 갖추어야 할 기능의 필요성, 즉 요구사항의 증가는 필연적이다. 요구사항은 계속 증가한다. 단순히 양만 증가하는게 아니고 적용 분야가 증가하면서 종류도 다양해진다. 또한 소프트웨어와 인간이 만나는 횟수가 증가하면서 요구사항은 점점 더 다양해지고 또 자주 바뀌게 된다. 개별적인 인간의 개성이나 변덕의 수준을 맞추기 위해서는 소프트웨어가 더 많이 바뀌고 더 자주 바뀌고 더 빨리 바뀔 수 있어야 한다. 이렇게 잘 바뀌는 특성, 즉 유연성(flexibility)이 소프트웨어의 최고 덕목이 된다. 그래서 프로그래밍 언어 중에서도 가장 유연성이 뛰어난 객체지향 언어가 새로운 강자로 떠오르게 된 것이다.


소프트웨어가 가져야 할 가치

TDD, xUnit 테스트, 애자일의 창시자로 유명한 소프트웨어 프로그래머 켄트 벡은 "켄트 벡의 구현 패턴"에서 소프트웨어가 가져야 할 가치를 유연성, 단순성, 커뮤니케이션으로 정의하였다. 커뮤니케이션은 갈수록 커져가는 소프트웨어의 규모를 맞추기 위해서는 여러 사람들의 협력이 필요하고, 이를 위해서는 소프트웨어가 자기 스스로를 설명할 수 있는 능력, 즉 커뮤니케이션 도구로서의 기능을 가져야 한다. 또한 사용자의 요구사항은 예측이 불가능하게 변화하거나 추가되기 때문에 소프트웨어는 유연성을 꼭 가져야만 한다. 이들 덕목을 갖추면서도 기능을 구현하는 가장 단순한 방법을 추구하는 단순성을 가지고 있어야 한다.

이들 중에서 소프트웨어의 유연성은 소프트웨어의 존재 이유에 해당하는 가치라고 볼 수 있다. 소프트웨어는 하드웨어와 같은 다른 요소가 감당하지 못하는 기능 변경 요구사항을 수용하기 위해서 만들어진 요소이다. 이로 인하여 소프트웨어의 생애 주기 중 대부분은 수정과 개선 기간이 된다. 로버트 L. 글래스는 (우리가 미처 알지 못한) 소프트웨어 공학의 사실과 오해라는 책을 통해서 소프트웨어의 유지보수가 보통 소프트웨어 비용의 40~80%를 차지한다고 말한다. 이와 함께 60/60 법칙을 제시하는데 소프트웨어 비용의 60%는 유지보수에 사용되고, 유지보수 비용의 60%는 소프트웨어 기능의 개선을 위해 사용된다고 한다. 이는 소프트웨어가 얼마나 자주 변경되는지, 요구사항이 얼마나 자주 바뀌는지를 말해준다. 더불어 유연성이 소프트웨어의 가치 중에서 얼마나 큰 비중을 차지하는지를 알 수 있게 해준다.

객체지향은 소프트웨어의 유연성이라는 측면에서 가장 뛰어난 접근 방식이다. 객체지향 언어는 소프트웨어에 새로운 기능을 추가하거나 기존의 기능을 변경하는 것, 또는 더 이상 필요 없는 기능을 제거하는 유지 보수 작업에 가장 유리한 언어이다. 다음 글에서는 객체지향 언어가 이 유연성을 어떻게 확보하는지를 알아보겠다.

Posted by 이세영2
,