제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



자바 디자인 패턴 14 - 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는 객체 생성과정의 복잡성을 떠넘기는 게 포인트입니다.

by 삼실청년 | 2009/01/09 17:41 | 컴터질~ | 트랙백 | 핑백(3) | 덧글(6)

트랙백 주소 : http://iilii.egloos.com/tb/4807809
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at 건실성실착실 3실 청년! : .. at 2011/10/02 18:30

... ; // 뭔가 통신해서 address라는 값을 세팅해주는 로직 }2. 또 다른 데이터 클래스를 만듭니다. 이 경우 기존의 Location 클래스를 Builder 처럼 이용하면 됩니다.public final class Location{ private final BigDecimal lat; private ... more

Linked at java design patt.. at 2017/05/31 09:26

... 14. 자바 디자인 패턴 14 – Builder ... more

Linked at 자바 디자인 패턴 종류 &#8.. at 2017/09/26 09:15

... 패턴 11 – Observer 12. 자바 디자인 패턴 12 – Prototype 13. 자바 디자인 패턴 13 – Flyweight 14. 자바 디자인 패턴 14 – Builder 15. 자바 디자인 패턴 15 – Mediator 16. 자바 디자인 패턴 16 – Visitor 출처 : http://iili ... more

Commented by 한경의 at 2010/04/13 21:25
글 잘 읽었습니다.
위에 올리신 builder예제들이 다 mandatory parameter를 적용하지 않고있군요.
예를들어
public class BatmanBuilder implements Builder {
private Hero batman;

BatmanBuilder(String mandatoryField) {
batman = new Hero("mandatoryField");
}

public void makeArm() {
batman.setArmSource("돈지랄");
}

public void makeLeg() {
batman.setLegSource("돈지랄");
}

public Hero getResult() {
return batman;
}
}

저는 개인적으로 GOF빌더보다 Joshua Bloch가 추천한 빌더를 훨씬 선호합니다.

글 잘 읽었습니다~~~
Commented by 삼실청년 at 2010/05/04 21:14
머 필수적인 데이터라면 있어야 하겠죠.
머 흐름에 크게 영향을 안 준다고 봤네요.

저도 GOF의 빌더 패턴 별로 그닥입니다. 그래서 아래다가 주절주절 조쉬아 씨가 추천한 패턴에 대해서 설명해 놓은 겁니다.

"생성에 관련된 복잡한 과정을 담당하는 패턴" 정도가 딱 적당할 듯하네요.
Commented by 김동식 at 2010/11/16 19:43
잘 읽었습니다~ 제가 잘 못 본것 일 수도 있는데요..

혹시

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

위의 요기 부분에서 cb.set1(...); 이 아닌가 해서요...;;; (아니면 어뜩하나 ㄷㄷㄷ)

ㅎㅎㅎ이펙티브 자바 보다가 빌더부분이 나와 참조하러 들렀습니다.. 그럼 즐거운 하루 보내세요 ^^
Commented by 삼실청년 at 2010/11/17 18:39
앗~ 그러네요. 후딱 수정했습니다.
알켜주셔서 감사합니다.^^
좋은 하루 되세요~~
Commented by 최익필 at 2011/06/22 11:48
Person p = new PersonBuilder().setName("홍길동").setCity("서울").build();
이 부분이 신선하고 재미있었습니다. 소스 코드가 있어 이해가 더 쉬웠습니다.
감사합니다.
Commented by asdf at 2016/12/04 03:23
director.build 하기 전에 director.getHero 하는 경우는 어떻게 막을거죠?
getHero는 director가 아닌 builder에 들어가는게 맞다 봅니다.

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