Notice
Recent Posts
Recent Comments
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Builder Pattern 본문

Design Pattern/Creational Pattern

Builder Pattern

defacto standard 2018. 2. 16. 23:51

1. 가정


 - 사람에 대한 정보를 저장하고, 출력하는 프로그램을 작성한다.

 - 사람에 대한 정보는 이름, 나이, 전화번호, 키, 몸무게 등이 있다.

 - 모든 정보를 입력할 필요 없이, 부분적으로 입력할 수 있다.

 - 모든 정보는 수정될 수 있다.


2. Naive Code

 - Person

public class Person {
private String name;
private int age;
private String phoneNumber;
private int height;
private int weight;

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}

public void setHeight(int height) {
this.height = height;
}

public void setWeight(int weight) {
this.weight = weight;
}

public void print() {
System.out.println("Name : " + this.name);
System.out.println("Age : " + this.age);
System.out.println("PhoneNumber : " + this.phoneNumber);
System.out.println("Height : " + this.height);
System.out.println("Weight : " + this.weight);
}
}

 사람 정보를 표현하는 Person클래스이다.


수정이 가능해야 하고, 부분적으로 입력해야 할 수 있기 때문에 생성자가 아닌 setter를 사용하여 값을 넣고, print()는 Person 객체의 정보를 일괄적으로 출력한다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
Person person = new Person();
person.setName("Person1");
person.setAge(20);
person.setHeight(175);
person.setWeight(80);

person.print();
}
}

실행 결과는 다음과 같다.



3. 문제점


다음과 같은 요구사항이 추가/변경되었다고 생각해보자.

 - 정보에 '주소'라는 값이 추가된다.

 - 사람 정보는 만들어지면 수정이 불가능하다.

 - 필드의 입력 순서에 대해서, 모든 경우의 수를 만족해야 한다.


첫 번째 요구사항을 만족시키기 위해서는, 주소 필드를 추가하고, 이에 대한 setter를 추가해야한다.

그리고 print()의 내용을 수정해야 한다.



두 번째 요구사항을 만족시키기 위해서는, setter를 제거하고 생성자를 통해 Person 객체를 구성해야한다.

setter가 있다면 내용을 수정할 수 있기 때문에, 읽기만 가능해야 한다는 조건을 충족시킬 수 없기 때문이다.


따라서 모든 setter를 제거하고, 이를 제거함에 따라 Person 클래스의 setter를 사용하던 모든 클래스의 내용을 수정해야 한다.


대신, Person 클래스에 생성자를 사용해서 정보를 입력하게 함으로써 더 이상 정보가 수정될 수 없도록 한다.




세 번째 요구사항을 만족시키기 위해서는, 모든 경우의 수의 생성자를 만들어야 한다.


부분적으로 입력하기 위해, 예를 들어 여러 개의 필드 중 1개의 필드만을 입력한다면, 해당 필드를 제외한 모든 필드를 null 또는 0으로 값을 넣으면 될 것이다.


하지만 필드의 입력 순서에 대해 모든 경우의 수를 만족시키기 위해서는, 생성자의 오버로딩이 불가피하다.


이 경우, 입력해야 하는 필드의 갯수가 증가할수록 생성자의 갯수는 점점 늘어난다.


또한, 기존 생성자도 수정해야한다.

또한, 생성자에서 필드에 저장할 목적으로 넘겨받는 파라미터의 갯수가 늘어남에 따라, 어떤 값이 인자로 넘어오는지 파악하기가 힘들다.


점층적 생성자 패턴(Telescoping constructor pattern)을 사용한다고 하더라도, 필드의 갯수가 늘어남에 따라 엄청난 갯수의 생성자가 필요하다. 잘 동작하지만, 인자 수가 늘어나거나 바뀌면 어떤 인자인지 주의를 기울여야 하므로 코드를 작성하기 어렵고, 읽기 어려운 코드가 된다.


또한, 설정할 필요가 없는 필드에도 인자를 전달해야 하는 상황이 올 확률이 농후하여 성능 또한 좋다고할 순 없다.




