Tips/디자인 패턴 (Design Pattern)

디자인 패턴 강좌 - Builder

dextto™ 2012. 9. 29. 11:47

이번 강좌에서는 GoF의 분류에 따라 생성(Creational) 디자인 패턴 중 하나인 Builder 패턴을 소개합니다.


Builder패턴은 여러 개의 복잡한 객체를 모아 합성을 하는 공정이 있고, 복합 객체의 생성알고리즘이 요소 객체들 각각의 생성방법과 조립방법에 독립적일 때 쓰입니다.

도대체 이게 무슨 소릴까요? 좀 더 쉽게 이야기 하면 객체들을 조립하는 인터페이스만 외부에 공개하고 내부에 실제 생성되는 객체들을 숨기자~ 하는 것이지요.

예제를 한 번 볼까요? 요구사항이 다음과 같이 주어졌습니다.
 

어떤 내용을 포함하는 문서를 Text와 HTML 포맷으로 작성하도록 하여, HTML을 지원하지 않는 디바이스에서도 내용을 볼 수 있도록 한다.


즉, 다음과 같은 출력결과를 가지도록 하는 게 목표입니다.


<Text로 출력 결과>

==============================
『Greeting』

■아침과 낮에

●좋은 아침입니다.
●안녕하세요

■밤에

●안녕하세요
●안녕히 주무세요
●안녕히 계세요

==============================

<HTML 파일로 저장 - 브라우저로 볼 경우>

Greeting

아침과 낮에

  • 좋은 아침입니다.
  • 안녕하세요

밤에

  • 안녕하세요
  • 안녕히 주무세요
  • 안녕히 계세요


HTML 소스는 이렇게 되겠죠? 

<html><head><title>Greeting</title></head><body>

<h1>Greeting</h1>

<p>아침과 낮에</p>

<ul>

<li>좋은 아침입니다.</li>

<li>안녕하세요</li>

</ul>

<p>밤에</p>

<ul>

<li>안녕하세요</li>

<li>안녕히 주무세요</li>

<li>안녕히 계세요</li>

</ul>

</body></html>


메인 클래스가 다음과 같이 작성되었다고 가정하고, 이제 내용을 채워 봅시다.
여기서 Director 클래스는 result를 작성(construct)하는 일을 하며 Main클래스는 Director 클래스에게 할 일을 위임합니다.

