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

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
,