주요글: 도커 시작하기
반응형
재사용을 위한 전통적인 객체지향 프로그래밍 접근법의 결점에 대한 극복

코드 재사용성 최대화-제 1 단계

코드의 재사용을 위한 방법에는 여러가지가 있다. 클래스의 상속은 그 중 한 방법이지만 연관성이 부족하므로 부분적으로 적합한 방법이라고 할 수 있다. 즉, 하나의 메소드를 재사용하기 위해서는 클래스 내의 다른 메소드와 멤버 데이터들도 상속해야 하기 때문이다. 불필요한 멤버들이 많아질수록 재사용을 위한 코드는 복잡해질 수 밖에 없다. 상속 받은 클래스의 부모 클래스에 대한 의존성은 또 다른 복잡성을 일으킨다. 부모 클래스에 가해진 변화는 자식 클래스들을 파괴한다. 부모 클래스와 자식 클래스 중 한쪽을 수정하면, 어떤 메소드가 오버라이드 되었는지 알기 어려워질 수 있으며, 오버라이드 메소드가 부모 클래스의 해당하는 메소드를 호출해야 하는지도 불분명해질 수 있다.

하나의 개념에 대한 작업을 수행하는 메소드는 자신의 부모 클래스에 의존할 수 있으므로 재사용할 수 있는 첫번째 대상이다. 이를 위하여, 고전적인 프로시져 프로그래밍처럼 코드를 클래스 인스턴스의 외부 메소드로 옮겨서 전역적으로 노출된 프로시져로 만든다. 그리고 이 프로시져의 재사용성을 높이기 위하여 코드를 정적 메소드로 만든다. 즉, 이 프로시져는 주어진 파라미터와 전역적으로 노출된 다른 프로시져만을 사용해야 하며, 로컬 변수 이외에는 사용하면 안된다. 외부 의존도를 축소시킴으로써 프로시져를 사용하는 복잡성을 감소시키고, 어디서나 재사용할 수 있는 가능성을 높인다. 물론, 코드를 재사용할 목적을 가지고 있지 않다고 해도, 이 구조는 확실히 더 명료하다.

자바에서는 클래스의 외부에 메소드가 존재할 수 없으므로, 관련된 프로시져들을 모아서 하나의 클래스 내에 전역적으로 노출된 정적 메소드로 만든다. 예를 들면, 다음과 같은 형태의 클래스를 생각해볼 수 있다.

class Polygon {
   public int getPerimeter() { ... }
   public int getArea() { ... }
}

이 코드를 다음과 같이 바꾼다.

class Polygon {
   public int getPerimeter() { return pPolygon.computePerimeter(this); }
   public int getArea() { return pPolygon.computeArea(this); }
}

그리고, pPolygon 클래스는 다음과 같다.

class pPolygon {
   static public int computePerimeter(Polygon polygon) { ... }
   static public int computeArea(Polygon polygon) { ... }
}

pPolygon 클래스는 Polygon 타입의 객체들과 관련된 프로시져들만을 반영하는 클래스이다. 이름 첫자의 p는 단지 이 클래스의 목적이 정적 프로시져들이 전역적으로 보이기 위하여 만든 그룹이라는 것을 나타낸다. 자바에서 클래스 이름이 소문자로 시작하는 것은 표준이 아니므로 pPolygon은 정상적인 클래스의 기능을 수행하기 위한 것이 아니라는 뜻도 포함된다. 즉, 객체를 만들기 위한 클래스가 아니라 단지 자바에서 사용되는 독립 요소일 뿐이다.

위의 예제에서 수행된 변화의 총체적인 효과는 클라이언트 코드가 기능의 재사용을 위하여 더 이상 Polygon 클래스를 상속하지 않아도 된다는 것이다. 그 기능은 이제 pPolygon 클래스에서 프로시져 원리로 사용될 수 있다. 클라이언트 코드는 필요한 기능성만을 사용하면서 필요 없는 기능들을 걱정할 필요가 없어진다.

이러한 새로운 프로시져 프로그래밍 스타일에서 클래스라는 단위가 유용한 목적으로 도움을 줄 수 없다는 것을 의미하는 것은 아니다. 반대로, 클래스는 그룹을 이루고 표현하는 객체의 멤버 필드들을 캡슐화하는 데 필요한 역할을 수행한다. 게다가, 복수의 인터페이스를 상속/구현함으로써 다형성을 지닐 수 있는 능력은 다음 단계에서 설명할 뛰어난 재사용성을 가능하게 해준다.