사실 수정이 불가능 하다는 제약조건은 setter를 쓰게 하지 않기 위함이었다.

Immutable Object는 생성 후 값에 대한 변경이 불가능한 객체를 의미한다.


이러한 제약조건 없이 setter를 사용한다고 하더라도, 사소하지만 문제는 존재한다.


Naive Code에서 보듯이 Java Bean Pattern(Setter를 사용하여 객체의 값을 지정)의 경우,

객체의 생성과 값들을 세팅하는 setter가 다른 문장( ';'로 구분되어 실행되는)으로 구성된다.


호출 1회 내에객체에 대한 정보를 포함하여 생성할 수 없으므로, 객체 일관성(Consistency)이 일시적으로 깨지며 문장이 늘어나게 된다.

일관성이 깨진 객체를 사용할 때 생기는 문제는 실제 버그 위치에서 한참 떨어진 장소에서 발생하므로 디버깅도 어렵다.


그리고 setter를 사용하므로 수정이 불가능한 객체의 생성은 불가능하다.

스레드 안정성(Thread-Safety)을 제공하기 위해 해야 할 일도 많아진다.


이는 생성이 끝난 객체에 대해서는 더 이상 수정을 못하도록 하는 방법도 있긴한데, 까다로우며 이에 따른 유지보수가 쉽지 않아 잘 사용되지 않는다. 


소스코드는 가능하다면 최대한 짧고 간결하게 작성하는 것이 좋다.

(그렇다고 변수명을 int a, b, c, d; 와 같이는 하지말자..)



4. 해결방안


현재 문제가 되는 부분은, setter를 사용할 수 없게 됨(Immutable)에 따라 생성자가 늘어나게 되고, 이에 따라 유지보수가 힘들어지게 되는 것이다.


읽기 전용이므로 생성자를 사용해야 한다면, 생성자의 갯수를 늘리지 않고 위 요구사항을 모두 만족해야 하는 방법을 생각해야 한다.





Person 클래스는 생성자를 사용하고, Person 객체를 생성하려는 클래스에서는 정보의 순서에 상관없이, 선택적으로 Person 객체의 정보를 구성할 수 있어야 한다.


이를 해결하기 위해서는 Person 클래스의 생성자를 사용하는 클래스와 Person 클래스의 결합도를 낮추어야 한다.


Person 클래스에 대한 정보를 대신 setter로 입력받고, 최종적으로 Person 클래스의 생성자 1개를 이용해 객체를 리턴하는 클래스를 중간에 두면 된다.


이 클래스를 Builder라고 하겠다.


즉, Person객체는 setter가 없지만, Person 객체를 만들어내는 Builder클래스는 setter가 존재한다.

Builder 클래스는, 자신의 필드로 Person 클래스의 부분집합에 해당하는 필드를 가진다.


setter가 있기 때문에, 필드가 늘어난다고 해도 생성자가 복잡하게 구성되는 일은 존재하지 않는다.

setter로 들어온 값을 Person 클래스의 AllArgsConstructor를 사용해서 만들기만 하면 된다.


즉, Client에서 직접 만들던 Person 객체의 setter들과 생성자 호출 부분을, 다른 클래스(Builder)로 분리한 것이다.


이를 Builder Pattern이라고 한다.



예제를 위와 같이 했다고 해서, Immutable 클래스에 대해서만 사용하는 패턴은 아니다.

다만, 읽기만 가능한(Immutable) 클래스에 대해서는, Builder Pattern을 사용해야만 복잡한 생성자 코드를 피할 수 있다.


읽기만 가능하여 setter를 없앴으나, 다른 클래스에서는 setter가 존재하기 때문이다.


여기서 헷갈리면 안되는 것이,

Person의 경우는 field, Constructor, getter가 존재하며

Builder의 경우는 field, setter, build가 존재한다.


Person과 Builder의 field의 갯수와 종류는 동일하거나 Person의 범위가 더 넓다.

Builder의 setter는 Person에게 직접 값을 넘기는 용도가 아니라, 자신의 필드에 setter로 받아놓는 역할이다.

