상속이라는 개념의 등장은 매우 획기적이었다. 유용한 클래스가 있다면 그것의 서브 클래스를 만들고 내가 원하는 부분과 차이가 있는 부분만 수정하여 사용하면 되었다. 상속만으로 코드를 재사용할 수 있었다.
하지만 곧 상속의 문제점이 드러났다. 상속의 과도한 사용은 아주 큰 대가를 치르게 되었다. 리스코프 치환 원칙 같은 원칙들을 깨면서 상속을 무작정 사용하고 당장의 코드를 재사용한다는 기분에 취해 쌓이고 있던 문제들이 한 번에 덮쳐오게 된 것이었다. 그래서 "클래스 상속보다는 차라리 복합이 더 낫다."라는 말도 나왔다. 현재는 경우에 따라서 복합이나 위임으로 상속을 대체하는 방법을 사용하고 있다.
이 Chapter는 구체적인 내용으로부터 일반적인 알고리즘을 분리하는 문제를 해결하는 패턴을 2가지 소개한다. 템플릿 메소드 패턴은 상속을 이용하여, 스트래터지 패턴은 위임을 통해서 이 문제를 해결하고 있다. 실제로 이 패턴들은 매우 자주 사용되고 소프트웨어 설계에도 많이 적용되는 패턴이다. DIP를 적용하기 위해서는 일반적인 알고리즘이 구체적인 구현에 의존하지 않도록 해야하고, 일반적인 알고리즘과 구체적인 구현이 추상화에 의존해야하게 한다. 이렇게 DIP를 지키게 하기 위해서도 해당 패턴을 사용하기도 한다.
- 메인 루프 구조: 초기화 -> 메인 루프(주요 로직 수행) -> 정리를 하고 종료
- 이런 메인 루프 구조에도 템플릿 메소드 패턴을 적용 가능하다.
public abstract class Application {
private boolean isDone = true;
abstract void init();
abstract void idle();
abstract void cleanup();
void setDone(boolean done) {
isDone = done;
}
boolean done() {
return isDone;
}
public void run() {
init();
while(!done()) {
idle();
}
cleanup();
}
}
- 하지만 모든 메인 루프 구조를 포함한 어플리케이션에 이 패턴을 적용하면 좋을까? 아니다!
- 패턴을 적용하기 전에는 내가 이 패턴을 적용함으로써 얻을 수 있는 이익을 생각해봐야한다. 패턴을 무작정 적용만 하면, 그저 아무 이득도 없이 프로그램이 복잡해지고 내용만 늘어날 수 있다. 이것을 패턴 오용이라고 한다.
책 내의 BubbleSorter에서 버블 정렬 알고리즘을 따로 떼어 BubbleSorter를 만들 수 있다.
public abstract class BubblSorter {
private int operation = 0;
protected int length = 0;
protected int doSort() {
operation = 0;
if(length <= 1) {
return operation;
}
for(int nextToLast = length - 2; nextToLast >= 0; nextToLast--) {
for(int index = 0; index <= nextToLast; index++) {
if(outOfOrder(index)) {
swap(index);
}
operation++;
}
}
return operation;
}
protected abstract void swap(int index);
protected abstract void outOfOrder(int index);
}
public class IntBubbleSorter extends BubbleSorter {
private int[] array = null;
public int sort(int[] theArray) {
array = theArray;
length = array.length;
return doSort();
}
protected void swap(int index) {
int temp = array[index];
array[index] = array[index + 1];
array[index + 1] = temp;
}
protected boolean outOfOrder(int index) {
return (array[index] > array[index + 1]);
}
}
이처럼 BubblSorter는 배열에 대해 전혀 알지 못한다. 뭐가 저장됐는지 전혀 신경쓸 필요가 없다. 그저 알고리즘의 흐름, outOfOrder를 호출하고 인덱스가 교환되어야 하는지 아닌지를 판단하고 수행할 뿐이다. 이제 BubbleSorter로 어떤 종류의 객체든 정렬할 수 있는 간단한 파생 클래스를 만들 수 있다. 그 예가 IntBubbleSorter다.
이처럼 일반적인 알고리즘은 기반 클래스에 있고, 다른 구체적인 내용에서 상속된다. 하지만 이 패턴은 비용이 사실 크다. 상속은 아주 강한 관계이기에 파생 클래스는 필연적으로 기반 클래스에 묶이게 된다. 위에 예시에서 보면 IntBubbleSorter의 swap과 outOfOrder만 보면 사실 다른 정렬 알고리즘에서도 사용 가능하다. 하지만 현 상황에서는 이 두 메서드를 다른 쪽에서 재사용할 방법이 마땅하지 않다. 이미 IntBubbleSorter는 BubblSorter를 상속함으로써 영원히 BubblSorter에 묶여버린 것이다.
- 일반적인 알고리즘과 구체적인 구현 사이의 의존성 반전 문제를 완전히 다른 방식으로 해결
- 위의 Application 예제를 사용해보면, 일반적인 알고리즘을 추상 기반 클래스가 아닌 ApplicationRunner라는 구체 클래스에 넣는다. Application이란 이름의 인터페이스 안에서 일반적 알고리즘이 호출해야할 추상 메소드를 정의한다. 그리고 ApplicationRunner에게 Application 인터페이스의 구현체를 넘겨준다. ApplicationRunner는 Application에게 이 일반적인 알고리즘의 흐름에서의 각 구체적인 동작의 책임을 위임하게 된다.
public class ApplicationRunner {
private Application application = null;
public ApplicationRunner(Application app) {
application = app;
}
public void run() {
application.init();
while(!application.done()) {
application.idle();
}
application.cleanup();
}
}
public interface Application {
void init();
void idle();
void cleanup();
boolean done();
}
// 적절한 Application 구현체
- 이 구조에서 ApplicationRunner의 내부 위임 포인터 때문에 실행 시간과 데이터 공간 면에서 상속보다 좀 더 많은 비용을 초래한다. 그리고 구조는 상속을 사용한 구조보다는 조금 더 복잡하다. 하지만 서로 다른 많은 어플리케이션을 실행한다면 ApplicationRunner 재사용하여 Application의 다른 많은 구현을 넘겨줄 수 있을 것이다. 그로인해 일반적인 알고리즘과 그것이 제어하는 구체적인 부분사이의 결합도를 감소시킬 수 있다.
이번엔 위의 예시였던 버블 정렬을 스트래터지 패턴으로 구현한 것이다.
public class BubbleSorter {
private int operation = 0;
private int length = 0;
private SortHandle handle = null;
public BubbleSorter(SortHandle handle) {
this.handle = handle;
}
public int sort(Object array) {
handle.setArray(array);
length = handle.length();
operation = 0;
if(length <= 1) {
return operation;
}
for(int nextToLast = length - 2; nextToLast >= 0; nextToLast--) {
for(int index = 0; index <= nextToLast; index++) {
if(handle.outOfOrder(index)) {
handle.swap(index);
}
operation++;
}
}
return operation;
}
}
public interface SortHandle {
void swap(int index);
void boolean outOfOrder(int index);
int length();
void setArray(Object array);
}
public class IntSortHandle implements SortHandle {
private int[] array = null;
public void swap(int index) {
int temp = array[index];
array[index] = array[index + 1];
array[index + 1] = temp;
}
public void setArray(Object array) {
this.array = (int[]) array;
}
public int length() {
return array.length;
}
public boolean outOfOrder(int index) {
return (array[index] > array[index + 1]);
}
}
IntSortHandle은 BubbleSorter에 대해 전혀 모른다. 즉 버블 정렬 구현부에 어떤 의존성도 가지고 있지 않다. 템플릿 메소드 패턴을 사용한 코드를 보면 swap과 outOfOrder가 버블 정렬 알고리즘에 직접 의존을 구현하고 있다. 하지만 스트래터지 패턴을 사용한 코드는 이런 의존성을 포함하고 있지 않다. 그렇기 때문에 IntSortHandle을 BubbleSorter가 아니라 다른 Sorter 구현과도 함께 사용할 수 있게 된다. 상세한 예시는 책에 더 설명되어있다.
탬플릿 메소드 패턴은 일반적인 알고리즘으로 많은 구체적인 구현을 조작할 수 있게 해준다. 스트래터지 패턴은 각각의 구체적인 구현이 다른 많은 일반적인 알고리즘에 의해 조작될 수 있게 해준다.
2가지 패턴 모두 상위 단계의 알고리즘을 하위 단계의 구체적인 부분으로부터 분리해주는 패턴이다. 그로인해 상위 단계의 알고리즘이 구체적인 부분과 독립적으로 재사용될 수 있게 해준다. 거기다가 스트래터지 패턴을 사용한다면 약간의 복잡성과 메모리, 실행시간을 감수하면 구체적인 부분이 상위 단계 알고리즘으로부터 독립적으로 재사용되게 할 수 있다.