E D R , A S I H C RSS

Programming With Interface

출처: Holub on Patterns by Allen Holub

책에서는 말한다. 많은 개발자들이 인터페이스 보다는 상속을 사용하여 개발한다고... 그렇다! 사실이다. 나도 여지껏 인터페이스로 무장한 코드를 보지 못했다.
언제나 개발을 할 때 '어라~ 같은 일 하는데? 이거 Base 클래스 만들어서 위로 올려야 겠는데?' 일말의 틈도 주지 않고 실행한다. 다형성을 사용하는 코드를 생성한다. '와우~! 한결 깔끔해 졌는걸?' 하지만 오산이었다. 시간이 지나서 먼가 추가할 동작들이 생겼다. 이제 고치기 시작한다. Base 클래스 부터... 고치고 나니 컴파일이 되지 않는다. 코드 수정의 여파가 하위 클래스들에게 까지 미친다. 정말 미친다. 이런 상속을 통한 계층 구조는 상위 클래스와 하위 클래스의 결합도를 높여준다. 지나 치게 크게..! 동감하지 않는가? 하나를 고쳤는데 수정할 꺼리가 마구 쏟아지는 상황을...
상속을 사용하는 상황을 국한 시켜야 할 것같다. 상위 클래스의 기능을 100%로 사용하면서 추가적인 기능을 필요로 하는 객체가 필요할 때! .. 이런 상황일 때는 상속을 사용해도 후풍이 두렵지 않을 것 같다. GoF의 책이나 다른 DP의 책들은 항상 말한다. 상속 보다는 인터페이스를 통해 다형성을 사용하라고... 그 이유를 이제야 알 것같다. 동감하지 않는가? Base 클래스를 수정할 때마다 하위 클래스를 수정해야 하는 상황이 발생한다면 그건 인터페이스를 통해 다형성을 지원하는게 더 낫다는 신호이다. 객체는 언제나 SRP (Single Responsiblity Principle)을 지켜야 한다고 생각한다.

Holub이 사용하는 예제를 보자. 상속을 사용해 Stack을 구현한다.
class Stack extends ArrayList {

 private int topOfStack = 0;

 

 public void push(Object article) {

  add(topOfStack++, article);

 }

 

 public Object pop() {

  return remove(--topOfStack);

 }

 

 public void pushMany(Object[] articles) {

  for(int i=0; i<articles.length; ++i) 

   push(articles[i]);

 }

}

완벽한 Stack이다. 하지만 다음 코드를 사용하면 우리는 Stack 객체가 어떻게 돌아갈지 예측 할 수 없다.

Stack aStack = new Stack();

stack.push("1");

stack.push("2");

stack.clear(); // ??? ArrayList의 메소드이다...;;
자 모든 값을 clear 를 사용해 삭제했는데 topOfStack의 값은 여전히 3일 것이다. 자 상속을 통한 문제를 하나 알게 되었다. 상속을 사용하면 원치 않는 상위 클래스의 메소드까지 상속할 수 있다 는 것이다.

상위 클래스가 가지는 메소드가 적다면 모두 버라이딩하는 방법이 있지만 만약 귀찮을 정도로 많은 메소드가 있다면 오랜 시간이 걸릴 것이다. 그리고 만약 상위 클래스가 수정된다면 다시 그 여파가 하위 클래스에게 전달된다. 또 다른 방법으로 함수를 오버라이딩하여 예외를 던지도록 만들어 원치않는 호출을 막을 수 있지다. 하지만 이는 컴파일 타임 에러를 런타임 에러로 바꾸는 것이다. 그리고 LSP (Liskov Sustitution Principle : "기반 클래스는 파생클래스로 대체 가능해야 한다") 원칙을 어기게 된다. 당연히 ArrayList를 상속받은 Stack은 clear 메소드를 사용할 수 있어야 한다. 그런데 예외를 던지다니 말이 되는가?

Stack을 구현하는 다른 방법은 상속 대신 슐화를 사용하는 것이다.
class Stack {

 private int topOfStack = 0;

 private ArrayList theData = new ArrayList();

 

 public void push(Object article){

  theData.add(topOfStack++, article);

 }

 

 public Object pop() {

  return theData.remove(--topOfStack);

 }

 

 public void pushMany(Object [] articles) {

  for(int i=0; i<articles.length; ++i)

   push(articles[i]);

 }

 

 public int size() {

  return return theData.size();

 }

}
자.. Stack과 ArrayList간의 결합도가 많이 낮아 졌다. 구현하지 않은 clear 따위 호출 되지도 않는다. 왠지 합성을 사용하는 방법이 더 나은 것 같다. 이런 말도 있다. 상속 보다는 합성을 사용하라고... 자 다시 본론으로 들어와 저 Stack을 상속하는 클래스를 만들어 보자. MonitorableStack은 Stack의 최소, 최대 크기를 기억하는 Stack이다.
class MonitorableStack extends Stack {

 private int maxHeight = 0;

 private int minHeight = 0;

 

 public void push(Object o) { 

  push(0);

  if(size() > maxHeight)

   maxHeight = size();

 }

 

 public Object pop() {

  Object poppedItem = pop();

  if(size() < minHeight)

   minHeight = size();

  return poppedItem;

 }

 

 public int maximumSize() { return maxHeight; }

 public int minimumSize() { return minHeight; }

}
깔끔한 코드가 나왔다. 하지만 MonitorableStack은 pushMany 함수를 상속한다. MonitorableStack을 사용해 pushMany 함수를 호출하면 MonitorableStack의 입력 받은 articles의 articles.length 만큼 push가 호출된다. 하지만 지금 호출된 push 메소드는 MonitorableStack의 것이라는 점! 매번 size() 함수를 호출해 최대 크기를 갱신한다. 속도가 느려질 수도 있다. 그리고 만약 누군가 Stack의 코드를 보고 pushMany 함수의 비 효율성 때문에 Stack을 밑의 코드와 같이 수정했다면 어떻게 될 것인가???
class Stack {

 private int topOfStack = -1;

 private Object[] theData = new Object[1000]; 

 public void push(Object article) {

  theData[++topOfStack] = article;

 }

 

 public Object pop() {

  Object popped = theData[topOfStack--];

  theData[topOfStack] = null;

  return popped; 

 }

 

 public void pushMany(Object [] articles) {

  assert((topOfStack + articles.length) < theData.length);

  System.arraycopy(articles, 0, theData, topOfStack + 1, articles.length);

  topOfStack += articles.length;

 }

 

 public int size() {

  return topOfStack + 1;

 }

}
와!~ 예전의 Stack보다 성능은 확실히 좋아 졌을 것이다. 그런데 문제가 발생했다. 더이상 pushMany 메소드에서 push 메소드를 호출하지 않는다. 이렇게 되면 MonitorableStack은 더이상 Stack의 최대 크기를 추적하지 못하게 된다. 예기치 않은 결과이다. 상속을 사용한 구현으로 발생한 문제이다. 여기까지 글을 (책의 내용) 읽었다면, 아마 '상속을 사용하기 전에 한번 더 생각하는게 좋겠다' 라는 생각을 가슴 깊이 느꼈을 것이다. 아니면 별수 없는 일이다... :(

자 길었던 여행의 종착점이다. 최종 코드를 보자. 위에서 말했던 상속보다는 합성을... 상속보다는 인터페이를... 이란 말을 종합 하면 ...

"위임(합성)을 통해 인터페이스를 구현하자." 라는 결론이 나온다.


interface Stack {

 void push(Object article);

 Object pop();

 void pushMany(Object [] articles);

 int size();

}

 

class SimpleStack implements Stack {

 private int topOfStack = 0;

 private ArrayList theData = new ArrayList();

 

 public void push(Object article){

  theData.add(topOfStack++, article);

 }

 

 public Object pop() {

  return theData.remove(--topOfStack);

 }

 

 public void pushMany(Object [] articles) {

  for(int i=0; i<articles.length; ++i)

   push(articles[i]);

 }

 

 public int size() {

  return return theData.size();

 } 

}

 

class MonitorableStack implements Stack {

 private int maxHeight = 0;

 private int minHeight = 0;

 private SimpleStack stack = new SimpleStack(); 

 

 public void push(Object o) { 

  stack.push(0);

  if(stack.size() > maxHeight)

   maxHeight = size();

 }

 

 public Object pop() {

  Object poppedItem = stack.pop();

  if(size() < minHeight)

   minHeight = stack.size();

  return poppedItem;

 }

 

 public void pushMany(Object [] articles) {

  for(int i=0; i<articles.length; ++i)  

   push(articles[i]);

 

  if(stack.size() > maxHeight)

   maxHeight = stack.size();  

 }

 

 public int maximumSize() { return maxHeight; }

 public int minimumSize() { return minHeight; } 

 public int size() { stack.size(); }

}
완성된 코드에서는 상속으로 인한 문제들이 발생하지 않는다.
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:24:03
Processing time 0.0523 sec