Builder의 build()는 Person의 Constructor를 불러 객체를 생성하는 역할이다. setter를 통해 Builder의 field에 저장된 값들을 Person의 Constructor에 인자로 넘긴다.

 


원래 Builder Pattern은 여러 요소들로 구성되는 복잡한 객체에 대해, 이를 구성하는 다양한 요소들을 쉽게 구성하여 객체를 생성할 수 있도록 도와주는 것이 핵심이다.



Builder 패턴에서 최종적으로 생성되는 객체를 Product라고 한다. 위 예제에서는 Person에 해당한다.


그리고 이러한 Product에 대한 생성을 담당하고, 객체의 값을 setter로 받아서 생성자에서 사용하게 끔 하는 클래스를 Builder라고 한다.


Builder의 경우 Product에 대한 정보와 싱크가 되어야 한다. 해당 Product에 대한 정보 세팅을 담당하기 때문이다


이렇게 Product클래스에 대해 지원해야 하므로, Product 클래스 내부에 static 으로 빌더를 구현하기도 한다.


여러 클래스에 대한 여러 개의 Builder가 있을 수 있기 때문에, Builder에 대한 추상적인 요소를 인터페이스로 정의하기도 한다.



5. Solution Code


 - ComplexObject

public class ComplexObject {
private ProductA productA;
private ProductB productB;

public ComplexObject(ProductA productA, ProductB productB) {
this.productA = productA;
this.productB = productB;
}

public void print() {
System.out.println("ComplexObject# ProductA Value 1 : " + this.productA.getAvalue1());
System.out.println("ComplexObject# ProductA Value 2 : " + this.productA.getAvalue2());
System.out.println("ComplexObject# ProductA Value 3 : " + this.productA.getAvalue3());
System.out.println("ComplexObject# ProductA Value 4 : " + this.productA.getAvalue4());
System.out.println();
System.out.println("ComplexObject# ProductB Value 1 : " + this.productB.getBvalue1());
System.out.println("ComplexObject# ProductB Value 2 : " + this.productB.getBvalue2());
System.out.println("ComplexObject# ProductB Value 3 : " + this.productB.getBvalue3());
System.out.println("ComplexObject# ProductB Value 4 : " + this.productB.getBvalue4());
}

}

최종적으로 구성될 객체이다. ProductA, B로 이루어져 있고, Composition을 사용한다.


 - ProductA, B

public class ProductA {
private String Avalue1;
private int Avalue2;
private int Avalue3;
private String Avalue4;

public ProductA(String avalue1, int avalue2, int avalue3, String avalue4) {
Avalue1 = avalue1;
Avalue2 = avalue2;
Avalue3 = avalue3;
Avalue4 = avalue4;
}
public String getAvalue1() {
return Avalue1;
}

public int getAvalue2() {
return Avalue2;
}

public int getAvalue3() {
return Avalue3;
}

public String getAvalue4() {
return Avalue4;
}

}
public class ProductB {
private char Bvalue1;
private char Bvalue2;
private int Bvalue3;
private String Bvalue4;

public ProductB(char bvalue1, char bvalue2, int bvalue3, String bvalue4) {
Bvalue1 = bvalue1;
Bvalue2 = bvalue2;
Bvalue3 = bvalue3;
Bvalue4 = bvalue4;
}

public char getBvalue1() {
return Bvalue1;
}

public char getBvalue2() {
return Bvalue2;
}

public int getBvalue3() {
return Bvalue3;
}

public String getBvalue4() {
return Bvalue4;
}

}

Immutable Class인 ProductA, B이다. 생성자를 통해서 만들어지지만 setter가 없다.

ProductA, B는 필드가 완전히 다른 별도의 클래스이다.


 - Builder

public interface Builder<T> {
public T build();
}


 - ConcreteBuilderA, B

