[Design Pattern] GoF's Builder vs. Joshua Bloch's Builder

2021. 12. 26. 22:12개발/디자인패턴

빌더패턴은 오브젝트 생성과 관련된 패턴이다. ( Createtional design pattern ) 빌더 패턴과 관련해서 GoF가 소개한것과 Joshua Bloch가 소개한 빌더패턴이 약간 다른데 오늘은 두가지 코드를 모두 작성해보려고 한다.

GoF's Builder Pattern


큰 맥락은 비슷하지만 GoF 빌더패턴을 구현하는 방법에 따라 인터페이스가 좀더 추가되는 경우가 있다.
개인적으로 아래의 예시가 제일 코드가 심플하고 이해가능한(?)수준인것 같다. GeeksForGeeks에 있는 예제코드는 개인적으로 너무 장황한 느낌이다.

 

Builder

Say you have a constructor with ten optional parameters. Calling such a beast is very inconvenient; therefore, you overload the constructor and create several shorter versions with fewer parameters. These constructors still refer to the main one, passing s

refactoring.guru

 

Builder Design Pattern - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org



먼저 Builder Pattern은 어떤 문제를 해결하는지 알아보자.

Phone이라는 클래스를 하나 만들었다.

public class Phone {
    private String name;
    private String realeseDate;
    private String companyName;
    private boolean isAndroid;
    private boolean isKoreaCompany;

    public Phone(String name, String realeseDate, String companyName, boolean isAndroid, boolean isKoreaCompany) {
        this.name = name;
        this.realeseDate = realeseDate;
        this.companyName = companyName;
        this.isAndroid = isAndroid;
        this.isKoreaCompany = isKoreaCompany;
    } 
    // getter 생략 
}


main에서 Phone 클래스의 인스턴스를 만든다. 인스턴스를 생성하면서 동시에 휴대폰 정보에 대한 데이터를 셋팅한다.

아래처럼 한곳에서 인스턴스를 생성하는것은 크게 어려움이 없지만 소스코드 곳곳에서 Iphone13과 관련된 인스턴스를 만들어야 한다면 매번 아래와 같이 생성자에 일일히 Iphone13에 대한 정보를 작성해줘야한다. 개발을 진행하다가 이름 정보가 바뀐다면 인스턴스 생성부분을 하나하나 찾아서 코드를 수정해야한다. 하지만 Builder Pattern을 사용하면 인스턴스의 생성과 데이터 셋팅을 분리하므로 이런 문제점을 해결할 수 있다.

public class Main { 
	public static void main(String[] args){ 
		Phone phone1 = new Phone("Iphone13", "202110", "apple", false, false); 
	} 
}



GoF 스타일의 Builder Pattern을 구현해보자.

1. Phone 클래스

휴대폰이 가질수 있는 필드와 그에 대한 Setter와 Getter 메소드를 정의한다.

 

2. PhoneBuilder 인터페이스

PhoneBuilder 인터페이스는 자신을 구현하는 클래스가 휴대폰의 각종 정보를 설정할 수 있도록 메소드를 정의한다.

PhoneBuilder 클래스

3. IphoneBuilder 클래스

PhoneBuilder의 구현체이다. PhoneBuilder로부터 Overriding 받은 메소드에서 Phone의 Setter를 통해 데이터를 셋팅한다.

Iphone13Builder 클래스

 

4. Director 클래스

phoneBuilder의 구현체를 주입받아서 구현체의 함수들을 실행하는 역할을 한다. 이름처럼 Builder에게 일을 시키기만 하는 지시자 역할을 한다고 생각하면 쉽다.

Director 클래스

지금까지 구현한 클래스와 인터페이스의 다이어그램을 그려보면 아래와 같다.

클래스 다이어그램

 

실행

이제 실행 해보자. 먼저 생성할 휴대폰의 구현체를 만든다. 여기선 Iphone13Builder 클래스의 인스턴스를 생성한다.
Director 클래스의 생성자를 통해 Iphone13Builder 인스턴스를 주입한다.
director 인스턴스는 makePhone 및 getPhone 메소드를 실행함으써 원하는 휴대폰 모델의 데이터를 가진 Phone 클래스의 인스턴스를 얻을 수 있게된다.

main 메소드
실행결과

 

소감(?)