Main.java
public class Main { public static void main(String[] args) { String option="plain"; //"plain" or "html" Director director = new Director(); 

if (option.equals("plain")) { String result = ""; // TODO: director를 이용하여 result 구성 System.out.println(result); } else if (option.equals("html")) { // TODO: director를 이용하여 result를 HTML파일로 저장 System.out.println("HTML 파일이 작성되었습니다."); } } }


Before) 패턴 미적용
먼저 그냥 평소에 하던 대로 코드를 작성해 볼까요?
다음과 같이 Main 클래스를 바꾸어 봅시다.

Main.java
public
class Main { public static void main(String[] args) { String option="plain"; //"plain" or "html"
Director director = new Director(); 
if (option.equals("plain")) { String result = director.constructText();
System.out.println(result); } else if (option.equals("html")) {
director.constructHTML(); System.out.println(filename + "이 작성되었습니다."); } } }

Director 클래스는 다음과 같이 생겨 먹었습니다.
(각 메쏘드에 채워질 Director 클래스의 전체 소스는 첨부파일을 참조하세요.)

Director.java

public class Director { public Director() { } public String constructText() { String result = ""; // TODO 텍스트 형태로 결과를 반환 return result; } public String constructHTML() { String filename = ""; // TODO HTML형태로 내용을 저장하고 파일명을 반환 return filename; } }

그런데 만약 Text나 HTML이 아니라 PDF형태로 또 결과를 저장해서 한다고 해 봅시다.
그러면 Director 클래스에 constructPDF() 이런 함수를 또 추가해야 겠죠? 이런 방식에는 다음과 같은 문제가 존재합니다.

1. 요구사항이 추가될 때 마다 Director클래스를 수정해야 합니다.
2. Director 클래스의 크기가 점점 커져서 관리가 어렵습니다. 
   즉, 이 클래스는 기능욕심(Feature Envy)을 가진 클래스가 됩니다. 클래스는 자기가 해야할 최소한의 일을 하는 것이 좋습니다.
3. 
또한 Director클래스의 크기가 커져 읽기에도 힙듭니다. 


After) Builder 패턴을 적용 

이제 이 지저분한 Director 클래스를 정리해서 깔끔하게 만들어 봅시다.
우리가 출력해야할 결과를 보면 타이틀, 소제목 및 각 항목으로 나눌 수 있습니다.
Director클래스는 문서의 각 부분을 조합하는 일만 하고, 실제 각 부분이 어떻게 구성되는 지는 각각의 클래스(TextBuilder, HTMLBuilder)를 만들어 위임시킵니다.

Main.java
public class Main { public static void main(String[] args) { String option="plain"; //"plain" or "html" if (option.equals("plain")) { Director director = new Director(new TextBuilder()); String result = (String) director.construct(); System.out.println(result); } else if (option.equals("html")) { Director director = new Director(new HTMLBuilder()); String filename = (String) director.construct(); System.out.println(filename + "이 작성되었습니다."); } }


Director.java
public class Director { private Builder builder; public Director(Builder builder) {
  // Builder의 하위 클래스의 인스턴스가 제공되기 때문에 builder 필드에 보관해 둔다.
this.builder = builder; } public Object construct() { builder.makeTitle("Greeting"); builder.makeString("아침과 낮에"); builder.makeItems(new String[] { "좋은 아침입니다.", "안녕하세요", }); builder.makeString("밤에"); builder.makeItems(new String[] { "안녕하세요", "안녕히 주무세요", "안녕히 계세요", }); return builder.getResult(); } }

Builder.java
public abstract class Builder { public abstract void makeTitle(String title); public abstract void makeString(String str); public abstract void makeItems(String[] items); public abstract Object getResult(); }

TextBuilder.java
public class TextBuilder extends Builder { private StringBuffer buffer = new StringBuffer(); // 이 필드에 문서를 구축한다. public void makeTitle(String title) { // 일반 텍스트에서의 타이틀 buffer.append("==============================\n"); // 장식선 buffer.append("" + title + "』\n"); // 『』가 붙은 타이틀 buffer.append("\n"); // 공란 } public void makeString(String str) { // 일반 텍스트에서의 문자열 buffer.append('' + str + "\n"); // ■이 붙은 문자열 buffer.append("\n"); // 공란 } public void makeItems(String[] items) { // 일반 텍스트에서의 항목 for (int i = 0; i < items.length; i++) { buffer.append("" + items[i] + "\n"); // ●이 붙은 항목 } buffer.append("\n"); // 공란 } public Object getResult() { // 완성된 문서 buffer.append("==============================\n"); // 장식선 return buffer.toString(); // StringBuffer를String을 변환 } }

HTMLBuilder.java
import java.io.*; public class HTMLBuilder extends Builder { private String filename; // 작성할 파일명 private PrintWriter writer; // 파일에 기술할 PrintWriter public void makeTitle(String title) { // HTML 파일에서의 타이틀 filename = title + ".html"; // 타이틀을 토대로 파일명을 결정 try { writer = new PrintWriter(new FileWriter(filename)); // PrintWriter만든다. } catch (IOException e) { e.printStackTrace(); } writer.println("<html><head><title>" + title + "</title></head><body>"); // 타이틀을 출력 writer.println("<h1>" + title + "</h1>"); } public void makeString(String str) { // HTML 파일에서의 문자열 writer.println("<p>" + str + "</p>"); // <p>태그에서 출력 } public void makeItems(String[] items) { // HTML 파일에서의 항목 writer.println("<ul>"); // <ul>과<li>에서 출력 for (int i = 0; i < items.length; i++) { writer.println("<li>" + items[i] + "</li>"); } writer.println("</ul>"); } public Object getResult() { // 완성된 문서 writer.println("</body></html>"); // 태그를 만든다. writer.close(); // 파일을 클로즈 return filename; // 파일명을 반환한다. } }

이해 되셨나요? 각 부분을 만드는 일을 하는 클래스(XXXBuilder)와 이를 조립해서 쓰는 클래스(Director)가 분리되었습니다. 
이제 PDF문서로 결과를 저장하는 일이 추가되더라도 Director클래스를 수정할 필요없이 Builder클래스를 상속받은 PDFBuilder클래스를 하나 만들고 Director클래스 객체를 생성할 때 생성자의 인자로 넘겨주기만 하면 되는 군요. ^^

각 클래스가 하는 일도 명확하고, 클래스 자체의 크기도 줄어 들어 코드를 읽기도 쉬워 졌습니다.

반응형