public class ConcreteBuilderA<T> implements Builder<T> {
private String Avalue1;
private int Avalue2;
private int Avalue3;
private String Avalue4;

public ConcreteBuilderA setAvalue1(String Avalue1) {
this.Avalue1 = Avalue1;
return this;
}

public ConcreteBuilderA setAvalue2(int Avalue2) {
this.Avalue2 = Avalue2;
return this;
}

public ConcreteBuilderA setAvalue3(int Avalue3) {
this.Avalue3 = Avalue3;
return this;
}

public ConcreteBuilderA setAvalue4(String Avalue4) {
this.Avalue4 = Avalue4;
return this;
}

@Override
public T build() {
return (T) new ProductA(Avalue1, Avalue2, Avalue3, Avalue4);
}

}
public class ConcreteBuilderB<T> implements Builder<T> {
private char Bvalue1;
private char Bvalue2;
private int Bvalue3;
private String Bvalue4;

public ConcreteBuilderB setBvalue1(char Bvalue1) {
this.Bvalue1 = Bvalue1;
return this;
}

public ConcreteBuilderB setBvalue2(char Bvalue2) {
this.Bvalue2 = Bvalue2;
return this;
}

public ConcreteBuilderB setBvalue3(int Bvalue3) {
this.Bvalue3 = Bvalue3;
return this;
}

public ConcreteBuilderB setBvalue4(String Bvalue4) {
this.Bvalue4 = Bvalue4;
return this;
}

@Override
public T build() {
return (T) new ProductB(Bvalue1, Bvalue2, Bvalue3, Bvalue4);
}

}

ProductA, ProductB에 대한 정보를 입력받아서 객체를 생성해내는 ConcreteBuilerA, B이다.


ProductA, B에 대한 정보를 가지고 있어야 하기 때문에 ProductA, B 내부에 선언되어도 상관이 없다.


나는 클래스를 내부에 작성하는 것을 좋아하지 않기 때문에 별도의 파일로 분리하였다.



 - ConcreteBuilderComplex

public class ConcreteBuilderComplex<T> implements Builder<T> {
private ProductA productA;
private ProductB productB;

public ConcreteBuilderComplex setProductA(ProductA productA) {
this.productA = productA;
return this;
}

public ConcreteBuilderComplex setProductB(ProductB productB) {
this.productB = productB;
return this;
}

@Override
public T build() {
return (T) new ComplexObject(this.productA, this.productB);
}

}

ComplextObject의 정보값을 세팅받아 생성해내는 클래스이다.

역시 Builer Pattern을 사용한다.


 - Client

public class Client {
public static void main(String args[]) {
Builder<ProductA> productABuilder
= new ConcreteBuilderA<ProductA>()
.setAvalue2(1).
//.setAvalue1("aString")
.setAvalue3(2)
.setAvalue4("bString");

Builder<ProductB> productBBuilder
= new ConcreteBuilderB<ProductB>()
.setBvalue4("cString")
.setBvalue1('a')
.setBvalue2('b')
.setBvalue1('c')
.setBvalue3(3);

Builder<ComplexObject> complexObjectBuilder
= new ConcreteBuilderComplex<ComplexObject>()
.setProductA(productABuilder.build())
.setProductB(productBBuilder.build());

ComplexObject complexObject = complexObjectBuilder.build();
complexObject.print();

}
}

Product A의 경우 Value 2-1-3-4 순으로 입력한다.

A-Value1은 선택적인 옵션이라 필요가 없기에 주석처리 하였다.


Product B의 경우 Value 4-1-2-1-3 순으로 입력한다.

B-Value1의 경우 2번 세팅하게 되는데, 첫 번째에 세팅한 값 'a'는 무시되고 이후에 세팅한 값인 'c'가 세팅된다.


실행 결과는 다음과 같다.



6. Builder Pattern


클래스의 생성자를 직접적으로 호출함으로써 유연하지 못한 설계를, 간접적으로 호출함으로써 유연하게 만드는 패턴.

Client와 Product간의 결합도를 Builder를 통해서 줄인다.


필요한 객체를 직접 생성하는 대신, 필수 인자들을 생성자(또는 정적 팩토리 메서드)에 전달하여 Builder Object를 만든다.