GoF의 Builder Pattern을 철저하게 따른 코드를 보면 Builder 인터페이스(위 예시에선 PhoneBuilder 인터페이스)와 그 구현체에서 Setter 메소드를 제공하지 않는다. 변경의 여지를 제거함으로써 Builder의 구현체를 좀더 신뢰성 있게 사용할 수 있게된다. 하지만 이런것은 처음에 봤던 생성자를 통해서도 충분히 가능한것이므로 장점이라 하기엔 애매하다.
하지만 BuilderPattern을 사용하면 인스턴스 생성에 대해 재사용성을 높이기 때문에 개발 중 수정이 발생 했을때 좀더 빠르게 대처가 가능하다. ( 이름이 변경된 경우에 Iphone13Builder 클래스의 BuildeName 메소드만 수정하면된다. )

하지만 단점도 명확히 보이는데 동적인 입력에 대한 인스턴스 생성이 불가능하다는 점이다.
새로운 모델이 추가된다면 소스코드를 수정해야한다. 가령 Iphone14가 출시되었다면 Iphone14Builder라는 클래스를 만들어야할텐데 사실 실무에서 새로운 모델이 추가되었다고 해서 소스코드를 수정하는 일은 거의 없다. 대부분이 Database에 추가하고 끝이다. ( 즉, 제품 정보같이 컬럼이 정형화된 데이터는 DB에 저장하지 위 예시처럼 Java 코드상에 클래스화 하지 않는다. )

개인적으로 Builder Pattern의 장점보다는 단점이 더욱 크게 다가와서 잘 사용하는 편은 아니다. Builder Pattern의 경우 생성해야할 인스턴스의 생성 범위가 한정적인 경우에만 좋고 동적으로 입력해서 인스턴스를 생성하는게 불가능하다. 실무에서는 대부분 제품 정보를 DB에 저장해두고 불러와서 데이터를 셋팅하는 식으로 쓰다보니까 GoF의 Builder Pattern은 자주 사용하지 않게 된다.
아래 stackexchange에서도 이부분에 대해서 언급을 하는데 Pure한 Builder Pattern은 잘 쓰지 않는다고 한다.

 

What are the advantages of Builder Pattern of GoF?

Update: Without fluent interface, builder pattern can still be done, see my implementation. Edit for possible duplication issues: When should the builder design pattern be used?: My question is a...

softwareengineering.stackexchange.com

 

Joshua Bloch's Builder Pattern


GoF의 Builder Pattern보다 실무에서 접할 기회가 더 많았던 조슈아 버전의 Builder Pattern을 소개한다. 구글링을 해보면 Builder Pattern이라고 하면 조슈아 버전이 정설(?)처럼 설명되고 있는것 같다.

조슈아 버전의 Builder Pattern도 GoF 버전처럼 데이터를 인스턴스를 생성할 때 한번 세팅하고 수정이 불가능하도록 한다. 다만 생성시에 사용자가 데이터를 직접 입력할 수 있다는 장점이 있다. 한마디로 인스턴스를 생성할때 동적으로 데이터 셋팅이 가능하며 한번 설정된 데이터는 수정이 불가능하다.

그렇다면 "그냥 클래스에서 Setter메소드를 제거하고 생성자를 통해 데이터를 셋팅하면 되는거 아냐?" 라는 생각이 들지만, 조슈아 Builder Pattern은 좀 더 명시적으로 인스턴스를 생성할 수 있도록 해준다.

어떤 뜻인지 소스코드를 통해 확인해보자.

Phone 클래스를 만든다. 인스턴스 생성 후에는 데이터 변경이 불가능 하도록 하기위해 생성자를 통해서 모든 정보를 입력받아야 한다.

public class Phone {
    private String name;
    private String realeseDate;
    private String companyName;
    private boolean isAndroid;
    private boolean isKoreaCompany;

    public Phone(String name, String realeseDate, String companyName, boolean isAndroid, boolean isKoreaCompany) {
        this.name = name;
        this.realeseDate = realeseDate;
        this.companyName = companyName;
        this.isAndroid = isAndroid;
        this.isKoreaCompany = isKoreaCompany;
    } 
    // getter 생략 
}


아래처럼 Phone에 대한 인스턴스를 생성할것인데, 문제는 파라미터에 대한 명시적인 정보가 없기 때문에 파라미터로 작성한 두개의 false가 어떤 정보를 의미하는지 한눈에 들어오지 않는다. 거기다 Phone 클래스 생성자의 파라미터 순서라도 바뀐다면 문제가 심각해진다. 특히 요즘은 Lombok을 사용해서 생성자를 자동 생성하는 경우가 흔한데 Lombok은 생성자를 클래스의 필드가 작성된 순서대로 생성자 파라미터를 만들기 때문에 아무 생각없이 필드의 순서를 바꿨다간 참사발생 가능성이 높아진다.

