Builder 패턴
7. Builder 패턴
빌딩을 세울 때 우선 지반을 다지고, 골격을 세우고, 아래에서 위로 조금씩 만들어 간다. 일반적으로 복잡한 건축물을 세울 때 한 번에 완성시키기는 어렵다. 우선 전체를 구성하고 있는 각 부분을 들고 단계를 밟아 만들어 나간다. 이와 같이 구조를 가진 인스턴스를 쌓아 올리는 것이 Builder 패턴이다.
예제 프로그램
Builder 패턴을 사용한 ‘문서’ 작성 프로그램을 만들어 보자. 여기에 만들 문서는 다음과 같은 구조를 가지고 있다.
타이틀을 한 개 포함한다.
문자열을 몇개 포함한다
개별항목을 몆개 포함한다.
Builder 클래스에서는 문서를 구성하기 위한 메소드를 결정한다. 그리고 Director 클래스가 그 메소드를 사용해서 구체적인 하나의 문서를 만든다. Builder 클래스는 추상 클래스로서 실제 처리는 하지 않고, 추상 메소드만 선언한다. 문서작성을 위한 구체적인 처리를 결정하는 것은 Builder 클래스의 하위 클래스 이다. 프로그램의 자세한 동작은 책을 참고한다. (책 120~121p)
Builder 클래스
Builder클래스는 ‘문서’를 만들 메소드들을 선언하고 있는 추상 클래스입니다. makeTitle, makeSTring, makeItems는 각각 타이틀, 문자열, 개별항목을 무서안에 구축하는 메소드이다. close 메소드는 문서를 완성시키는 메소드이다.
//문서를 구성하기 위한 메소드를 결정하는 추상 클래스 public abstract class Builder { public abstract void makeTitle(String title); public abstract void makeString(String str); public abstract void makeItems(String[] items); public abstract void close(); }
Director 클래스
Director 클래스안에서는 Builder 클래스로 선언되어 있는 메소드를 사용해서 문서를 만든다. Director 클래스의 생성자의 인수는 Builder형이다. 그러나 Builder 클래스의 인스턴스가 인수로 주어지는 경우는 없다. 왜냐하면 Builder 클래스는 추상 클래스이므로 인스턴스를 만들 수 없기 때문이다. Director의 생성자에서 실제로 전달되는 것은 Builder클래스의 하위 클래스 인스턴스이다. 주어진 Builder 클래스의 하위 클래스 종류에 따라 Director 클래스가 만들 구체적인 문서의 형식이 정해진다. construct 메소드는 문서를 만드는 메소드이겨, Builder에서 선언되어 있는 메소드만을 사용한다. 이 메소드를 호출하면 문서가 만들어진다.
//한 개의 문서를 만드는 클래스 public class Director { private Builder builder; public Director(Builder builder){ this.builder = builder; } public void construct(){ //문서구축 builder.makeTitle("Greeting"); //타이틀 builder.makeString("아침과 낮에"); //문자 builder.makeItems(new String[]{ //개별 항목 "좋은 아침입니다.", "안녕하세요." }); builder.makeString("밤에"); //별도의 문자열 builder.makeItems(new String[]{ //별도의 개별 항목 "안녕하세요.", "안녕히 주무세요.", "안녕히 계세요." }); builder.close(); //문서를 완성시킨다 } }
TextBuilder 클래스
TextBuilder 클래스는 Builder 클래스의 하위 클래스이다. 일반 텍스트를 사용해서 문서를 구축하고 결과는 String으로 반환한다.
//일반 텍스트를 이용해서 문서를 만드는 클래스 public class TextBuilder extends Builder{ private StringBuffer buffer = new StringBuffer(); public void makeTitle(String title){ buffer.append("============================\n"); buffer.append("『"+title+"』"); 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 void close() { buffer.append("============================\n"); } public String getResult(){ return buffer.toString(); } }
HTMLBuilder 클래스
HTMLBuilder 클래스도 builder 클래스의 하위 클래스이다. HTMLBuilder 클래스는 HTML 파일로 문서를 구축한다. 구축 결과는 HTML 파일의 파일명으로 반환한다. (태그가 섞이는 문제가 있어서 예제를 생략한다. 책을 참고하자)
Main 클래스
Builder 패턴의 테스트 프로그램이다. plain과 html 이라는 명령형 매개 변수를 이용해 지정한 형식에 따른 문서를 만든다. 커맨드 라인에서 plain을 지정한 경우에는 TextBuilder 클래스의 인스턴스를 Director 클래스의 생성자에게 전달된다. 또한 커맨드 라인에서 html을 지정한 경우에는 HTMLBuilder 클래스의 인스턴스를 Director 클래스의 생성자에게 전달된다. TextBuilder와 HTMLBuiledr는 Builder의 하위 클래스이고 Director는 Builder의 메소드만을 사용해서 문서를 작성한다. Builder의 메소드만을 사용한다는 것은 Director는 실제로 동작하는 것이 TextBuilder인지, HTMLBuilder인지 모른다는 의미이다. 따라서 Builder는 문서를 구축하려는 목적을 달성하기 위해서 필요 충분한 메소드군을 선언할 필요가 있다. 단, 일반 텍스트나 HTML파일에 고유의 메소드까지 Builder가 제공해서는 안된다.
public class Main { public static void main(String[] args) { if (args.length != 1){ usage(); System.exit(0); } if (args[0].equals("plain")) { TextBuilder textbuilder = new TextBuilder(); Director director = new Director(textbuilder); director.construct(); String result = textbuilder.getResult(); System.out.println(result); } else if(args[0].equals("html")) { HTMLBuilder htmlbuilder = new HTMLBuilder(); Director director = new Director(htmlbuilder); director.construct(); String filename = htmlbuilder.getResult(); System.out.println(filename + "가 작성되었습니다."); } else { usage(); System.exit(0);; } } public static void usage() { System.out.println("Usage: java Main plain 일반 텍스트로 문서 작성"); System.out.println("Usage: java Main html HTML 파일로 문서 작성"); } }
Builder 패턴의 등장인물
Builder(건축자)의 역할 예제에서 Builder 클래스에 해당하며, 인스턴스를 생성하기 위한 인터페이스(API)를 결정합니다. Builder 역할에는 인스턴스의 각 부분을 만들기 위한 메소드가 준비되어 있다.
ConcreteBuilder(구체적인 건축자)의 역할 예제에서 TextBuilder나 HTMLBuilder 클래스에 해당하며, ConcreteBuilder 역할은 Builder 역할의 인터페이스(API)를 구현하고 있는 클래스이다. 실제의 인스턴스 작성으로 호출되는 메소드가 여기에서 정의된다. 또한 최종적인 결과를 얻기 위한 메소드가 준비되어 있다.
Director(감독자)의 역할 예제에서 Director 클래스에 해당하며, Builder 역할의 인터페이스(API)를 사용해서 인스턴스를 생성한다. ConcreteBuilder 역할에 의존한 프로그래밍은 수행하지 않는다. ConcreteBuilder 역할과 관계없이 제대로 가능하도록 Builder 역할의 메소드만을 사용한다.
Client(의뢰인)의 역할 예제에서 Main 클래스에 해당하며, Builder 패턴을 이용하는 역할이다(Gof의 책에서는 Client 역할은 Builder 패턴안에 포함되어 있지 않다.)
사고 넓히기
누가 무엇을 알고 있을까?
OOP(객체지향 프로그램)에서는 ‘누가 무엇을 알고 있을까?’라는 화두는 상당히 중요하다. 즉, 어떤 클래스가 어떤 메소드를 사용할 수 있을까?에 주의하며 프로그래밍을 할 필요가 있다. 예제를 상기해보자. Main 클래스는 Builder 클래스의 메소드를 모른다. Main 클래스는 Director 클래스의 construct 메소드만을 호출한다. 그러면 Direcotr 클래스 안에서 조용히 일이 진행되고 문서가 완성된다. 한편 Director클래스가 알고 있는것은 Builder클래스이다. Director 클래스는 Builder 클래스의 메소드를 사용해서 문서를 구축한다. 그러나 Director 클래스는 자신이 실제로 이용하고 있는 클래스가 사실 모른다. TextBuilder인지, HTMLBuilder인지 또는 Builder의 하위 클래스인지 모른다. Director클래스는 Builder 클래스의 메소드만을 사용하고 있고 Builder 클래스의 하위 클래스는 그 메소드를 구현하기 때문이다. Director 클래스가 자신이 이용하고 있는 Builder클래스의 하위 클래스를 모르는 것은 정말 잘 된 일입니다. 왜냐하면 모르기 때문에 교체할 수 있기 때문이다. TextBuilder의 인스턴스를 Director에 제공하거나 Director의 HTMLBuilder의 인스턴스를 Director에 제공해도 제대로 기능하는 것은 Director 클래스가 Builder클래스의 구체적인 하위 클래스를 모르기 때문이다. 모르기 때문에 교환이 가능하고, 교체가 가능하기 때문에 부품으로서 가치가 높다. 이 ‘교환가능성’에 대해서 클래스의 설계자는 항상 기억할 필요가 있다. …… 패턴 사용에 대한 주의점 등은 책을 참고 하고 포스팅에는 생략한다. …… 7-1 예제 프로그램의 Builder 클래스를 인터페이스로 변경하고 그것에 맞게 다른 클래스를 수정하십시오 내가 생각한 답 : Builder
public interface Builder { public void makeTitle(String title); public void makeString(String str); public void makeItems(String[] items); public void close(); }
HTMLBuilder
public class HTMLBuilder implements Builder{ ...... }
TextBuilder
public class TextBuilder implements Builder{ ...... }
책 해설 : 내가 생각한 답과 동일 7-2 예제 프로그램의 HTMLBuilder 클래스에서는 makeTitle 메소드를 처음에 호출해야 하지만, TextBuilder 클래스에서는 메소드를 호출할 때는 순서의 제한이 없다. Builder 클래스, TextBuilder 클래스, HTMLBuilder 클래스를 수정해서, ’makeString 메소드, makeItems 메소드, close 메소드가 호출되기 전에 한번만 makeTitle 메소드가 호출되도록’해 보십시오. 내가 생각한 답 : 문제의 ‘한번만 호출’ 이라는 부분이 정확하지가 않아 잘 모르겠지만 인스턴스 생성후 단 한번만 호출 되게끔 하려면 선언문을 바꾼뒤에 생성자에서 title을 받도록 하면 될것 같다. 책 해설 : Templete 메소드 패턴을 이용하고 initialize 되었는지 확인하는 필드를 만들어서 체크한다. 자세한건 책을 참고하자. (답이 좀 억지인듯…..) 7-3 예제 프로그램의 Builder클래스의 하위 클래스로 ConcreteBuilder 역할을 하는 클래스를 작성하십시오. 일반 텍스트, HTML파일을 제외하고 소재는 자유이다. 내가 생각한 답: System.out.println 해주는 java 파일 생성기
public class JavaBuilder extends Builder{ private String filename; private PrintWriter writer; @Override public void makeTitle(String title) { filename = title + ".java"; try { writer = new PrintWriter(new FileWriter(filename)); }catch(IOException e) { e.printStackTrace(); } writer.println("class " + title + " {"); writer.println("\tpublic static void main(String[] args){ "); writer.println("\t\tSystem.out.println(\""+title+ "\");"); } @Override public void makeString(String str) { writer.println("\t\tSystem.out.println(\""+str+"\");"); } @Override public void makeItems(String[] items) { for(String item : items){ writer.println("\t\tSystem.out.println(\""+item+"\");"); } } @Override public void close() { writer.println("\t}\n}"); writer.close(); } public String getResult() { return filename; } }
책 해설 : JFC를 이용한 GUI 구현을 하는 클래스가 나와 있다. (소스는 책을 참고하자…) 7-4 TextBuilder 클래스에서 문서를 구축하는 필드 buffer가 sting 클래스가 아니고 StringBuffer 클래스로 되어있는 이유는 무엇입니까? string을 사용하면 안되는 이유는 무엇입니까? 내가 생각한 답 : 딱히 String 사용시에 동작의 문제는 없지만 쓸데없는 가비지가 많이 생겨난다. 책 해설 : StringBuffer가 빈번한 수정시에 더 빠르다(내가 생각한 답이 그 이유다.)