빌더 패턴의 이론적 지식을 공부하던중에 빌더 패턴도 두 가지의 디자인 종류가 존재한다는 것을 알게 되었다. GOF에서 소개하고 있는 빌더 패턴과 이펙티브 자바에서 소개된 빌더 패턴이 있다.
개발자들이 빌더를 직접 생성하거나 Lombok 라이브러리에서 @Builder 어노테이션을 사용하여 컴파일한 결과물이 보통 이펙티브 자바 빌더 패턴 방식이다. @Builder 어노테이션에 대해서는 나중에 정리할 생각이다. GOF 빌더 패턴과 구분하기 위해 심플 빌더 패턴이라고도 불린다.
앞에서도 설명했지만 심플 빌더 패턴을 사용하는 경우는 생성자가 많거나 변경이 불가능한 불변객체가 필요한 경우 코드의 가독성과 일관성, 불변성을 유지하는 것에 중점을 둔다. 심플 빌더 패턴은 지난번에 배운 빌더 패턴과 큰 차이는 없다. 하지만 빌더 클래스가 구현할 클래스의 static Inner Class로 구현이 되어있다.
이펙티브 자바 빌더 패턴 방식으로 구현하려면 다음과 같은 방식을 따라야 한다.
Music 빌더 클래스
public class Music {
private final String name;
private final String artist;
private final String country;
// 빌더 클래스를 Static Nested Class로 정의한다.
public static class Builder {
private final String name; // 필수 파라미터
private String artist; // 선택 파라미터
private String country;
// 빌더 클래스의 생성자는 public
// 필수 파라미터는 생성자 파라미터로 받는다
public Builder(String name){
this.name = name;
}
// 선택적 파라미터에 대해서는 메소드로 제공
public Builder artist(String artist){
this.artist = artist;
// 메소드의 반환값은 빌더 객체 자신(this)
return this;
}
public Builder country(String country){
this.country = country;
return this;
}
// 객체를 생성하는 build() 메서드를 정의
public Music build() {
return new Music(this);
}
}
// private 생성자 - 생성자는 외부에서 호출되는것이 아닌 빌더 클래스에서만 호출
private Music(Builder builder){
this.name = builder.name;
this.artist = builder.artist;
this.country = builder.country;
}
}
빌더 사용 클래스
public class test {
public static void main(String[] args) {
Music music = new Music // 생성자의 인수로 빌더 인스턴스 자기자신을 전달
.Builder("test") // static inner class 초기화
.artist("tester") // 선택 파라미터
.country("test")
.build();
// 대상 객체 생성자에서 빌더 인스턴스의 필드를 각각 대입
}
}
빌더 클래스는 하나의 대상 객체 생성에만 사용된다. 즉 두 클래스를 물리적으로 연결하면 두 클래스간의 관계 파악이 쉬워진다.
정적 내부 클래스는 외부 클래스 인스턴스 없이도 생성이 가능하다. 하지만 만약 static이 아닌 일반 내부 클래스로 구성하면 빌더를 사용하기위해 외부 클래스를 인스턴스화를 해야한다. 즉 빌더가 최종적으로 생성할 인스턴스를 먼저 생성해야 한다는 모순이 생기게 된다.
메모리 누수 문제 때문에 static으로 내부 클래스를 정의하는 이유도 있다.
디렉터 빌더 패턴 즉 GOF에서 정의하고 있는 디자인 패턴은 복잡한 객체의 생성 알고리즘과 조립 방법을 분리하여 빌드 공정을 구축하는 것이 목적인데 빌더를 받아 조립방법을 정의한 클래스를 Director라고 부른다.
이펙티브 자바 빌더 패턴은 하나의 대상에 대한 객체 생성이 주 목적이지만 디렉터 빌더 패턴은 여러가지 빌드 형식을 유연하게 처리하는 것에 목적을 둔다. 디렉터 빌더 패턴은 기존의 빌더 패턴을 고도화 시킨거라고 볼 수 있겠다.
Builder : 빌더 추상 클래스
ConcreteBuilder : 빌더 패턴을 거친 구현체이다. Product 결과 생성을 담당한다.
Director : 빌더에서 제공하는 메서드들을 사용해 정해진 순서대로 Product 생성하는 프로세스를 정의
Product : Director가 Builder로 만들어진 결과물
빌더 패턴은 빌더의 대상이 되는 객체가 있고 해당 객체에 빌드 패턴을 적용시킨 클래스가 있다. 디렉터 빌더 패턴은 기존의 빌더 구조에 Director를 추가해 다양한 타입의 빌드 타입을 만들기 위해 만들어 졌다고 생각한다. 상단의 이미지와 비교 해보니 맞는말 같았다.
해당 예제는 음악 데이터를 저장하고 있는 Music 객체를 Builder 클래스를 통해 String, json 타입등의 포멧으로 변환하는 예제이다.
TextBuilder : Music 인스턴스의 데이터를 텍스트 형태로 만드는 API
JSONBuilder : Music 인스턴스의 데이터를 JSON 형태로 만드는 API
Music Class
public class Music {
private final String name;
private final String artist;
private final String country;
// Music 생성자 선언
public Music(String name, String artist, String country){
this.name = name;
this.artist = artist;
this.country = country;
}
public String getName(){
return name;
}
public String getArtist(){
return artist;
}
public String getCountry(){
return country;
}
}
Music Builder Class
abstract class MusicBuilder {
// 상속한 자식클래스에서 사용하도록 protected 접근제어자 지정
protected Music music;
public MusicBuilder(Music music){
this.music = music;
}
// Music 객체의 데이터들을 원하는 형태의 문자열 포맷을 해주는
// 메서드
public abstract String header();
public abstract String body();
public abstract String footer();
}
Text Bulder Class
public class TextBuilder extends MusicBuilder {
public TextBuilder(Music music){
super(music);
}
@Override
public String header() {
return "";
}
@Override
public String body() {
// 문자열과 문자열을 연결하도록 해주는 클래스
StringBuilder sb = new StringBuilder();
sb.append("Name: ");
sb.append(music.getName());
sb.append(", Artist: ");
sb.append(music.getArtist());
sb.append(", Country: ");
sb.append(music.getCountry());
return sb.toString();
}
@Override
public String footer() {
return "";
}
}
JSON Bulder Class
public class JSONBuilder extends MusicBuilder {
public JSONBuilder(Music music){
super(music);
}
@Override
public String header() {
return "{\n";
}
@Override
public String body() {
StringBuilder sb = new StringBuilder();
sb.append("\t\"Name\" : ");
sb.append("\"" + music.getName() + "\",\n");
sb.append("\t\"Artist\" : ");
sb.append("\"" + music.getArtist() + "\",\n");
sb.append("\t\"Country\" : ");
sb.append("\"" + music.getCountry() + "\",\n");
return sb.toString();
}
@Override
public String footer() {
return "}";
}
}
디렉터 빌더 클래스
public class Director {
private MusicBuilder musicBuilder;
public Director(MusicBuilder musicBuilder){
this.musicBuilder = musicBuilder;
}
public String build(){
StringBuilder sb = new StringBuilder();
sb.append(musicBuilder.header());
sb.append(musicBuilder.body());
sb.append(musicBuilder.footer());
return sb.toString();
}
}
디렉터 빌더 클래스 출력
public class MusicMain {
public static void main(String[] args) {
Music music = new Music("love letter", "Yoasobi", "Japen");
// 텍스트 포맷하여 출력하기
MusicBuilder builder1 = new TextBuilder(music);
Director director1 = new Director(builder1);
String result1 = director1.build();
// JSON 형태로 포맷하여 출력하기
MusicBuilder builder2 = new JSONBuilder(music);
Director director2 = new Director(builder2);
String result2 = director2.build();
System.out.println(result1);
System.out.println(result2);
}
}
위의 구조를 보면 Builder는 부품을 만들고 Director는 Builder가 만든 부품을 조합해 제품을 만든다고 볼 수 있겠다.
디렉터 빌더 패턴은 여러가지의 디자인 패턴이 섞여 있다고 볼 수 있다.
이렇게 이펙티브 자바 스타일의 빌더 패턴과 GOF 스타일의 디렉터 빌더 패턴의 특징을 작성해봤는데 나의 경우하면 이펙티브 자바 스타일의 빌더 패턴을 사용할거 같았다. GOF 스타일의 빌더 패턴은 확장성의 관점에서 보면 확실히 좋은 방법이지만 다양한 타입을 나타내기위해 빌드 패턴 클래스 뿐만아니라 특정 타입의 빌드 패턴 클래스를 한번더 생성해야 하고 Director파일까지 생성한다면 빌드 패턴의 단점인 코드의 복잡성이 더욱 증가할 우려가 있다. 무엇보다 대부분 현업에서 이펙티브 자바 스타일의 빌드 패턴을 사용한다는 이야기를 들은 적이 있다.
(항상 감사합니다.)