2010년 12월 29일 수요일

Builder 패턴은..

1. Builder 패턴은..

뭔가가 만들어 지는 과정은 꽤나 복잡할 수가 있습니다. 게다가 그 복잡한 과정이 순서대로 실행되어야 할 때도 있습니다. 객체의 생성에 있어서 이런 복잡한 과정들을 분리해 내는 것이 Builder 패턴입니다.

2. 예제

---------------- 복잡한 과정을 거쳐서 만들어 지는 객체가 될 Hero 클래스 ----------------

package ch14_builder;

public class Hero {
private String armSource;
private String legSource;
private String name;

public Hero(String name) {
super();
this.name = name;
}
public void setArmSource(String armSource) {
this.armSource = armSource;
}
public void setLegSource(String legSource) {
this.legSource = legSource;
}
public void showResult(){
System.out.println(armSource +"로 만든 팔과 " + legSource +"로 만든 다리를 가진 " + name);
}
}

---------------- 복잡한 Hero 객체를 만들어내기 위한 객체 생성과정을 관리하는 Builder 인터페이스 ----------------

package ch14_builder;

public interface Builder {
void makeArm();
void makeLeg();
Hero getResult();
}

---------------- 복잡한 Hero 객체를 실제로 만들어내는 Builder의 구현체인 배트맨 찍어내는 클래스 ------------------

package ch14_builder;

public class BatmanBuilder implements Builder {
private Hero batman;
BatmanBuilder(){
batman = new Hero("배트맨");
}
public void makeArm() {
batman.setArmSource("돈지랄");
}
public void makeLeg() {
batman.setLegSource("돈지랄");
}
public Hero getResult() {
return batman;
}
}

---------------- Builder를 관리해 주는 Director ----------------

package ch14_builder;

public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void build(){
builder.makeArm();
builder.makeLeg();
}
public Hero getHero(){
return builder.getResult();
}
}

---------------- Director를 이용해 Hero를 찍어내는 Test클래스 -------------

package ch14_builder;

public class Test {
public static void main(String[] args) {
Builder builder = new BatmanBuilder();
Director director = new Director(builder);
director.build();
Hero hero = director.getHero();
hero.showResult();
}
}

---------------- 테스트 결과 -------------
돈지랄로 만든 팔과 돈지랄로 만든 다리를 가진 배트맨

Hero 라는 클래스가 있습니다. 이 클래스는 그냥 생성자만 호출해서는 아무 쓸모없는 클래스입니다. 이런 저런 정보들이 많이 쎄팅이 되어야 비로소 쓸만한 객체가 됩니다.(예제에서는 setArmSource()과 setLegSource()와 같은 게 그런 복잡한 세팅 과정에 관여하는 메쏘듭니다.) 따라서 이 클래스의 객체를 생성하는 과정은 매우 번거롭습니다.
이런 번거로운 과정을 Director에서 간단하게 build()라는 메쏘드로 해결을 하려고 합니다. build()라는 메쏘드는 참 간단한 것 같은데, 번거로운 과정을 어떻게 다 커버하느냐는 결국 Builder에 위임해서 해결합니다.
Builder는 비교적 세부적인 사항들을 다룹니다. 이에 비해 Director는 좀 더 포괄적인 과정은 다룹니다. 위의 예제의 경우는 Builder는 팔은 어떻게 만들어 지고 다리는 어떻게 만들어지는 지 등과 같은 것을 다루며(세부적인 사항인 makeArm(), makeLeg()와 같은 메쏘드), Director는 팔을 만들고 다리를 만들면 대략 Hero 하나 완성시킬 수 있다는 전체적인 로직(포괄적인 과정인 build() 메쏘드)을 다룹니다. 즉, Director는 다른 Hero를 만드는데도 활용할 수 있지만, Builder는 각각의 Hero에 국한됩니다.

위의 예제는 예제인 만큼 간단하게 만들었습니다만, Hero를 만들기 위해서 수십수백 가지의 정보가 세팅되어야 한다고 칩시다. 이럴 때, Hero의 생성자에 그런 정보들을 다 세팅해줄 수는 없습니다.

3. UML을 벗어나서...

위 에서는 등장인물이 Builder(interface)와 그 구현체들, 그를 관리하는 Director 그리고 만들어지는 생산품(Hero) 등이 있었습니다. 그러나, 이는 그냥 전형적이 UML 모냥새를 나타내는 것 뿐이고, Builder 패턴에 있어서는 저런 UML을 벗어나는 경우들이 허다합니다.
Builder에서의 포인트는 "뭔가 복잡한 과정을 거쳐 만들어지는 객체가 있는데, 이때 이 복잡한 과정을 Builder에게 담당시키겠다"는 것입니다. 따라서 Builder와 Product 두 개만으로도 구성될 수 있습니다. 즉, Builder 자체가 Abstract Class나 인터페이스가 아니라 그냥 클래스 일수도 있습니다.
StringBuilder와 같은 것이 대표적인 예입니다. StringBuilder는 이름 그대로 String을 만들어냅니다. 코드를 봅시다.

String ab = new StringBuilder("a").append("b").toString();

여기서 StringBuilder는 결국 "ab"라는 String을 만들기 위한 것입니다. StringBuilder 자체가 의미를 가지는 것이 아니라, 만들어 내는 String이 의미를 가지는 것입니다.

4. Builder를 쓰면 좋은 경우

같은 클래스의 인자를 여러 개 받은 Constructor를 생각해봅시다.

public class Person{
private final String name;
private final String city;
public Person(Stirng name, String city){ //인자가 둘 다 String
this.name = name;
this.city = city;
}
//getter는 생략.
}

이 코드는 다음과 같이 호출되어야 합니다.
Person p = new Person("홍길동", "서울");

