본문 바로가기

JAVA/Design Patterns

인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제3

반응형

Template Method와 Factory Method 패턴

updateBuffer()는 템플릿 메소드(Template Method) 패턴의 예이다.

Template Method에서는 기반 클래스 코드가 오버라이딩 가능한 메소드를 호출하고, 메소드의 구현은 기반클래스를 구현한 상속 클래스에서 제공한다.

기반 클래스의 메소드는 대부분의 경우 abstact이지만, 때로는 디폴드 연산을 구현하고 있을 수도 있다.


Template Method 패턴에서는 기반 클래스가 알고리즘을 정의하고 이 알고리즘에서 오버라이딩 가능한 메소들을 호출한다. 그리고 기반 클래스를 상속한 파생클래스에서 각 메소드를 재저의하여 알고리즘의 행위를 커스터마이징 한다.


Template Method 패턴은 가능한 절제해 사용해야한다.

클래스 자체가 전적으로 파생클래스의 커스터마이징에 의존하는 일종의 '프레임워크'가되면 이 역시 매우 부서지기 쉽기 때문이다.


기반 클래스는 깨지지 쉽기 때문에 구현 상속 기반의 프레임워크 역시 깨지기 쉽다. 그러므로 Template Method 패턴은 가능한 사용을 피해야 한다.


자바 라이브러리의 '그 자체로 사용 가능한(합성을 통해 사용 가능한)' 구조는 MFC의 상속 기반 프레임워크보다 낫다. 유지보수하고 사용하기 쉬우며 벤더가 클래스 구현을 바꾸어도 위험하지 않다.


Template Method 패턴은 또한 '이디엄'과 '패턴' 사이가 얼마나 가까울 수 있는지 보여줄 수 있는 예이다.

Template Method 패턴은 다형성을 조금 응용했을 뿐 패턴이란 영광의 타이틀을 쓰기엔 부족하다고 주장할 수도 있는 것이다.


Template Method 패턴을 논의하는 한가지 이유는 알려지지 않는 구체 클래스를 생성하는 객체를 만드는데 Template Method 패턴의 변형을 이용할 수 없기 때문이다.

Factory Method 패턴은 기반 클래스에 알려지지 않는 구체 클래스를 생성하는 Template Method라 할 수 있다.

Factory Method의 반환 타입은 생성되어 반환되는 객체가 구현하고 있는 인터페이스이다.

또한 기반 클래스 코드에 구체 클래스의 이름을 감추는 방법이기도 하다.(Factory Method는 부절한 이름이다. 사람들은 객체를 생성하는 모든 메소드를 자연스레 팩토리 메소드라 부르는 경향이 있는데, 이러한 생성 메소드가 모두 Factory Method 패턴을 사용하는 것은 아니다.)


Factory Method 패턴은 파생 클래스가 어떤 객체를 생성할지를 결정하도록 한다. Factory Method 패턴은 Template Method 패턴의 생성 패턴 버전이라 할 수 있다.


스윙의 JEditorPane은 Factory Method 패턴의 실체화 예이다.


JFrame mainFrame = new JFrame();

 JEditorPane pane = new JEditorPane();


 pane.setContentType("text/html");

 pane.setEditable(false);

 pane.setText(

  "<html>" +

  "<head>" +

  "</head>" +

  "<body>" +

  "<center><b>Hello</b><i>World</i></center>" +

  "</body>" +

  "</html>" +

 );


 mainFrame.setContentPane(pane);

 mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

 mainFrame.pack();

 mainFrame.show();


해야할일은 JEditorPane이 태그를 해석하도록 콘텐츠 타입을 "test/html"로 지정하는 것뿐이다.

하지만 JEditorPane에는 하부 디자인 너무 복잡해서 JEditorPane의 행동을 조금 변경하는 작업도 매우 힘들다는 이면이 있다.


JEditorPane은 HTML을 렌더링해 주는 기능을 갖고 있다.

HTML과 커스텀 태그로 레이아수을 지정할 수 있는 패널인 MarkupPane을 만들기로 했다.


이문제를 해결하기 위해 우선 커스텀 LayoutManager를 생성하려 시도하였지만 두가지 이유로 포기하게 되었다.

먼저 HTML 파서를 만들기를 원치 않았다.