public class Main {
    public static void main(String[] args) {
        Phone iphone = new Phone("Iphone13", "2021", "apple", false, false);
        System.out.println("iphone.getName() = " + iphone.getName());
        System.out.println("iphone.getCompanyName() = " + iphone.getCompanyName());
    }
}



생성자를 통한 데이터 셋팅을 좀 더 명시적인 방법으로 푼 패턴이 조슈아 Build Pattern이다. 조금 복잡해 보이긴하나 핵심은 static nested class인 Builder이다. Builder 클래스에 있는 메소드들은 setter의 역할을 하는데 반환값으로 Builder 자신을 반환한다.
Builder 클래스의 마지막 메소드는 buildInstance인데 private로 설정된 Phone의 생성자를 호출한다. 단, 여기서 자신이 지금까지 작성했던 정보를 생성자의 파라미터로 전달한다.

Builder 클래스에 static 키워드가 붙어 있어 Thread-safe하지 않는 것처럼 보일수 있지만, nested static class는 사실 Phone의 인스턴스 없이 Builder에 접근이 가능하도록 해줄뿐 사용하려면 인스턴스를 생성해야한다. 결국 Thread-safe하니 안심하고 사용하면 된다.

public class Phone {
    private String name;
    private String realeseDate;
    private String companyName;
    private boolean isAndroid;
    private boolean isKoreaCompany;

    public static class Builder {
        private String name;
        private String realeseDate;
        private String companyName;
        private boolean isAndroid;
        private boolean isKoreaCompany;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder realeseDate(String realeseDate) {
            this.realeseDate = realeseDate;
            return this;
        }

        public Builder companyName(String companyName) {
            this.companyName = companyName;
            return this;
        }

        public Builder isAndroid(boolean isAndroid) {
            this.isAndroid = isAndroid;
            return this;
        }

        public Builder isKoreaCompany(boolean isKoreaCompany) {
            this.isKoreaCompany = isKoreaCompany;
            return this;
        }

        public Phone buildInstance() {
            return new Phone(this);
        }
    }

    private Phone(Builder builder) {
        this.name = builder.name;
        this.realeseDate = builder.realeseDate;
        this.companyName = builder.companyName;
        this.isAndroid = builder.isAndroid;
        this.isKoreaCompany = builder.isKoreaCompany;
    }

    public String getName() {
        return name;
    }

    public String getRealeseDate() {
        return realeseDate;
    }

    public String getCompanyName() {
        return companyName;
    }

    public boolean isAndroid() {
        return isAndroid;
    }

    public boolean isKoreaCompany() {
        return isKoreaCompany;
    }
}


실행코드를 살펴보자. 내가 어떤 필드의 데이터를 셋팅하는지 확인하기 수월해졌다. 거기다 데이터 셋팅이 필요없는 필드가 있는경우는 그냥 호출자체를 하지 않으면 된다. 이런 방식은 Spring 개발시 Setter 사용을 지양해야하는 Entity 클래스에 적용하면 꽤 유용하다.

public class Main {
    public static void main(String[] args) {
        Phone iphone = new Phone.Builder().companyName("apple").name("Iphone13").isKoreaCompany(false).isAndroid(false).realeseDate("2021").buildInstance();
        System.out.println("iphone.getName() = " + iphone.getName());
        System.out.println("iphone.getCompanyName() = " + iphone.getCompanyName());
    }
}

출력결과




참고자료


 

 

[Java] 내부(inner) 클래스와 내부(inner) static 클래스의 차이

이번 글에서는 클래스 안에 클래스가 존재하는 경우에 대해서 정리해보려 한다. public class Test { class InnerClass { // InnerClass } static class InnerStaticClass { // static InnerClass } } 내부 클래스..

devlog-wjdrbs96.tistory.com

 

Java의 내부 클래스는 static으로 선언하자

메모리를 더 먹고, 느리고, 바깥 클래스가 GC 대상에서 빠질 수 있다

johngrib.github.io

 

 

내부(inner) class와 내부(inner) static class 차이

개요 class MyClass { class InnerClass{} static class InnerStaticClass{} //내부 클래스에 static이 붙는다면? } 클래스 내부에 선언된 두개의 내부 클래스에 대한 차이점에 대해서 얘기해보겠습니다. 만약에 '..

siyoon210.tistory.com