Builder는 Product의 필드의 일부분을 자신의 필드로 가지고 있어, Client는 Builder의 setter를 통해 Product 생성자 사용 시 인자를 유연성 있게 결정할 수 있다.


Builder의 build()메서드는 아무런 인자 없이 수행되어 Immutable OBject를 생성해낼 수 있다.

build()메서드 내부에서는 Product 클래스의 생성자를 호출하므로, Delegation을 사용한다고 할 수 있다.


Product에 setter 사용이 허용된다면 크게 메리트가 없어 보일 수는 있으나, 객체 생성과 관련된 소스코드의 일관성을 얻을 수 있다.

또한, 후에 Product에 setter가 허용되지 않게 끔 요구사항이 변경되더라도 이를 방지할 수 있다.


또한 setter가 허용되지 않는 Product에 대해 Builder가 setter를 제공하므로, 복잡한 조건(수정 불가)과 늘어나는 필드로 인한 복잡한 생성자의 구성을 획기적으로 단축시킬 수 있다.


자바 빈 패턴(Java Bean Pattern)과 점층적 생성자 패턴(Telescoping constructor pattern)의 장점을 결합한 패턴이라고 할 수 있다.


생성자와 마찬가지로, Builder Pattern을 사용하면 인자에 불변식(Invariant)을 적용할 수 있다. 따라서, build()메서드 내부에서 해당 불변식이 위반되었는지 검사할 수 있다. 그리고 불변식을 빌더 객체의 필드가 아니라, 실제 객체의 필드를 두고 검사할 수 있다는 것은 중요하다.


불변식을 위반한 경우, build()메서드는 어떤 불변식을 위반했는지 알아낼 수도 있어야 한다. 위에서 구현한 코드의 경우, setter가 있기 때문에 이 내부에서 불변식을 위반했는지 검사하면 된다. 불변식이 만족되지 않으면, IllegalArgumentException을 던질 수 있다.


이렇게 함으로써 해당 필드에 들어가려는 값이 올바른 값인지 신속하게 검사할 수 있다.


어떤 필드의 값은 객체 생성 마다 자동적으로 증가되게 할 수도 있는 등의 구현도 가능하다.

JDK1.5이상을 사용하는 경우, Generic 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐에 관계없이 모든 Builder에 적용이 가능해진다.

public interface Builder<T> {
public T build();
}



 * 주의

setter 메서드 정의 시에 this 키워드를 통해, 현재 클래스의 타입 객체를 리턴함으로써 object.setA().setB().setC(); 와 같이 사용하는 형태를 Builder Pattern이라고 말하는 경우가 있다.

이는 단순히 Builder Class의 setter메서드에서 this 객체를 리턴함으로서 얻는 편의성에 불과하고, 이 자체로서 Builder Pattern이라고 할 수는 없다.


Builder패턴은 객체를 생성하는 클래스를 중간자로 두어, 만들려는 객체의 타입을 직접적으로 참조하지 않도록 구조화 하는 패턴을 의미한다.

중간자 클래스인 Builder 없이, 특정 클래스에서 setter를 위와 같이 정의한다고 이를 Builder패턴이라고 하지 않는다는 뜻이다.



- UML & Sequence Diagram




7. 장단점


장점

 - 객체 생성 시 복잡한 경우의 수에 대한 생성자 소스코드를 단 하나로 해결할 수 있음

 -> 읽기 전용의 클래스에 대해, 선택적인 값을 넣어야 할 경우에 특히 유용

 - 후에 인자가 늘어나는 경우 자바빈 패턴보다 훨씬 안전하다.


단점

 - 객체를 생성하려면 우선 빌더 객체를 생성해야 한다.

 -> 오버헤드가 발생한다. 실무에서 문제의 소지는 없지만 성능이 극한에 달하는, 조금의 성능이라도 아까운 환경에서는 말이 달라질 수 있다.

 - 점층적 생성자 패턴보다 많은 코드를 요구하기에, 인자가 충분히 많은 상황(보통 4개 이상)에서 효율이 좋다.

Comments