이 기법과 유사한 형태가 디자인 패턴 서적에 간단히 언급되어 있다. Strategy 패턴은 관련된 알고리즘들의 패밀리 멤버들을 공통 인터페이스 뒤에 캡슐화하여 클라이언트 코드가 그 알고리즘들을 상호교환할 수 있도록 만드는 것을 제안한다. 알고리즘은 보통 하나 또는 몇 개의 독립적인 프로시져로 코딩되므로, 이런 캡슐화는 여러 작업들을 수행하는 코드와 데이터를 포함하는 객체의 재사용을 넘어서 하나의 작업을 수행하는 프로시져의 재사용성을 강조한다.

그러나, 알고리즘을 인터페이스 뒤에 캡슐화하는 것은 알고리즘이 인터페이스를 구현한 객체로 코딩되어야 한다는 것을 암시한다. 이것은 프로시져가 여전히 내포된 객체의 데이터와 다른 메소드들과 커플링되어 있으며, 따라서, 재사용을 복잡하게 한다는 것을 의미한다. 또한 알고리즘이 필요할 때마다 그 객체를 생성해야 하며 프로그램의 성능을 저하시킨다. 디자인 패턴에서는 이 두가지 문제에 대한 해결책을 제시하고 있다. Strategy 패턴의 객체를 코딩할 때 Flyweight 패턴을 적용하면 잘 알려진 공유 인스턴스가 되며, 각 공유 객체는 액세스들 간에 상태를 유지하지 않는다. 즉, 객체는 멤버 데이터를 가지고 있지 않으므로 커플링 문제를 잘 처리할 수 있다. Flyweight-Strategy 패턴은 이 단계의 전역적으로 노출되고 상태가 없는 프로시져로 기능을 캡슐화한 기법과 매우 유사하다.

코드 재사용성 극대화-제 2 단계

클래스를 상속하는 것보다 인터페이스 파라미터 타입을 통하여 다형성의 장점을 추구하는 것이 객체지향 프로그래밍에서 재사용의 원리에 더 충실하다.

"클래스보다 인터페이스로 프로그래밍하는 것으로 재사용성을 얻을 수 있다. 메소드의 모든 파라미터가 이미 알고 있는 인터페이스에 대한 레퍼런스이고, 그 인터페이스가 전혀 알 수 없는 클래스에 의해 구현된다면, 그 메소드는 코드가 작성될 때 존재하지도 않는 클래스의 객체와도 작동할 수 있다. 기술적으로, 재사용 가능한 것은 메소드이며, 메소드로 전달되는 객체가 아니다."
이러한 사항을 1단계의 결과에 적용하면, 어떤 기능의 코드가 독립하여 전역적으로 노출된 프로시져이면, 클래스 타입의 입력 파라미터를 인터페이스 타입으로 바꿈으로서 재사용 가능성을 더 높일 수 있다. 그러면 인터페이스 타입을 구현한 어떤 클래스의 객체도 파라미터로 사용될 수 있게 된다. 따라서, 이 프로시져는 잠재적으로 다수의 객체 타입에 대하여 사용이 가능해진다. 예를 들면, 다음과 같은 전역적으로 노출된 정적 메소드를 생각해보자.

static public boolean contains(Rectangle rect, int x, int y) { ... } 

이 메소드는 주어진 사각형이 주어진 좌표를 포함하고 있는지 아닌지를 알려준다. 여기서 rect 파라미터를 클래스 타입 Rectangle에서 인터페이스 타입으로 바꾸어보자.

static public boolean contains(Rectangular rect, int x, int y) { ... }

Rectangular 인터페이스는 다음과 같다.

public interface Rectangular {
   Rectangle getBounds();
}

이제 Rectangular 인터페이스를 구현한 클래스의 객체가 pRectangular.contains() 메소드의 rect 파라미터로 전달될 것이다. 이 메소드는 전달되는 파라미터의 제약으로부터 느슨해지면서 재사용 가능성이 높아졌다.

그러나, 위의 예제에서 Rectangular 인터페이스의 getBounds() 메소드가 Rectangle을 반환한다면 실질적인 이점이 있는지 의아하게 생각될 것이다. 즉, Rectangle 객체가 전달될 것을 안다면 Rectangle 타입을 전달하는 대신에 Rectangular 인터페이스 타입을 전달하는 이유가 무엇인가하는 것이다. 이렇게 하는 가장 중요한 이유는 콜렉션을 다룰 때이다. 다음 메소드를 생각해보자.

static public boolean areAnyOverlapping(Collection rects) { ... }