그런데, 인자가 둘 다 String이라 다음과 같은 실수가 있을 수도 있죠.
Person p = new Person("서울", "홍길동"); // 앞 뒤가 바뀜!

이러면 "서울"에 사는 "홍길동"씨가 아니라, "홍길동"에 사는 "서울"씨가 만들어집니다.

Person p = new PersonBuilder().setName("홍길동").setCity("서울").build();

와 같이 호출된다면, 앞 뒤가 바뀔 가능성이 별로 없겠지요.

(PersonBuilder는 생략합니다.)

이번에는 다음과 같은 복잡한 클래스를 생각해 봅시다.

public class Complex(){
private Object member1;
등등등 수많은 member 변수..
public void set1(Object arg) { member1 = arg;}
등등등 수많은 setter..
public Object get1() { return member1;}
등등등 수많은 getter..
}

------ Complex를 만들어 내는 클라이언트 ----
Complex complex = new Complex();
complex.set1(...);
여러 개의 setter 호출....

------ Complex를 가져다 쓰는 클라이언트 ----
complex.get1();
여러 개의 getter 호출

이 클래스의 사용 코드를 보면, 복잡하게 만들어내는 부분과 가져다가 사용하는 부분이 명확하게 분리가 되어있습니다. 따라서 다음과 같은 요구가 있을 수 있습니다.

1. 한번 세팅된 값은 변하면 안 된다.
2. getter는 모든 member 변수가 세팅이 된 후에만 사용할 수 있다.

모든 setter는 수정하려는 변수가 세팅이 되어있는지 확인해야 하고, 모든 getter에서는 모든 member 변수가 세팅이 되었는지 확인해야 합니다. 이런 경우 코드가 다음과 같이 좀 지저분하게 수정되어야 합니다.

public class Complex(){
private Object member1;
등등등 수많은 member 변수..
private boolean completed(){
//member 들이 세팅이 전부 완료되었는 지를 체크해서 리턴.
}


public void set1(Object arg) { if(member1 != null) throw new IllegalStateException("세팅 중복") member1 = arg;}
등등등 수많은 setter..
public Object get1() { if(!completed()) throw new IllegalStateException("세팅 미완료") return member1;}
등등등 수많은 getter..
}

모 든 getter 들에서 세팅이 완료되었는지 체크하는 로직이 들어가야 합니다. completed() 안의 부분을 효율적으로 바꿀 수는 있지만, getter에서 IllegalStateException이 발생하는 것은 근본적으로 막을 수는 없습니다.

여 기서의 문제는 Complex 클래스를 한 번에 만들어내지 못한다는 것이었습니다. 이럴 경우 만들어내는 복잡한 과정을 ComplexBuilder를 만들어서 해결하면 됩니다. 또 ComplexBuilder는 오로지 Complex 클래스를 만들어내기 위한 클래스입니다. 따라서 ComplexBuilder를 분리하면 됩니다.

public class ComplexBuilder{
private Object member1;
등등등 수많은 member 변수..
public void set1(Object arg) { member1 = arg;}
등등등 수많은 setter..
//여기는 getter가 있을 지 없을 지 생각 좀 해보고 필요에 따라 넣던지 빼던지...
public Complex build(){
//Complex 객체를 만드는 과정을 전부 넣어둠.
}
}

public class Complex(){
Complex(){} //public constructor를 제공하지 않음.

private Object member1;
등등등 수많은 member 변수..
void set1(Object arg) { member1 = arg;} // public이 아님!!
등등등 수많은 setter..
public Object get1() { return member1;}
등등등 수많은 getter..
}

위와 같이 2개의 클래스를 같은 패키지에 넣어두고 쓰면 됩니다.

이를 사용하는 코드는 아래와 같이 됩니다.

------ Complex를 만들어 내는 클라이언트 ----
ComplexBuilder cb = new ComplexBuilder();
cb.set1(...);
여러 개의 setter 호출....
Complex complex = cb.build();

------ Complex를 가져다 쓰는 클라이언트 ----
complex.get1();
여러 개의 getter 호출

일단 Complex는 public constructor를 제공하지 않도록 했습니다. 잘못된 객체 생성을 막고, 오로지 ComplexBuilder를 통해서만 만들어지게 합니다. 또 Complex의 setter 들도 public이 아닙니다. 즉, 외부에서 변수를 세팅하지 못하도록 막았습니다. 따라서 멤버 변수가 변경될 수 있는 가능성을 근본적으로 막아버렸습니다. 그리고 getter에서 member 변수가 세팅이 되었는지를 일일이 확인할 필요도 없어졌습니다.

간략히 정리하면, 복잡하게 만들어지는 Immutable 클래스는 Builder를 통해 만들면 편하다는 얘기죠.



---------------------------------------------------------------------------


5. Factory 패턴과의 차이점


Factory와 Builder는 모두 객체 생성에 관련된 패턴이고 둘이 좀 비슷해보이기도 합니다. 그러나 용도가 좀 다릅니다.

Factory는 일반적으로 다음의 두 가지 경우에 해당하는 경우에 사용합니다.

1. 리턴 타입이 일정하지 않은 경우. 예를들어, create("소") 라고 하면 Cow 클래스의 인스턴스를 리턴하고, create("개")라고 하면 Dog 클래스의 인스턴스를 리턴하는 것처럼 인자에 따라 리턴 타입이 바뀌는 경우.
2. 대략 비슷하지만 멤버가 살짝 다른 경우. Boolean.valueOf(String arg) 와 같은 경우 리턴 타입은 모두 Boolean 이지만, 속의 내용이 조금 다름. 이 경우는 대부분 Singleton 패턴으로 처리됨.

그러나 Builder는 객체 생성과정의 복잡성을 떠넘기는 게 포인트입니다.