그리고 컨테이너에 넣은 컴포넌트 객체를 HTML의 특정 위치에 넣는 작업은 생각보다 어려웠다.

그래서 결국 커스텀 LayoutManager를 만드는 대신 JEditorPane을 수정하기로 결정했다.

JEditorPane은 이미 커스텀 태그를 지원하는 등 내가 원하는 거의 모든 기능을 이미 제공하고 있기 때문이다.

클라이언트를 위해 만든 MarkupClass를 단순화한것이다.

오늘의 날짜를 화면에 그려주는 <today> 태그를 추가하였다.

단순화한 버전에서 조차 커스텀 태그를 추가하기 위해 해줘야 하는 작업이 예상외로 만만치 않다는 걸 느낄 수 있었다.


LayoutManager를 이용하여 문제를 해결하려 했지만 생가보다 어려워 JEditorPanel을 수정하기로 결정했다.


스윙은 프로그램 설계의 단순함과 명확함을 잘 보여주는 예가 아니다. 조금만 수정을 위해 너무 많은 복잡한 작업을 필요로 하기 때무이다.

스윙의 아키텍처는 필요이상으로 복잡하다. 이러한 복잡도 증가의 원인 중 하나는 디자인 패턴에 '광적인' 설계자가 시스템의 사용성을 생각하지 않고 디자인을 했기 때문인듯하다. 패턴은 필요한 곳에서 의도에 맞게 적절히 사용해야만 한다.

이코드에서는 Factory Method 패턴이 무려 세번이나 오버랩되는 방식을 사용했다.


왜 이렇게 복잡해야 할까?

우선 스윙의 텍스트 패키지를 한번 생각해보자

이 패키지는 '이례적으로'유연하다. 스윙 패키지들은 지나치게 유연하다.

스윙이 제공하는 유연성 전체를 필요로 하는 프로그램이 있을지도 모른다는 이야기가 있다.

하지만 이러한 프로그램을 실제로 본적이 없다.


많은 디자이너들은 가상의 요구 사항을 만들어 내는 함정에 빠지기 쉽다.

하지만 이러한 요구사항은 사용자가 실제 요구하는 것이 아니다.

이 함정에 빠지게 되면 코드는 필요 이상으로 복잡하게 되고, 이러한 복잡함은 시스템의 유지보수성과 사용 편의성을 크게 해치게 된다.

스윙의 유연성은 가상의 요구 사항에 의한것이 많다.

즉 아무도 필요하다고 요구하지 않는 부분까지 유현하다.

이와 같은 복잡도를 필요로 하는 시스템이 실제로 있을지는 모르겠지만, 스윙을 사용하다 보면  "왜 이정도로 유연성이 필요한가?"라는 생각을 자주하게 되며, 과도한  유연성으로 인한 복잡함이 별 이점없이 개발기간을 길게 만든다.

아무도 이 정도로 스윙을 커스터마이징하는 사람이 없다는 사실은, 누구도 이정도로 스윙을 커스터마이징 할 필요가 없다는 주장에 좋은 논지가 된다.

물론 어느 누구도 스윙을 어떻게 사용할지에 대해 알지 못한다고 주장할수 있겠지만, 지금까지 스윙의 커스터마이징을 쉽게 해달라고 선에 요청한 사람이 없다는 사실로 미루어 본다면 내 주장이 타당할것이다.


Factory Method 패턴은 단순히 객체 생성을 제어하기 위해 구현 상속을 사용하게끔한다는 점을 지적하고 싶다.

Factory Method 패턴은 extends 관계를 잘못 사용하고 있다.


파생 클래스가 기반 클래스를 전혀 확장하고 있지 않기 때문이다.

파생 클래스는 기반 클래스에 어떤 새로운 기능도 추가하지 않는다.

이와 같이 extends 관계를 부적절히 사용하게 되면 깨지기 쉬운 기반 클래스 문제를 야기할 수 도 있다.


Factory Method 패턴은 extends 관계를 잘못 사용하고 있다. 파생 클래스가 기반 클래스에 아무런 기능도 추가하지 않기 때문이다.



깨지지 쉬운 기반 클래스 문제 정리

깨지지 쉬운 기반 클래스를 사용했을 때는 항상 여러 꼼수를 사용하며 걱정을 해야한다.

디버깅을 마친후에도 마찬가지다. 누군가 기반 클래스에 새로운 메소드를 추가한다면 모든 파생 클래스가 망가잘수 있기에 모든 '꼼수'는 영원하지 않다. 새로운 메소드 추가와 같은 문제를 해결할 수 있는 유일한 해결책은 상속 대신 캡슐화(합성)를 사용하는 것이다.


깨지기 쉬운 기반 클래스 문제를 해결 할 수 있는 유일한 해결책은 재사용을 위해 구현 상속 대신 합성을 사용하는 것이다.


거의 모든 디자인 패턴이 구현 상속 대신 합성과 인터페이스를 사용하고 있다.


일반적으로 구현 상속 관계를 피하는 것이 최선의 선택이다.

내 경험에 의하면 대부분의 프로그래머가 작성하는 코드의 80% 정도는 인터페이스 관점에서 재 작성해야 한다.

예를 들어 HashMap에 대한 레퍼런스를 직접 사용하지 말라. 대신 Map 인터페이스로의 레퍼런스를 사용하라.

물론 나는 인터페이스란 단어를 폭 넓은 의미로 사용하고 있다.

예를 들어 InputStream은 추상 클래스로 구현되어 있지만, 이를 어떻게 사용해야 하는지의 측면에서 바로볼 땐 인터페이스로 여길 수 있는 것이다.


인터페이스 관점에서 프로그래밍하라. 의존  관계 역전의 원칙(DIP)를 준수하라.


추상화를 잘 할수로고 프로그램은 더욱 유연해진다.

오늘날 비즈니스 환경에서는 개발 도중에도 요구 사항이 빈번히 바뀌곤 하기 때문에 유연성은 필수적이다.

더구나 크리스털이나 익스트림 프로그래밍과 같은 대부분의 '애자일 개발 방법론은 추상화를 적절히 이용한 코드를 작성하지 않는다면 제대로 효과를 발휘하지 못한다. 물로 ㄴ유연성에는 복잡도가 증가한다는 대가가 있다.

내 생각에는 스윙의 경우에는 조금 덜 유연하게 만든다면 훨씬 프로그래밍하기 쉽고, 유지보수하기 쉽게 될것이다.

즉 너무 유연하게 만들어 지나치게 복잡하다. 

모든 선택에는 트레이드 오프가 있다는 사실을 명심하도록 하자.


만약 GoF의 패턴들을 자세히 살펴본다면 이들 중 상당수가 구현 상속을 인터페이스 상속으로 바꾸는 방법에 대해 이야기하고 있다는 사실을 알게 될 것이다.

인터페이스를 사용하는 것은 많은 패턴들에서 공통적으로 발견되는 특성이다.

정의상으로 성공적인 디자인 패턴은 잘 작성되고 유지보수 하기 쉬운 코드로부터 뽑아낸 것이다.

수많은 훌륭한 코드들이 구현 상속을 역병을 보듯 피하고 있다는 사실은 구현 상속 대신 인터페이스 상속을 사용해야 한다는 주장의 뒷받침 해주는 설득력 있는 증거가 될 것이다.


GoF 패턴 중 상당수가 구현 상속을 인터페이스 상속으롤 바꾸는 방법을 이야기 하고 있다.

인터페이스를 사용한 프로그래밍을 하자.


하지만 때로는 구현 상속이 문제 해결하는 최선의 방법일수도 있다.

지금까지 extends를 사용하면 안되는 이유를 섦여하는데 많은 지면을 할애했지만, 구현 상속이 전혀 가치가 없다거나 절대로 사용하면 안된다는 것을 주장하는것은 아니다.

분명 extends가 유용할 경우가 있다.

하지만 이러한 경우라도 주의깊게, 그리고 깨지기 쉬운 기반 클래스 문제를 상쇄할 마한 다른 고려 사항이 있을 때 사용하기 바란다.

모든 선택에는 트레이드 오프가 있으며 해당 방법과 이를 대체할 수 있는 방법의 장점과 단점을 잘 헤아려 조율해야 할 것이다.

구현 상속은 분명 편리하고 종종 문제를 해결하는 가장 단순한 방법일 수도 있지만 깨지기 쉬운 기반 클래스 문제 역시 지니고 있다.

그리고 인터페이스와 이 인터페이스를 구현한 디폴트 구현을 통해 대부분의 경우 구현 상속을 대체할 수 있다.

반응형