위의 메소드는 주어진 콜렉션 내의 사각형 객체 중에 어느것이라도 중첩된 것이 있는지 알려주는 메소드이다. 이 메소드 내에서 콜렉션 내의 객체들을 차례로 순환할 때, 객체의 타입을 Rectangular 같은 인터페이스 타입으로 캐스트 할 수 없다면 객체의 사각좌표를 어떻게 구할 것인가. 유일한 방법은 객체를 원래의 지정된 클래스 타입으로 캐스트하는 것으로, 작동할 때 사용될 클래스 타입을 미리 메소드가 알아야하므로 그 타입의 재사용성이 제한된다. 이 단계에서 꼭 피해야만 할 점이다.

코드 재사용성 극대화-제 3 단계

2단계를 수행할 때, 주어진 클래스 타입을 대신하도록 어떤 인터페이스 타입을 선택해야 할까. 그 답은 프로시져가 파라미터로 필요로 하는 모든 것을 충분히 표현하면서도 부가적인 것들을 최소한으로 가진 것이다. 파라미터 객체가 구현할 것이 작은 인터페이스일수록, 어떤 특정한 클래스가 그 인터페이스를 구현하기에 좋고, 따라서 더 많은 수의 클래스의 객체가 파라미터가 될 수 있다. 다음과 같은 메소드를 보자.

static public boolean areOverlapping(Window window1, Window2) { ... }

두개의 (사각형인) 윈도우가 중첩되었는지 판별하는 메소드로, 이 메소드는 오직 두 파라미터의 사각좌표만을 필요로 하며, 따라서 이러한 사실을 반영하여 파라미터의 타입을 줄이는 것이 좋다.

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) { ... }

위의 코드는 앞의 Window 타입의 객체가 Rectangular 인터페이스를 구현할 수 있다는 것을 가정한다. 이제 모든 사각형 객체들에 대하여 첫번째 메소드에 포함된 기능을 재사용할 수 있다.

파라미터로부터 필요한 것들이 충분히 지정된 유용한 인터페이스가 불필요한 메소드들을 너무 많이 가지고 있는 경우를 경험한적이 있을 것이다. 이 경우에는 이런 모순에 직면한 다른 메소드에 의해 재사용될 수 있도록 전역 이름공간에 새로운 인터페이스를 정의하면 된다.

하나의 프로시져에 대하여 단 하나의 파라미터로부터 필요한 것을 지정하기 위한 단일 인터페이스를 만드는 것이 최적인 경우도 경험한 적이 있을 것이다. 그 인터페이스를 오직 그 파라미터에 대해서만 사용할 것이다. 보통 이런 경우는 C 언어에서 함수 포인터처럼 파라미터를 다루고 싶은 경우이다. 예를 들면, 다음과 같은 프로시져가 있다.

static public void sort(List list, SortComparison comp) { ... } 

이것은 주어진 리스트의 모든 객체를 제공된 비교 객체인 comp를 사용하여 비교하여 소트하는 것으로, 모든 sort가 comp로부터 원하는 것은 비교를 위해서 comp의 하나의 메소드를 호출하는 것이다. SortComparison은 단 하나의 메소드를 가진 인터페이스가 될 것이다.

public interface SortComparison {
   boolean comesBefore(Object a, Object b);
}

이 인터페이스의 단 하나의 목적은 sort 메소드가 그 역할을 할 수 있도록 기능을 제공하는 것이므로, SortComparison 인터페이스는 다른 곳에서는 재사용될 수 없다.

결론

이 세 단계는 전통적인 객체지향 기법을 사용하여 작성된 기존의 코드에 적용될 수 있으며, 또한, 객체지향 프로그래밍과 결합된 세 단계는 새로운 코드를 작성할 때 적용할 수 있는 새로운 기법들과 함께 사용될 수 있으며, 메소드의 재사용성과 결합능력을 증가시키면서 커플링과 복잡성을 감소시킨다.

물론, 이러한 단계를 원래부터 재사용에 부적절한 코드에 적용할 수는 없다. 그러한 코드는 보통 프로그램의 프레젠테이션 계층에서 주로 발견된다. 프로그램의 유저 인터페이스를 만드는 코드와 입력 이벤트를 실제 작업이 수행되는 프로시져에 연결하는 콘트롤 코드는 둘 다 프로그램마다 너무나 다른 기능을 보여주기 때문에 재사용일 불가능한 예이다.

본 글의 저작권은 작성자(이동훈)에게 있으며, 작성자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts