[아이티센 부트캠프] 빌더 패턴 응용예제

이언덕·2026년 5월 14일

아이티센 부트캠프

목록 보기
95/115
post-thumbnail

1. 생성자 오버로딩으로 햄버거 객체 만들기 응용예제

이 예제는 햄버거 객체를 생성자 방식으로 만드는 흐름을 보여 준다.
햄버거에는 빵, 패티, 치즈, 양상추, 토마토, 베이컨 같은 재료가 들어간다.
이 재료들은 모두 숫자로 표현되고, 객체를 만들 때 생성자에 순서대로 전달된다.


여기서 중요한 점은 값이 많아질수록 생성자에 전달하는 숫자의 의미가 잘 보이지 않는다는 것이다.
new Hamburger1(1, 1, 1, 2, 1, 1)처럼 작성하면 객체는 만들 수 있다.
하지만 코드를 처음 보는 사람은 각 숫자가 빵인지, 패티인지, 치즈인지 바로 알기 어렵다.


이 예제의 핵심은 생성자 오버로딩으로 다양한 햄버거 객체를 만들 수는 있지만, 값이 많아질수록 코드의 의미를 읽기 어려워진다는 점이다.


예제 전체 코드

// Hamburger1.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

import lombok.Getter; // 필드 값을 읽는 메서드 자동 생성
import lombok.Setter; // 필드 값을 바꾸는 메서드 자동 생성
import lombok.ToString; // 객체 정보를 문자열로 만드는 메서드 자동 생성

@Getter // 필드 값을 읽는 메서드 자동 생성
@Setter // 필드 값을 바꾸는 메서드 자동 생성
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger1 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수

    public Hamburger1(int bun, int patty, int cheese, int lettuce, int tomato, int bacon) {
        this.bun = bun; // 전달받은 빵 개수 저장
        this.patty = patty; // 전달받은 패티 개수 저장
        this.cheese = cheese; // 전달받은 치즈 개수 저장
        this.lettuce = lettuce; // 전달받은 양상추 개수 저장
        this.tomato = tomato; // 전달받은 토마토 개수 저장
        this.bacon = bacon; // 전달받은 베이컨 개수 저장
    }

    public Hamburger1(int bun, int patty, int cheese, int lettuce, int tomato) {
        this.bun = bun; // 전달받은 빵 개수 저장
        this.patty = patty; // 전달받은 패티 개수 저장
        this.cheese = cheese; // 전달받은 치즈 개수 저장
        this.lettuce = lettuce; // 전달받은 양상추 개수 저장
        this.tomato = tomato; // 전달받은 토마토 개수 저장
    }

    public Hamburger1(int bun, int patty, int cheese, int lettuce) {
        this.bun = bun; // 전달받은 빵 개수 저장
        this.patty = patty; // 전달받은 패티 개수 저장
        this.cheese = cheese; // 전달받은 치즈 개수 저장
        this.lettuce = lettuce; // 전달받은 양상추 개수 저장
    }

    public Hamburger1(int bun, int patty, int cheese) {
        this.bun = bun; // 전달받은 빵 개수 저장
        this.patty = patty; // 전달받은 패티 개수 저장
        this.cheese = cheese; // 전달받은 치즈 개수 저장
    }
}
// Hamburger1App.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

public class Hamburger1App {
    public static void main(String[] args) {
        Hamburger1 dooly = new Hamburger1(1, 1, 1, 2, 1, 1); // 모든 재료 값을 전달해 둘리 햄버거 생성
        Hamburger1 ddochi = new Hamburger1(1, 1, 0, 2, 2, 1); // 모든 재료 값을 전달해 또치 햄버거 생성
        Hamburger1 dounar = new Hamburger1(1, 2, 1, 0, 0, 2); // 모든 재료 값을 전달해 도우너 햄버거 생성
        Hamburger1 gogildong = new Hamburger1(1, 2, 3); // 빵, 패티, 치즈만 전달해 고길동 햄버거 생성
        Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 빵과 베이컨 중심으로 마이콜 햄버거 생성

        System.out.printf("둘리의 햄버거 : %s %n", dooly); // 둘리 햄버거 정보 출력
        System.out.printf("또치의 햄버거 : %s %n", ddochi); // 또치 햄버거 정보 출력
        System.out.printf("도우너의 햄버거 : %s %n", dounar); // 도우너 햄버거 정보 출력
        System.out.printf("고길동의 햄버거 : %s %n", gogildong); // 고길동 햄버거 정보 출력
        System.out.printf("마이콜의 햄버거 : %s %n", micol); // 마이콜 햄버거 정보 출력
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 Hamburger1.java는 햄버거 객체의 구조를 정의하는 클래스이다.
두 번째 Hamburger1App.java는 실제로 햄버거 객체를 만들고 출력하는 실행 클래스이다.


Hamburger1 클래스에는 빵, 패티, 치즈, 양상추, 토마토, 베이컨을 저장할 필드가 있다.
그리고 전달받는 재료 개수에 따라 생성자가 여러 개 준비되어 있다.
이처럼 같은 이름의 생성자를 매개변수 개수나 구성이 다르게 여러 개 만드는 방식을 생성자 오버로딩이라고 한다.


Hamburger1App에서는 new Hamburger1(...)을 사용해서 햄버거 객체를 만든다.
이때 생성자에 숫자를 순서대로 넣는다.
생성자는 전달받은 숫자를 bun, patty, cheese, lettuce, tomato, bacon 필드에 저장한다.


이 예제는 생성자 오버로딩으로 객체를 만들 수 있다는 점과, 생성자에 값이 많아질수록 코드가 읽기 어려워진다는 점을 동시에 보여 준다.


이 예제에서 확인할 핵심

  • Hamburger1 클래스는 햄버거 객체를 만들기 위한 설계도이다.
  • bun, patty, cheese, lettuce, tomato, bacon은 햄버거 재료 개수를 저장하는 필드이다.
  • 생성자는 객체가 만들어질 때 전달받은 값을 필드에 저장한다.
  • 생성자 오버로딩은 같은 이름의 생성자를 매개변수 구성이 다르게 여러 개 만드는 방식이다.
  • new Hamburger1(...)을 실행하면 전달한 값의 개수와 타입에 맞는 생성자가 호출된다.
  • 생성자에서 값을 저장하지 않은 int 필드는 기본값 0이 남는다.
  • @ToString 덕분에 객체를 출력할 때 자동 생성된 문자열 표현이 사용된다.
  • 생성자 방식은 코드가 짧지만 숫자의 의미가 바로 드러나지 않는다.

이 예제는 Builder Pattern으로 넘어가기 전에, 생성자 방식이 왜 불편해질 수 있는지 확인하는 출발점이다.


생성자 오버로딩 다시 복기하기

생성자는 객체가 만들어질 때 처음 실행되는 특별한 메서드이다.
new Hamburger1(...)을 실행하면 Hamburger1 객체가 만들어지고, 그 순간 생성자가 호출된다.


생성자의 역할은 외부에서 전달받은 값을 객체 안의 필드에 저장하는 것이다.
예를 들어 빵 개수, 패티 개수, 치즈 개수를 전달하면 생성자는 그 값을 각각 bun, patty, cheese 필드에 저장한다.


생성자 오버로딩은 생성자를 여러 개 만드는 방식이다.
단, 이름은 모두 클래스 이름과 같아야 한다.
대신 매개변수 개수나 매개변수 구성이 달라야 한다.


이 예제에서는 아래처럼 생성자가 여러 개 있다.

  • 재료 6개를 받는 생성자
  • 재료 5개를 받는 생성자
  • 재료 4개를 받는 생성자
  • 재료 3개를 받는 생성자

생성자를 여러 개 만든 이유는 햄버거마다 필요한 재료 개수가 다를 수 있기 때문이다.
어떤 햄버거는 모든 재료를 받을 수 있고, 어떤 햄버거는 빵, 패티, 치즈만 받을 수도 있다.


생성자 오버로딩은 객체를 만들 때 전달하는 값의 개수나 구성을 다르게 받기 위해 사용한다.
하지만 생성자가 많아질수록 어떤 생성자를 써야 하는지 헷갈릴 수 있다.


Hamburger1 클래스는 햄버거 객체의 설계도이다

// Hamburger1.java
@Getter // 필드 값을 읽는 메서드 자동 생성
@Setter // 필드 값을 바꾸는 메서드 자동 생성
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger1 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}

Hamburger1 클래스는 햄버거 객체를 만들기 위한 설계도이다.
여기서 설계도라는 말은 햄버거 객체가 어떤 값을 가질 수 있는지 미리 정해 둔 구조라는 뜻이다.


이 클래스에는 여섯 개의 필드가 있다.
필드는 객체 안에 저장되는 값이다.
bun은 빵 개수, patty는 패티 개수, cheese는 치즈 개수, lettuce는 양상추 개수, tomato는 토마토 개수, bacon은 베이컨 개수를 의미한다.


모든 필드의 타입은 int이다.
int는 정수를 저장하는 타입이다.
이 예제에서는 재료가 몇 개 들어가는지 숫자로 표현하기 때문에 int 타입을 사용한다.


private은 클래스 밖에서 필드에 직접 접근하지 못하게 막는 접근 제한자이다.
즉, bun, patty, cheese 같은 필드에 외부 코드가 바로 접근하는 것을 막는다.
대신 @Getter, @Setter를 사용해 값을 읽거나 바꾸는 메서드를 Lombok이 자동으로 만들어 준다.


@ToString은 객체를 출력할 때 필드 값이 보이도록 toString() 메서드를 자동으로 만들어 준다.
그래서 System.out.printf("%s", dooly)처럼 객체를 출력하면 객체의 필드 값이 문자열로 표시된다.


이 클래스는 햄버거를 직접 출력하거나 실행하는 클래스가 아니다.
햄버거 객체가 어떤 재료 값을 가질 수 있는지 정해 두는 역할을 한다.


재료 6개를 받는 생성자

// Hamburger1.java
public Hamburger1(int bun, int patty, int cheese, int lettuce, int tomato, int bacon) {
    this.bun = bun; // 전달받은 빵 개수 저장
    this.patty = patty; // 전달받은 패티 개수 저장
    this.cheese = cheese; // 전달받은 치즈 개수 저장
    this.lettuce = lettuce; // 전달받은 양상추 개수 저장
    this.tomato = tomato; // 전달받은 토마토 개수 저장
    this.bacon = bacon; // 전달받은 베이컨 개수 저장
}

이 생성자는 재료 6개를 모두 받는다.
빵, 패티, 치즈, 양상추, 토마토, 베이컨 값을 한 번에 전달받는다.


this.bun = bun은 현재 만들어지는 객체의 bun 필드에 매개변수 bun 값을 저장한다는 뜻이다.
여기서 왼쪽의 this.bun은 객체 안의 필드이고, 오른쪽의 bun은 생성자가 전달받은 매개변수이다.


초보자가 가장 많이 헷갈리는 부분은 이름이 같다는 점이다.
this.bun과 bun은 이름이 비슷하지만 역할이 다르다.
this.bun은 객체 안에 저장되는 값이고, bun은 생성자로 잠깐 전달된 값이다.


이 생성자를 사용하면 모든 재료 값을 직접 지정할 수 있다.
예를 들어 둘리 햄버거는 아래처럼 만든다.

// Hamburger1App.java
Hamburger1 dooly = new Hamburger1(1, 1, 1, 2, 1, 1); // 빵, 패티, 치즈, 양상추, 토마토, 베이컨 순서

이 코드는 순서대로 보면 다음과 같다.

  • 첫 번째 1은 bun에 들어간다.
  • 두 번째 1은 patty에 들어간다.
  • 세 번째 1은 cheese에 들어간다.
  • 네 번째 2는 lettuce에 들어간다.
  • 다섯 번째 1은 tomato에 들어간다.
  • 여섯 번째 1은 bacon에 들어간다.

생성자 방식은 값을 순서대로 넣기 때문에, 순서를 정확히 알아야 올바른 필드에 값이 저장된다.


재료 개수에 따라 생성자를 여러 개 만든 이유

// Hamburger1.java
public Hamburger1(int bun, int patty, int cheese, int lettuce, int tomato) {
    this.bun = bun; // 전달받은 빵 개수 저장
    this.patty = patty; // 전달받은 패티 개수 저장
    this.cheese = cheese; // 전달받은 치즈 개수 저장
    this.lettuce = lettuce; // 전달받은 양상추 개수 저장
    this.tomato = tomato; // 전달받은 토마토 개수 저장
}

public Hamburger1(int bun, int patty, int cheese, int lettuce) {
    this.bun = bun; // 전달받은 빵 개수 저장
    this.patty = patty; // 전달받은 패티 개수 저장
    this.cheese = cheese; // 전달받은 치즈 개수 저장
    this.lettuce = lettuce; // 전달받은 양상추 개수 저장
}

public Hamburger1(int bun, int patty, int cheese) {
    this.bun = bun; // 전달받은 빵 개수 저장
    this.patty = patty; // 전달받은 패티 개수 저장
    this.cheese = cheese; // 전달받은 치즈 개수 저장
}

이 세 생성자는 재료를 일부만 받는다.
첫 번째 생성자는 재료 5개를 받고, 두 번째 생성자는 재료 4개를 받고, 세 번째 생성자는 재료 3개를 받는다.


이렇게 생성자를 여러 개 만들면 객체를 만들 때 필요한 만큼만 값을 전달할 수 있다.
예를 들어 고길동 햄버거는 빵, 패티, 치즈만 전달한다.

// Hamburger1App.java
Hamburger1 gogildong = new Hamburger1(1, 2, 3); // 빵, 패티, 치즈만 전달

이 코드는 매개변수 3개를 받는 생성자를 호출한다.
그래서 bun에는 1, patty에는 2, cheese에는 3이 저장된다.


그런데 이 생성자에서는 lettuce, tomato, bacon 값을 저장하지 않는다.
이 필드들은 int 타입이므로 객체가 만들어질 때 기본값인 0을 가진다.
따라서 생성자에서 값을 따로 저장하지 않으면 그 기본값 0이 그대로 남는다.


즉, 고길동 햄버거는 아래처럼 만들어진다.

  • bun은 1이다.
  • patty는 2이다.
  • cheese는 3이다.
  • lettuce는 생성자에서 저장하지 않았으므로 0이다.
  • tomato는 생성자에서 저장하지 않았으므로 0이다.
  • bacon은 생성자에서 저장하지 않았으므로 0이다.

생성자 오버로딩을 사용하면 필요한 값만 받을 수 있지만, 생성자가 많아질수록 코드가 반복된다.


Hamburger1App에서 객체를 만드는 흐름

// Hamburger1App.java
Hamburger1 dooly = new Hamburger1(1, 1, 1, 2, 1, 1); // 둘리 햄버거
Hamburger1 ddochi = new Hamburger1(1, 1, 0, 2, 2, 1); // 또치 햄버거
Hamburger1 dounar = new Hamburger1(1, 2, 1, 0, 0, 2); // 도우너 햄버거
Hamburger1 gogildong = new Hamburger1(1, 2, 3); // 고길동 햄버거
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 마이콜 햄버거

Hamburger1App의 main() 메서드에서는 햄버거 객체 5개를 만든다.
main() 메서드는 Java 프로그램을 실행할 때 시작점이 되는 메서드이다.


각 객체는 new Hamburger1(...)로 만들어진다.
new는 새로운 객체를 만들 때 사용하는 키워드이다.
즉, new Hamburger1(...)은 Hamburger1 클래스 구조를 바탕으로 실제 햄버거 객체 하나를 만든다는 뜻이다.


여기서 dooly, ddochi, dounar, gogildong, micol은 각각 만들어진 햄버거 객체를 담는 변수이다.
같은 Hamburger1 클래스로 만들었지만 전달한 재료 값이 다르기 때문에 서로 다른 햄버거 객체가 된다.


이 흐름은 아래처럼 정리할 수 있다.

  • dooly는 재료 6개를 모두 전달해서 만든다.
  • ddochi도 재료 6개를 모두 전달해서 만든다.
  • dounar도 재료 6개를 모두 전달해서 만든다.
  • gogildong은 재료 3개만 전달해서 만든다.
  • micol은 재료 6개를 전달하지만 중간 재료를 0으로 직접 넣는다.

같은 클래스에서 만든 객체라도 생성자에 어떤 값을 전달하느냐에 따라 객체 안에 저장되는 값이 달라진다.


생성자 방식에서 값의 순서가 중요한 이유

생성자 방식은 값을 순서대로 전달한다.
그래서 코드가 짧아 보일 수 있다.
하지만 값의 의미가 코드에 직접 드러나지 않는다는 문제가 있다.


예를 들어 아래 코드를 보자.

// Hamburger1App.java
Hamburger1 ddochi = new Hamburger1(1, 1, 0, 2, 2, 1); // 숫자만 보면 재료 의미가 바로 보이지 않음

이 코드는 또치 햄버거를 만든다.
하지만 1, 1, 0, 2, 2, 1만 보고 각 숫자가 어떤 재료인지 바로 알기 어렵다.


이 코드를 정확히 이해하려면 Hamburger1 생성자의 매개변수 순서를 다시 확인해야 한다.
생성자 순서는 bun, patty, cheese, lettuce, tomato, bacon이다.
그래서 또치 햄버거는 아래처럼 해석된다.

  • bun은 1이다.
  • patty는 1이다.
  • cheese는 0이다.
  • lettuce는 2이다.
  • tomato는 2이다.
  • bacon은 1이다.

문제는 모든 값이 int라는 점이다.
모두 숫자이기 때문에 치즈 자리에 토마토 값을 잘못 넣어도 문법 오류가 나지 않을 수 있다.
하지만 객체 안에는 잘못된 값이 저장된다.


생성자 방식은 값의 순서가 틀려도 타입이 같으면 문법 오류가 나지 않을 수 있어서 실수를 찾기 어렵다.
이 점이 Builder Pattern이 필요한 이유와 연결된다.


고길동과 마이콜 객체에서 보이는 차이

고길동과 마이콜 객체를 비교하면 생성자 오버로딩 방식의 특징이 더 잘 보인다.

// Hamburger1App.java
Hamburger1 gogildong = new Hamburger1(1, 2, 3); // 재료 3개만 전달
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 재료 6개를 모두 전달

고길동은 매개변수 3개짜리 생성자를 사용한다.
그래서 빵, 패티, 치즈만 직접 저장된다.
나머지 양상추, 토마토, 베이컨은 생성자에서 저장하지 않았기 때문에 기본값 0이 남는다.


반면 마이콜은 재료 6개를 받는 생성자를 사용한다.
빵과 베이컨만 의미 있는 값으로 넣고, 패티, 치즈, 양상추, 토마토는 직접 0을 넣는다.


두 방식은 결과적으로 사용하지 않는 재료가 0이 된다는 점은 비슷하다.
하지만 코드의 의도는 다르게 보인다.
고길동은 아예 짧은 생성자를 사용해서 나머지 재료를 생략한 것이다.
마이콜은 긴 생성자를 사용하면서 필요 없는 재료에 0을 직접 넣은 것이다.


이 차이 때문에 생성자 방식은 선택값이 많을수록 코드가 지저분해질 수 있다.
사용하지 않는 값을 표시하기 위해 0을 여러 개 넣어야 하기 때문이다.


선택값이 많아질수록 생성자 방식은 값을 생략하거나 0으로 채우는 코드가 늘어난다.
이런 불편함을 줄이기 위해 다음 예제에서는 직접 만든 Builder 방식을 사용한다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • Hamburger1App의 main() 메서드가 실행된다.
  • new Hamburger1(...)이 실행되면서 햄버거 객체가 만들어진다.
  • 전달한 숫자의 개수에 맞는 생성자가 선택된다.
  • 생성자는 전달받은 값을 this.bun, this.patty, this.cheese 같은 필드에 저장한다.
  • 생성자에서 값을 저장하지 않은 int 필드는 기본값 0이 남는다.
  • System.out.printf()가 각 햄버거 객체를 출력한다.
  • @ToString이 만든 toString() 덕분에 객체의 필드 값이 콘솔에 보인다.

흐름을 한 줄로 정리하면 다음과 같다.


main() 실행 → new Hamburger1(...) 호출 → 생성자 선택 → 필드 값 저장 → 객체 출력


생성자 방식은 객체를 빠르게 만들 수 있지만, 값이 많아질수록 생성자 순서를 계속 확인해야 한다.


결과물 뽑기

이 예제는 웹 화면이 아니라 Java 실행 예제이다.
따라서 결과물은 브라우저가 아니라 IntelliJ 콘솔에서 확인한다.


실행 방법은 아래와 같다.

  • Hamburger1App.java 파일을 연다.
  • main() 메서드 왼쪽의 실행 버튼을 누른다.
  • 또는 파일에서 마우스 오른쪽 클릭 후 Run 'Hamburger1App.main()'을 선택한다.
  • 실행 후 IntelliJ 하단 콘솔에서 출력 결과를 확인한다.

예상 출력 결과는 아래처럼 볼 수 있다.

// 출력결과
// 둘리의 햄버거 : Hamburger1(bun=1, patty=1, cheese=1, lettuce=2, tomato=1, bacon=1)
// 또치의 햄버거 : Hamburger1(bun=1, patty=1, cheese=0, lettuce=2, tomato=2, bacon=1)
// 도우너의 햄버거 : Hamburger1(bun=1, patty=2, cheese=1, lettuce=0, tomato=0, bacon=2)
// 고길동의 햄버거 : Hamburger1(bun=1, patty=2, cheese=3, lettuce=0, tomato=0, bacon=0)
// 마이콜의 햄버거 : Hamburger1(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

출력 결과에서 확인할 부분은 두 가지이다.
첫 번째는 생성자에 전달한 값이 필드에 순서대로 저장되었는지이다.
둘리, 또치, 도우너, 마이콜은 재료 6개를 전달했기 때문에 여섯 필드 값이 모두 생성자 인자에 따라 저장된다.


두 번째는 고길동 객체의 값이다.
고길동은 new Hamburger1(1, 2, 3)으로 재료 3개만 전달했다.
그래서 bun, patty, cheese만 값이 들어가고, 나머지 lettuce, tomato, bacon은 0으로 출력된다.


이 출력 결과가 Hamburger1 객체의 값이 보이는 이유는 @ToString 때문이다.
@ToString은 객체를 출력할 때 사용할 문자열 표현을 자동으로 만들어 준다.
그래서 객체를 출력해도 단순한 주소값처럼 보이지 않고, 각 필드와 값이 함께 보인다.


캡처를 남긴다면 IntelliJ 콘솔에 출력된 다섯 줄 결과를 캡처하면 된다.
이 결과가 생성자 오버로딩 방식으로 객체가 어떻게 만들어졌는지 보여 주는 최종 결과물이다.


핵심 정리

이 예제의 핵심은 생성자 오버로딩으로 햄버거 객체를 만드는 방식이다.
Hamburger1 클래스는 햄버거 객체가 가질 재료 필드를 정의하고, 재료 개수에 따라 여러 생성자를 제공한다.
Hamburger1App은 이 생성자들을 사용해서 여러 햄버거 객체를 만든다.


이 흐름은 아래처럼 정리할 수 있다.

  • Hamburger1은 햄버거 객체를 만들기 위한 클래스이다.
  • bun, patty, cheese, lettuce, tomato, bacon은 햄버거 재료 개수를 저장하는 필드이다.
  • 생성자는 객체가 만들어질 때 전달받은 값을 필드에 저장한다.
  • 생성자 오버로딩은 같은 이름의 생성자를 매개변수 구성이 다르게 여러 개 만드는 방식이다.
  • new Hamburger1(1, 1, 1, 2, 1, 1)은 재료 6개를 받는 생성자를 호출한다.
  • new Hamburger1(1, 2, 3)은 재료 3개를 받는 생성자를 호출한다.
  • 생성자에서 값을 저장하지 않은 int 필드는 기본값 0이 남는다.
  • @ToString 덕분에 객체를 출력하면 필드 값이 문자열로 보인다.
  • 생성자 방식은 간단하지만, 값이 많아지면 각 숫자의 의미를 바로 알기 어렵다.
  • 같은 타입의 값이 여러 개일 때는 순서를 잘못 넣어도 문법 오류가 나지 않을 수 있다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
생성자 오버로딩은 다양한 방식으로 객체를 만들 수 있게 해 주지만, 값이 많고 선택값이 많아질수록 어떤 값이 어떤 필드에 들어가는지 읽기 어려워진다.
이 문제를 줄이기 위해 다음 예제에서는 직접 만든 Builder 방식으로 햄버거 객체를 생성한다.



2. 직접 만든 Builder로 햄버거 객체 만들기 응용예제

이 예제는 생성자 오버로딩 대신 직접 만든 Builder를 사용해서 햄버거 객체를 만드는 흐름을 보여 준다.
앞의 예제에서는 new Hamburger1(1, 1, 1, 2, 1, 1)처럼 숫자를 생성자에 순서대로 넣었다.
그 방식은 객체를 만들 수는 있지만, 각 숫자가 어떤 재료를 의미하는지 바로 알기 어렵다.


이번 예제에서는 Hamburger2.builder()로 객체 생성 흐름을 시작한다.
그다음 .bun(1), .patty(1), .cheese(1)처럼 재료 이름이 붙은 메서드를 사용해서 값을 넣는다.
마지막에는 .build()를 호출해서 최종 Hamburger2 객체를 만든다.


이 예제의 핵심은 생성자에 값을 순서대로 넣는 방식이 아니라, 재료 이름이 붙은 메서드로 값을 하나씩 설정한 뒤 마지막에 객체를 완성하는 것이다.


예제 전체 코드

// Hamburger2.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

import lombok.*; // 원본 코드에 포함된 Lombok 관련 import

public class Hamburger2 {
    private final int bun; // 빵 개수
    private final int patty; // 패티 개수
    private final int cheese; // 치즈 개수
    private final int lettuce; // 양상추 개수
    private final int tomato; // 토마토 개수
    private final int bacon; // 베이컨 개수

    // 외부에서 Builder 객체를 시작할 수 있게 해주는 static 메서드
    public static Builder builder() {
        return new Builder(); // 새 Builder 객체 반환
    }

    // Hamburger2 객체를 만들기 전에 값을 모아 두는 내부 Builder 클래스
    public static class Builder {
        private int bun = 0; // 빵 기본값
        private int patty = 0; // 패티 기본값
        private int cheese = 0; // 치즈 기본값
        private int lettuce = 0; // 양상추 기본값
        private int tomato = 0; // 토마토 기본값
        private int bacon = 0; // 베이컨 기본값

        public Builder bun(int val) {
            this.bun = val; // 빵 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Builder patty(int val) {
            this.patty = val; // 패티 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Builder cheese(int val) {
            this.cheese = val; // 치즈 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Builder lettuce(int val) {
            this.lettuce = val; // 양상추 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Builder tomato(int val) {
            this.tomato = val; // 토마토 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Builder bacon(int val) {
            this.bacon = val; // 베이컨 개수 저장
            return this; // 같은 Builder 객체 반환
        }

        public Hamburger2 build() {
            return new Hamburger2(this); // Builder에 모은 값으로 Hamburger2 객체 생성
        }
    }

    private Hamburger2(Builder builder) {
        this.bun = builder.bun; // Builder의 빵 값을 최종 객체에 저장
        this.patty = builder.patty; // Builder의 패티 값을 최종 객체에 저장
        this.cheese = builder.cheese; // Builder의 치즈 값을 최종 객체에 저장
        this.lettuce = builder.lettuce; // Builder의 양상추 값을 최종 객체에 저장
        this.tomato = builder.tomato; // Builder의 토마토 값을 최종 객체에 저장
        this.bacon = builder.bacon; // Builder의 베이컨 값을 최종 객체에 저장
    }

    @Override
    public String toString() {
        return String.format("Hamburger [bun=%d, patty=%d, cheese=%d, lettuce=%d, tomato=%d, bacon=%d]",
                bun, patty, cheese, lettuce, tomato, bacon); // 객체의 재료 값을 문자열로 반환
    }
}
// Hamburger2App.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

public class Hamburger2App {
    public static void main(String[] args) {
        Hamburger2 dooly = Hamburger2.builder()
                .bun(1) // 빵 개수 설정
                .patty(1) // 패티 개수 설정
                .cheese(1) // 치즈 개수 설정
                .lettuce(2) // 양상추 개수 설정
                .tomato(1) // 토마토 개수 설정
                .bacon(1) // 베이컨 개수 설정
                .build(); // Hamburger2 객체 생성

        Hamburger2 ddochi = Hamburger2.builder()
                .bun(1) // 빵 개수 설정
                .patty(1) // 패티 개수 설정
                .cheese(0) // 치즈 개수 설정
                .lettuce(2) // 양상추 개수 설정
                .tomato(2) // 토마토 개수 설정
                .bacon(1) // 베이컨 개수 설정
                .build(); // Hamburger2 객체 생성

        Hamburger2 dounar = Hamburger2.builder()
                .bun(1) // 빵 개수 설정
                .patty(2) // 패티 개수 설정
                .cheese(1) // 치즈 개수 설정
                .lettuce(0) // 양상추 개수 설정
                .tomato(0) // 토마토 개수 설정
                .bacon(2) // 베이컨 개수 설정
                .build(); // Hamburger2 객체 생성

        Hamburger2 gogildong = Hamburger2.builder()
                .bun(1) // 빵 개수 설정
                .patty(2) // 패티 개수 설정
                .cheese(3) // 치즈 개수 설정
                .build(); // 설정하지 않은 재료는 기본값 0 사용

        Hamburger2 micol = Hamburger2.builder()
                .bun(2) // 빵 개수 설정
                .bacon(6) // 베이컨 개수 설정
                .build(); // 설정하지 않은 재료는 기본값 0 사용

        System.out.printf("둘리의 햄버거 : %s %n", dooly); // 둘리 햄버거 정보 출력
        System.out.printf("또치의 햄버거 : %s %n", ddochi); // 또치 햄버거 정보 출력
        System.out.printf("도우너의 햄버거 : %s %n", dounar); // 도우너 햄버거 정보 출력
        System.out.printf("고길동의 햄버거 : %s %n", gogildong); // 고길동 햄버거 정보 출력
        System.out.printf("마이콜의 햄버거 : %s %n", micol); // 마이콜 햄버거 정보 출력
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 Hamburger2.java는 직접 만든 Builder 구조를 가진 햄버거 클래스이다.
두 번째 Hamburger2App.java는 Builder를 사용해서 실제 햄버거 객체를 만들고 출력하는 실행 클래스이다.


Hamburger2 클래스 안에는 Builder라는 내부 클래스가 있다.
이 내부 Builder 클래스는 최종 객체를 바로 만드는 클래스가 아니다.
최종 Hamburger2 객체를 만들기 전에 빵, 패티, 치즈, 양상추, 토마토, 베이컨 값을 임시로 모아 두는 역할을 한다.


Hamburger2App에서는 Hamburger2.builder()로 Builder 객체를 가져온다.
그다음 재료 이름이 붙은 메서드로 값을 하나씩 설정한다.
마지막에 .build()를 호출하면 Builder에 모아 둔 값으로 Hamburger2 객체가 만들어진다.


이 예제는 직접 만든 Builder가 값을 임시로 모으고, build()가 최종 객체를 만드는 흐름을 보여 준다.


이 예제에서 확인할 핵심

  • Hamburger2는 직접 만든 Builder를 사용해 객체를 생성한다.
  • builder()는 객체 생성 흐름을 시작하는 메서드이다.
  • Builder 내부 클래스는 최종 객체를 만들기 전에 값을 임시로 저장한다.
  • .bun(1), .patty(1) 같은 메서드는 각 재료 값을 Builder 안에 저장한다.
  • 각 설정 메서드는 return this를 통해 같은 Builder 객체를 다시 반환한다.
  • return this가 있기 때문에 .bun().patty().cheese()처럼 메서드를 이어서 호출할 수 있다.
  • build()는 Builder에 모아 둔 값으로 최종 Hamburger2 객체를 만든다.
  • 설정하지 않은 재료는 Builder 내부 기본값 0이 사용된다.
  • private 생성자는 외부에서 생성자를 직접 호출하는 흐름을 막고, build() 내부에서 최종 객체가 만들어지게 한다.

이 예제는 생성자에 숫자를 순서대로 넣는 방식보다, 어떤 재료에 어떤 값을 넣는지 더 잘 보이게 만드는 예제이다.


Builder Pattern 다시 복기하기

Builder Pattern은 객체를 만들 때 필요한 값을 한 번에 생성자에 넣지 않고, 단계별로 설정한 뒤 마지막에 객체를 만드는 방식이다.
여기서 pattern은 자주 반복되는 문제를 해결하기 위해 정리된 설계 방식이라고 이해하면 된다.


앞의 생성자 방식에서는 아래처럼 값을 넣었다.

// Hamburger1App.java
Hamburger1 dooly = new Hamburger1(1, 1, 1, 2, 1, 1); // 숫자 순서로 재료 전달

이 코드는 짧지만 숫자만 보고 재료 의미를 바로 알기 어렵다.
첫 번째 1이 빵인지, 두 번째 1이 패티인지, 네 번째 2가 양상추인지 코드를 읽는 사람이 생성자 순서를 확인해야 한다.


Builder 방식에서는 아래처럼 값을 넣는다.

// Hamburger2App.java
Hamburger2 dooly = Hamburger2.builder()
        .bun(1) // 빵 개수 설정
        .patty(1) // 패티 개수 설정
        .cheese(1) // 치즈 개수 설정
        .lettuce(2) // 양상추 개수 설정
        .tomato(1) // 토마토 개수 설정
        .bacon(1) // 베이컨 개수 설정
        .build(); // 객체 생성

이 코드는 생성자 방식보다 길다.
하지만 각 값 앞에 .bun(), .patty(), .cheese()처럼 이름이 붙어 있다.
그래서 어떤 재료에 어떤 값이 들어가는지 바로 보인다.


Builder Pattern은 객체 생성 코드가 조금 길어지더라도, 값의 의미가 코드에 드러나도록 만드는 방식이다.


Hamburger2 클래스의 필드는 final로 선언되어 있다

// Hamburger2.java
public class Hamburger2 {
    private final int bun; // 빵 개수
    private final int patty; // 패티 개수
    private final int cheese; // 치즈 개수
    private final int lettuce; // 양상추 개수
    private final int tomato; // 토마토 개수
    private final int bacon; // 베이컨 개수
}

Hamburger2 클래스에는 여섯 개의 필드가 있다.
각 필드는 햄버거에 들어가는 재료 개수를 저장한다.
bun은 빵, patty는 패티, cheese는 치즈, lettuce는 양상추, tomato는 토마토, bacon은 베이컨을 의미한다.


여기서 필드 앞에 final이 붙어 있다.
final은 한 번 값이 정해지면 다시 바꿀 수 없다는 뜻이다.
즉, Hamburger2 객체가 만들어질 때 재료 값이 정해지면, 그 객체의 재료 값을 나중에 바꾸지 않는 구조이다.


이 구조는 Builder 방식과 잘 어울린다.
객체를 만들기 전에는 Builder에 값을 모아 둔다.
그리고 build()가 호출되면 Builder에 모인 값이 private Hamburger2(Builder builder) 생성자를 통해 final 필드에 저장된다.
최종 객체가 만들어진 뒤에는 그 필드 값을 다시 바꾸지 않는다.


Builder는 객체를 만들기 전까지 값을 모으고, 최종 객체가 만들어질 때 그 값을 final 필드에 확정해서 저장하는 흐름과 잘 어울린다.


builder 메서드는 Builder 객체를 시작하는 입구이다

// Hamburger2.java
public static Builder builder() {
    return new Builder(); // 새 Builder 객체 반환
}

builder() 메서드는 Builder 객체를 시작하는 입구이다.
Hamburger2App에서 Hamburger2.builder()를 호출할 수 있는 이유가 바로 이 메서드 때문이다.


여기서 static은 객체를 만들지 않고도 클래스 이름으로 바로 호출할 수 있게 해 주는 키워드이다.
즉, 아직 Hamburger2 객체가 없어도 Hamburger2.builder()를 호출할 수 있다.


builder()는 최종 Hamburger2 객체를 바로 반환하지 않는다.
대신 new Builder()를 반환한다.
즉, 값을 모아 둘 준비 객체를 먼저 만들어서 돌려준다.


흐름을 정리하면 아래와 같다.

  • Hamburger2.builder()를 호출한다.
  • builder() 메서드가 실행된다.
  • new Builder()로 새 Builder 객체를 만든다.
  • 그 Builder 객체를 반환한다.
  • 반환된 Builder 객체에 .bun(), .patty() 같은 메서드를 이어서 호출한다.

builder()는 최종 객체를 만드는 메서드가 아니라, 값을 모으기 위한 Builder 객체를 시작하는 메서드이다.


내부 Builder 클래스는 값을 임시로 모아 둔다

// Hamburger2.java
public static class Builder {
    private int bun = 0; // 빵 기본값
    private int patty = 0; // 패티 기본값
    private int cheese = 0; // 치즈 기본값
    private int lettuce = 0; // 양상추 기본값
    private int tomato = 0; // 토마토 기본값
    private int bacon = 0; // 베이컨 기본값
}

Builder 클래스는 Hamburger2 클래스 안에 들어 있는 내부 클래스이다.
내부 클래스는 클래스 안에 정의된 또 다른 클래스라고 이해하면 된다.


이 Builder 클래스는 최종 햄버거 객체가 아니다.
최종 Hamburger2 객체를 만들기 전에 재료 값을 잠시 저장해 두는 준비 객체이다.


Builder 안에도 bun, patty, cheese, lettuce, tomato, bacon 필드가 있다.
이 필드들은 최종 객체의 필드가 아니라, 최종 객체를 만들기 전 임시로 값을 모아 두는 공간이다.


각 필드는 처음에 0으로 초기화되어 있다.
초기화는 값을 처음 정해 두는 것을 의미한다.
그래서 어떤 재료 메서드를 호출하지 않으면 그 재료 값은 기본값 0으로 남는다.


예를 들어 마이콜 햄버거는 .bun(2)와 .bacon(6)만 호출한다.
패티, 치즈, 양상추, 토마토는 설정하지 않는다.
그래서 설정하지 않은 값들은 Builder 안의 기본값 0이 그대로 최종 객체에 들어간다.


Builder 안의 필드는 최종 객체를 만들기 전까지 값을 임시로 보관하는 공간이다.


재료 설정 메서드는 값을 저장하고 같은 Builder를 반환한다

// Hamburger2.java
public Builder bun(int val) {
    this.bun = val; // 빵 개수 저장
    return this; // 같은 Builder 객체 반환
}

public Builder patty(int val) {
    this.patty = val; // 패티 개수 저장
    return this; // 같은 Builder 객체 반환
}

public Builder cheese(int val) {
    this.cheese = val; // 치즈 개수 저장
    return this; // 같은 Builder 객체 반환
}

bun(), patty(), cheese() 같은 메서드는 Builder 안에 값을 저장한다.
예를 들어 .bun(1)을 호출하면 Builder의 bun 필드에 1이 저장된다.


여기서 매개변수 이름은 val이다.
val은 외부에서 전달받은 값을 담는 자리이다.
.bun(1)을 호출하면 val에는 1이 들어간다.
그다음 this.bun = val이 실행되어 Builder 안의 bun 필드에 1이 저장된다.


이 메서드들의 반환 타입은 Builder이다.
즉, 값을 저장한 뒤 다시 Builder 객체를 반환한다.
이때 반환하는 값이 this이다.


this는 현재 사용 중인 객체 자기 자신을 의미한다.
여기서는 현재 사용 중인 Builder 객체를 뜻한다.
따라서 return this는 값을 저장한 뒤 같은 Builder 객체를 다시 돌려주는 코드이다.


각 재료 설정 메서드는 값을 저장하고, 다음 설정 메서드를 이어서 호출할 수 있도록 같은 Builder 객체를 반환한다.


return this가 있어야 메서드 체이닝이 가능하다

// Hamburger2App.java
Hamburger2 dooly = Hamburger2.builder()
        .bun(1) // 빵 값을 저장하고 같은 Builder 반환
        .patty(1) // 패티 값을 저장하고 같은 Builder 반환
        .cheese(1) // 치즈 값을 저장하고 같은 Builder 반환
        .lettuce(2) // 양상추 값을 저장하고 같은 Builder 반환
        .tomato(1) // 토마토 값을 저장하고 같은 Builder 반환
        .bacon(1) // 베이컨 값을 저장하고 같은 Builder 반환
        .build(); // 최종 객체 생성

이 코드처럼 메서드를 점으로 이어서 계속 호출하는 방식을 메서드 체이닝이라고 한다.
method chaining은 메서드 호출 결과로 같은 객체를 반환해서 다음 메서드를 계속 이어서 호출하는 방식이다.


이 흐름이 가능한 이유는 bun(), patty(), cheese() 같은 메서드가 모두 return this를 하기 때문이다.


흐름을 순서대로 보면 아래와 같다.

  • Hamburger2.builder()가 Builder 객체를 만든다.
  • .bun(1)이 Builder 안에 빵 값을 저장한다.
  • .bun(1)은 같은 Builder 객체를 반환한다.
  • 반환된 같은 Builder 객체에 .patty(1)을 이어서 호출한다.
  • .patty(1)도 값을 저장하고 같은 Builder 객체를 반환한다.
  • 이런 흐름이 .build()까지 계속 이어진다.

만약 return this가 없으면 .bun(1).patty(1)처럼 이어서 호출할 수 없다.
.bun(1)을 호출한 뒤 다음에 사용할 Builder 객체가 반환되지 않기 때문이다.


return this는 Builder 메서드들을 줄줄이 이어서 호출하게 만드는 핵심 코드이다.


build 메서드는 최종 Hamburger2 객체를 만든다

// Hamburger2.java
public Hamburger2 build() {
    return new Hamburger2(this); // Builder에 모은 값으로 Hamburger2 객체 생성
}

build()는 Builder에 모아 둔 값을 사용해서 최종 Hamburger2 객체를 만드는 메서드이다.
이 메서드가 호출되기 전까지는 아직 최종 햄버거 객체가 만들어진 것이 아니다.


.bun(1), .patty(1), .cheese(1) 같은 메서드는 Builder 안에 값만 저장한다.
실제로 사용할 Hamburger2 객체는 .build()가 호출될 때 만들어진다.


build() 안에서는 new Hamburger2(this)가 실행된다.
여기서 this는 현재 Builder 객체이다.
즉, 지금까지 재료 값을 모아 둔 Builder 객체 전체를 Hamburger2 생성자에 전달한다.


정리하면 build()의 역할은 다음과 같다.

  • Builder에 모아 둔 재료 값을 최종 객체로 넘긴다.
  • private Hamburger2(Builder builder) 생성자를 호출한다.
  • 완성된 Hamburger2 객체를 반환한다.

build()가 호출되어야 비로소 최종 Hamburger2 객체가 만들어진다.


private 생성자는 build 내부에서 최종 객체를 만들 때 사용된다

// Hamburger2.java
private Hamburger2(Builder builder) {
    this.bun = builder.bun; // Builder의 빵 값을 최종 객체에 저장
    this.patty = builder.patty; // Builder의 패티 값을 최종 객체에 저장
    this.cheese = builder.cheese; // Builder의 치즈 값을 최종 객체에 저장
    this.lettuce = builder.lettuce; // Builder의 양상추 값을 최종 객체에 저장
    this.tomato = builder.tomato; // Builder의 토마토 값을 최종 객체에 저장
    this.bacon = builder.bacon; // Builder의 베이컨 값을 최종 객체에 저장
}

Hamburger2 생성자는 private으로 선언되어 있다.
private은 클래스 밖에서 직접 접근할 수 없게 막는 접근 제한자이다.


이 생성자의 매개변수는 Builder builder이다.
즉, 이 생성자는 빵, 패티, 치즈 값을 각각 따로 받는 생성자가 아니라, 값을 모아 둔 Builder 객체 하나를 받는 생성자이다.


이 생성자가 private이기 때문에 외부 코드에서 이 생성자를 직접 호출할 수 없다.
최종 Hamburger2 객체는 build() 메서드 안의 new Hamburger2(this)를 통해 만들어진다.


이 생성자는 Builder 안에 저장된 값을 최종 Hamburger2 객체의 필드에 복사한다.
예를 들어 builder.bun에 1이 들어 있다면 this.bun에도 1이 저장된다.
builder.bacon에 6이 들어 있다면 this.bacon에도 6이 저장된다.


private Hamburger2(Builder builder) 생성자는 외부에서 직접 쓰는 생성자가 아니라, build()가 최종 객체를 만들 때 내부에서 사용하는 생성자이다.
이 구조 덕분에 객체 생성 흐름을 Builder 중심으로 제한할 수 있다.


설정하지 않은 재료는 기본값 0이 된다

// Hamburger2App.java
Hamburger2 gogildong = Hamburger2.builder()
        .bun(1) // 빵 개수 설정
        .patty(2) // 패티 개수 설정
        .cheese(3) // 치즈 개수 설정
        .build(); // 나머지는 기본값 0

Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 나머지는 기본값 0

Builder 방식의 장점은 필요한 값만 골라서 설정할 수 있다는 점이다.
고길동 햄버거는 빵, 패티, 치즈만 설정한다.
양상추, 토마토, 베이컨은 설정하지 않는다.


이때 설정하지 않은 값들은 Builder 안에서 처음 정해 둔 기본값 0을 가진다.
그리고 .build()가 호출되면 그 0 값도 최종 Hamburger2 객체의 final 필드에 저장된다.
그래서 고길동 햄버거의 lettuce, tomato, bacon은 모두 0이 된다.


마이콜 햄버거는 빵과 베이컨만 설정한다.
패티, 치즈, 양상추, 토마토는 설정하지 않는다.
그래서 설정하지 않은 재료는 모두 0이 된다.


생성자 방식이었다면 마이콜처럼 빵과 베이컨만 의미 있게 넣고 싶어도 중간에 0을 여러 개 직접 넣어야 했다.
하지만 Builder 방식에서는 .bun(2)와 .bacon(6)처럼 필요한 값만 보이게 작성할 수 있다.


Builder 방식에서는 선택하지 않은 값은 기본값으로 두고, 필요한 값만 이름이 있는 메서드로 설정할 수 있다.


생성자 방식과 직접 만든 Builder 방식 비교하기

생성자 방식과 Builder 방식은 모두 객체를 만들 수 있다.
하지만 코드를 읽는 방식이 다르다.


생성자 방식은 아래처럼 값만 순서대로 들어간다.

// Hamburger1App.java
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 값의 의미를 생성자 순서로 해석해야 함

이 코드는 짧지만 중간의 0들이 무엇을 의미하는지 바로 알기 어렵다.
정확히 이해하려면 생성자 매개변수 순서를 확인해야 한다.


직접 만든 Builder 방식은 아래처럼 작성한다.

// Hamburger2App.java
Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 객체 생성

이 코드는 더 길어 보일 수 있다.
하지만 bun과 bacon만 설정했다는 의도가 분명하게 보인다.
중간 재료를 사용하지 않는다는 것을 0으로 줄줄이 채워 넣지 않아도 된다.


정리하면 아래처럼 볼 수 있다.

  • 생성자 방식은 코드가 짧다.
  • 생성자 방식은 값의 순서를 알아야 한다.
  • 생성자 방식은 선택하지 않는 값도 0으로 직접 넣어야 할 수 있다.
  • Builder 방식은 코드가 길어질 수 있다.
  • Builder 방식은 값의 의미가 메서드 이름으로 보인다.
  • Builder 방식은 필요한 값만 골라서 설정하기 쉽다.

생성자 방식은 값의 순서에 의존하고, Builder 방식은 값의 이름을 보면서 객체를 만든다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • Hamburger2App의 main() 메서드가 실행된다.
  • Hamburger2.builder()가 호출된다.
  • builder()가 새 Builder 객체를 반환한다.
  • .bun(), .patty(), .cheese() 같은 메서드가 Builder 안에 값을 저장한다.
  • 각 설정 메서드는 return this로 같은 Builder 객체를 반환한다.
  • .build()가 호출된다.
  • build()가 new Hamburger2(this)를 실행한다.
  • private Hamburger2(Builder builder) 생성자가 Builder 값을 최종 객체의 final 필드에 저장한다.
  • 완성된 Hamburger2 객체가 반환된다.
  • System.out.printf()가 각 햄버거 객체를 출력한다.

흐름을 한 줄로 정리하면 다음과 같다.


builder() 호출 → Builder 객체 생성 → 재료 값 설정 → return this로 연결 → build() 호출 → Hamburger2 객체 생성 → 출력


직접 만든 Builder 방식은 값을 먼저 Builder에 모으고, 마지막 build() 단계에서 최종 객체를 만든다.


결과물 뽑기

이 예제는 웹 화면이 아니라 Java 실행 예제이다.
따라서 결과물은 브라우저가 아니라 IntelliJ 콘솔에서 확인한다.


실행 방법은 아래와 같다.

  • Hamburger2App.java 파일을 연다.
  • main() 메서드 왼쪽의 실행 버튼을 누른다.
  • 또는 파일에서 마우스 오른쪽 클릭 후 Run 'Hamburger2App.main()'을 선택한다.
  • 실행 후 IntelliJ 하단 콘솔에서 출력 결과를 확인한다.

예상 출력 결과는 아래처럼 볼 수 있다.

// 출력결과
// 둘리의 햄버거 : Hamburger [bun=1, patty=1, cheese=1, lettuce=2, tomato=1, bacon=1]
// 또치의 햄버거 : Hamburger [bun=1, patty=1, cheese=0, lettuce=2, tomato=2, bacon=1]
// 도우너의 햄버거 : Hamburger [bun=1, patty=2, cheese=1, lettuce=0, tomato=0, bacon=2]
// 고길동의 햄버거 : Hamburger [bun=1, patty=2, cheese=3, lettuce=0, tomato=0, bacon=0]
// 마이콜의 햄버거 : Hamburger [bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6]

출력 결과에서 확인할 부분은 두 가지이다.
첫 번째는 .bun(), .patty(), .cheese() 같은 메서드로 설정한 값이 최종 Hamburger2 객체에 잘 저장되었는지이다.
둘리, 또치, 도우너는 모든 재료 값을 설정했기 때문에 각 필드 값이 설정한 그대로 출력된다.


두 번째는 고길동과 마이콜 객체의 기본값 처리이다.
고길동은 빵, 패티, 치즈만 설정했으므로 양상추, 토마토, 베이컨은 0으로 출력된다.
마이콜은 빵과 베이컨만 설정했으므로 나머지 재료는 0으로 출력된다.


이 출력 결과는 Hamburger2 클래스에서 직접 작성한 toString() 메서드 때문에 보인다.
toString()은 객체를 출력할 때 사용할 문자열을 직접 만들어 반환한다.
그래서 콘솔에서 각 재료 이름과 값이 함께 보인다.


캡처를 남긴다면 IntelliJ 콘솔에 출력된 다섯 줄 결과를 캡처하면 된다.
특히 마이콜 햄버거 결과에서 bun=2, bacon=6만 설정되고 나머지가 0으로 출력되는 부분을 확인하면 Builder의 선택값 처리 흐름을 잘 보여 줄 수 있다.


핵심 정리

이 예제의 핵심은 직접 만든 Builder로 햄버거 객체를 만드는 방식이다.
Hamburger2는 생성자에 숫자를 순서대로 넣는 대신, Builder 내부 클래스에 값을 하나씩 저장한 뒤 마지막에 최종 객체를 만든다.


이 흐름은 아래처럼 정리할 수 있다.

  • Hamburger2는 직접 만든 Builder를 사용하는 클래스이다.
  • builder()는 새 Builder 객체를 시작하는 메서드이다.
  • Builder 내부 클래스는 최종 객체를 만들기 전 재료 값을 임시로 모아 둔다.
  • .bun(1), .patty(1), .cheese(1) 같은 메서드는 재료 값을 Builder에 저장한다.
  • 각 설정 메서드는 return this로 같은 Builder 객체를 반환한다.
  • return this가 있기 때문에 메서드 체이닝이 가능하다.
  • build()는 Builder에 모은 값으로 최종 Hamburger2 객체를 만든다.
  • private Hamburger2(Builder builder) 생성자는 build() 내부에서 최종 객체를 만들 때 사용된다.
  • 설정하지 않은 재료는 Builder 내부 기본값 0이 사용된다.
  • 생성자 방식은 값의 순서에 의존하지만, Builder 방식은 메서드 이름으로 값의 의미를 보여 준다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
직접 만든 Builder 방식은 객체를 만들기 전에 값을 이름이 있는 메서드로 하나씩 모으고, 마지막에 build()를 호출해 완성된 객체를 만드는 방식이다.
이 흐름을 이해하면 다음 예제에서 Lombok @Builder가 어떤 반복 코드를 대신 만들어 주는지 더 쉽게 이해할 수 있다.



3. Lombok @Builder로 햄버거 객체 만들기 응용예제

이 예제는 직접 Builder 클래스를 작성하지 않고, Lombok의 @Builder를 사용해서 햄버거 객체를 만드는 흐름을 보여 준다.
앞의 예제에서는 Hamburger2 클래스 안에 Builder 내부 클래스, builder() 메서드, 재료 설정 메서드, build() 메서드, private 생성자를 직접 작성했다.
그 방식은 객체 생성 흐름이 잘 보이지만 작성해야 하는 코드가 많다.


이번 예제에서는 Hamburger3 클래스에 @Builder를 붙인다.
그러면 Lombok이 builder()로 시작해서 .bun(), .patty(), .cheese()처럼 값을 설정하고, 마지막에 .build()로 객체를 만드는 코드를 대신 만들어 준다.
사용하는 쪽의 코드는 직접 만든 Builder 방식과 거의 같다.


이 예제의 핵심은 직접 작성했던 Builder 반복 코드를 Lombok @Builder가 대신 만들어 주지만, 객체 생성 흐름 자체는 Builder 방식 그대로 유지된다는 점이다.


예제 전체 코드

// Hamburger3.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

import lombok.AccessLevel; // 원본 코드에 포함된 Lombok 관련 import
import lombok.AllArgsConstructor; // 원본 코드에 포함된 Lombok 관련 import
import lombok.Builder; // Builder 관련 코드를 자동 생성
import lombok.ToString; // 객체 정보를 문자열로 만드는 메서드 자동 생성

@Builder // Builder 패턴으로 객체를 만들 수 있게 관련 코드 자동 생성
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger3 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}
// Hamburger3App.java
package com.example.springrestedu.builderpattern; // 클래스가 속한 패키지 경로

public class Hamburger3App {
    public static void main(String[] args) {
        Hamburger3 dooly = Hamburger3.builder()
                .bun(1) // 빵 개수 설정
                .patty(1) // 패티 개수 설정
                .cheese(1) // 치즈 개수 설정
                .lettuce(2) // 양상추 개수 설정
                .tomato(1) // 토마토 개수 설정
                .bacon(1) // 베이컨 개수 설정
                .build(); // Hamburger3 객체 생성

        Hamburger3 ddochi = Hamburger3.builder()
                .bun(1) // 빵 개수 설정
                .patty(1) // 패티 개수 설정
                .cheese(0) // 치즈 개수 설정
                .lettuce(2) // 양상추 개수 설정
                .tomato(2) // 토마토 개수 설정
                .bacon(1) // 베이컨 개수 설정
                .build(); // Hamburger3 객체 생성

        Hamburger3 dounar = Hamburger3.builder()
                .bun(1) // 빵 개수 설정
                .patty(2) // 패티 개수 설정
                .cheese(1) // 치즈 개수 설정
                .lettuce(0) // 양상추 개수 설정
                .tomato(0) // 토마토 개수 설정
                .bacon(2) // 베이컨 개수 설정
                .build(); // Hamburger3 객체 생성

        Hamburger3 gogildong = Hamburger3.builder()
                .bun(1) // 빵 개수 설정
                .patty(2) // 패티 개수 설정
                .cheese(3) // 치즈 개수 설정
                .build(); // 설정하지 않은 재료는 기본값 0 사용

        Hamburger3 micol = Hamburger3.builder()
                .bun(2) // 빵 개수 설정
                .bacon(6) // 베이컨 개수 설정
                .build(); // 설정하지 않은 재료는 기본값 0 사용

        System.out.printf("둘리의 햄버거 : %s %n", dooly); // 둘리 햄버거 정보 출력
        System.out.printf("또치의 햄버거 : %s %n", ddochi); // 또치 햄버거 정보 출력
        System.out.printf("도우너의 햄버거 : %s %n", dounar); // 도우너 햄버거 정보 출력
        System.out.printf("고길동의 햄버거 : %s %n", gogildong); // 고길동 햄버거 정보 출력
        System.out.printf("마이콜의 햄버거 : %s %n", micol); // 마이콜 햄버거 정보 출력
    }
}

이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째 Hamburger3.java는 Lombok @Builder를 사용해서 Builder 관련 코드를 자동 생성하는 클래스이다.
두 번째 Hamburger3App.java는 자동 생성된 Builder를 사용해서 햄버거 객체를 만들고 출력하는 실행 클래스이다.


Hamburger3 클래스는 Hamburger2와 비교하면 코드가 훨씬 짧다.
직접 만든 Builder 클래스가 보이지 않고, builder() 메서드도 보이지 않는다.
재료 값을 설정하는 .bun(), .patty(), .cheese() 같은 메서드도 코드에 직접 작성되어 있지 않다.


하지만 Hamburger3App에서는 Hamburger3.builder()를 호출할 수 있다.
그 이유는 @Builder가 컴파일 과정에서 Builder 관련 코드를 자동으로 만들어 주기 때문이다.


Lombok @Builder는 코드에 보이지 않는 Builder 생성 흐름을 자동으로 만들어 주기 때문에, 클래스 코드는 짧아지고 사용 방식은 Builder 형태로 유지된다.


이 예제에서 확인할 핵심

  • Hamburger3는 Lombok @Builder를 사용해 객체를 생성한다.
  • @Builder는 builder() 메서드와 내부 Builder 구조를 자동으로 만들어 준다.
  • @Builder를 사용하면 직접 Builder 클래스를 작성하지 않아도 된다.
  • Hamburger3.builder()로 객체 생성 흐름을 시작한다.
  • .bun(1), .patty(1) 같은 메서드로 재료 값을 설정한다.
  • .build()를 호출해야 최종 Hamburger3 객체가 만들어진다.
  • 설정하지 않은 int 필드는 기본값 0이 사용된다.
  • @ToString 덕분에 객체를 출력하면 필드 값이 문자열로 보인다.
  • 직접 만든 Builder와 사용 방식은 거의 같지만, 클래스 안의 반복 코드가 줄어든다.

이 예제는 직접 만든 Builder 구조를 Lombok이 어떻게 대신 만들어 주는지 확인하는 예제이다.


Lombok @Builder 다시 복기하기

Lombok은 반복되는 코드를 애노테이션으로 줄여 주는 라이브러리이다.
여기서 애노테이션은 코드 위에 붙여서 특정 기능을 자동으로 적용하게 하는 표시라고 이해하면 된다.


@Builder는 Builder Pattern에 필요한 코드를 자동으로 만들어 주는 애노테이션이다.
직접 만든 Builder 방식에서는 아래와 같은 코드를 직접 작성해야 했다.

  • builder() 메서드
  • 내부 Builder 클래스
  • 재료 값을 저장하는 메서드
  • return this
  • build() 메서드
  • Builder 값을 최종 객체에 넘기는 생성 흐름

그런데 @Builder를 사용하면 이 반복 코드를 직접 작성하지 않아도 된다.
클래스 위에 @Builder를 붙이면 Lombok이 객체 생성에 필요한 Builder 코드를 만들어 준다.


@Builder는 Builder Pattern을 없애는 문법이 아니라, 직접 작성해야 하는 Builder 반복 코드를 자동으로 만들어 주는 문법이다.


Hamburger3 클래스는 코드가 매우 짧다

// Hamburger3.java
@Builder // Builder 패턴으로 객체를 만들 수 있게 관련 코드 자동 생성
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger3 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}

Hamburger3 클래스에는 필드 여섯 개만 직접 보인다.
빵, 패티, 치즈, 양상추, 토마토, 베이컨 값을 저장하는 필드이다.


앞의 Hamburger2에서는 같은 재료를 다루기 위해 내부 Builder 클래스를 직접 만들었다.
그리고 각 재료 값을 설정하는 메서드도 직접 작성했다.
마지막에 build() 메서드와 private 생성자까지 작성했다.


하지만 Hamburger3에서는 그런 코드가 보이지 않는다.
대신 클래스 위에 @Builder가 붙어 있다.
이 한 줄이 직접 작성했던 Builder 관련 반복 코드를 대신한다.


@ToString도 함께 붙어 있다.
@ToString은 객체를 출력할 때 사용할 문자열 표현을 자동으로 만들어 준다.
그래서 System.out.printf()로 객체를 출력하면 필드 이름과 값이 함께 보인다.


Hamburger3 클래스는 @Builder 덕분에 객체 생성용 코드가 짧아지고, @ToString 덕분에 출력용 문자열 표현도 자동으로 만들어진다.


import에 포함된 코드도 구분해서 봐야 한다

// Hamburger3.java
import lombok.AccessLevel; // 원본 코드에 포함된 Lombok 관련 import
import lombok.AllArgsConstructor; // 원본 코드에 포함된 Lombok 관련 import
import lombok.Builder; // Builder 관련 코드를 자동 생성
import lombok.ToString; // 객체 정보를 문자열로 만드는 메서드 자동 생성

원본 코드에는 AccessLevel, AllArgsConstructor, Builder, ToString이 import되어 있다.
하지만 현재 Hamburger3 클래스 위에서 실제로 사용되는 애노테이션은 @Builder와 @ToString이다.


@Builder는 Builder 객체 생성 흐름을 자동으로 만들어 준다.
@ToString은 객체 출력 시 사용할 문자열 표현을 자동으로 만들어 준다.


반면 현재 코드에는 @AllArgsConstructor가 붙어 있지 않다.
AccessLevel도 직접 사용되지 않는다.
따라서 이 예제의 핵심을 볼 때는 @Builder와 @ToString을 중심으로 이해하면 된다.


원본 코드에 import가 있다고 해서 그 기능이 모두 현재 클래스에 적용되는 것은 아니다.
실제로 적용되는 기능은 클래스 위에 붙은 애노테이션을 기준으로 확인해야 한다.


Hamburger3App에서 객체를 만드는 흐름

// Hamburger3App.java
Hamburger3 dooly = Hamburger3.builder()
        .bun(1) // 빵 개수 설정
        .patty(1) // 패티 개수 설정
        .cheese(1) // 치즈 개수 설정
        .lettuce(2) // 양상추 개수 설정
        .tomato(1) // 토마토 개수 설정
        .bacon(1) // 베이컨 개수 설정
        .build(); // Hamburger3 객체 생성

Hamburger3App에서는 Hamburger3.builder()로 객체 생성 흐름을 시작한다.
이 사용 방식은 앞에서 직접 만든 Hamburger2.builder()와 거의 같다.


차이는 Hamburger3 클래스 안에 builder() 메서드를 직접 작성하지 않았다는 점이다.
코드에는 없지만 @Builder가 이 메서드를 자동으로 만들어 준다.
그래서 Hamburger3.builder()를 호출할 수 있다.


그다음 .bun(1), .patty(1), .cheese(1)처럼 재료 이름이 붙은 메서드를 이어서 호출한다.
이 메서드들도 코드에 직접 작성되어 있지 않지만, @Builder가 자동으로 만들어 준다.


마지막에 .build()를 호출하면 최종 Hamburger3 객체가 만들어진다.
build()도 직접 작성하지 않았지만, @Builder가 만들어 주는 메서드이다.


@Builder를 사용하면 builder(), 재료 설정 메서드, build()를 직접 작성하지 않아도 Builder 방식으로 객체를 만들 수 있다.


직접 만든 Builder와 Lombok Builder의 사용 방식은 거의 같다

직접 만든 Builder 예제에서는 아래처럼 객체를 만들었다.

// Hamburger2App.java
Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 객체 생성

Lombok @Builder 예제에서는 아래처럼 객체를 만든다.

// Hamburger3App.java
Hamburger3 micol = Hamburger3.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 객체 생성

두 코드는 클래스 이름만 다르고 사용 흐름은 거의 같다.
둘 다 builder()로 시작한다.
둘 다 재료 이름이 붙은 메서드로 값을 설정한다.
둘 다 마지막에 build()를 호출해서 최종 객체를 만든다.


차이는 Builder 코드를 누가 작성했는지이다.
Hamburger2는 개발자가 직접 Builder 클래스를 작성했다.
Hamburger3은 개발자가 @Builder만 붙이고, Lombok이 Builder 코드를 만들어 준다.


정리하면 아래처럼 볼 수 있다.

  • 직접 만든 Builder는 구조가 눈에 보인다.
  • Lombok Builder는 구조가 코드에 직접 보이지 않는다.
  • 직접 만든 Builder는 작성 코드가 많다.
  • Lombok Builder는 작성 코드가 적다.
  • 두 방식 모두 사용하는 쪽에서는 builder() → 값 설정 → build() 흐름을 가진다.

직접 만든 Builder와 Lombok Builder는 사용하는 방식은 비슷하지만, Builder 관련 코드를 직접 작성하느냐 자동 생성하느냐가 다르다.


설정하지 않은 재료는 기본값 0이 된다

// Hamburger3App.java
Hamburger3 gogildong = Hamburger3.builder()
        .bun(1) // 빵 개수 설정
        .patty(2) // 패티 개수 설정
        .cheese(3) // 치즈 개수 설정
        .build(); // 설정하지 않은 재료는 기본값 0 사용

Hamburger3 micol = Hamburger3.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 설정하지 않은 재료는 기본값 0 사용

Lombok @Builder를 사용해도 선택하지 않은 값은 기본값을 가진다.
이 예제의 필드들은 모두 int 타입이다.
int 타입은 값을 따로 설정하지 않으면 기본값 0을 가진다.


고길동 햄버거는 .bun(1), .patty(2), .cheese(3)만 설정한다.
양상추, 토마토, 베이컨은 설정하지 않는다.
그래서 lettuce, tomato, bacon은 0으로 남는다.


마이콜 햄버거는 .bun(2)와 .bacon(6)만 설정한다.
패티, 치즈, 양상추, 토마토는 설정하지 않는다.
그래서 설정하지 않은 값은 모두 0으로 남는다.


이 흐름은 직접 만든 Builder와 같다.
Builder 방식에서는 필요한 값만 골라서 설정하고, 나머지는 기본값으로 둘 수 있다.


Lombok @Builder를 사용해도 설정하지 않은 int 값은 기본값 0으로 처리된다.


@ToString으로 객체 출력 결과를 확인한다

// Hamburger3.java
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger3 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}

Hamburger3에는 @ToString이 붙어 있다.
@ToString은 객체를 출력할 때 사용할 문자열 표현을 자동으로 만들어 준다.


그래서 System.out.printf("둘리의 햄버거 : %s %n", dooly)처럼 객체를 출력하면, 객체의 필드 이름과 값이 함께 출력된다.


직접 만든 Hamburger2에서는 toString() 메서드를 개발자가 직접 작성했다.
반면 Hamburger3에서는 @ToString이 toString() 메서드를 자동으로 만들어 준다.


즉, 두 방식 모두 출력 결과를 볼 수 있지만, 출력용 문자열을 만드는 방법은 다르다.
Hamburger2는 직접 작성한 toString()을 사용한다.
Hamburger3은 Lombok이 자동 생성한 toString()을 사용한다.


@ToString은 객체의 필드 값을 확인할 수 있도록 출력용 문자열 표현을 자동으로 만들어 준다.


Lombok Builder를 사용할 때 주의할 점

Lombok @Builder는 코드를 짧게 만들어 준다.
하지만 코드가 자동 생성되기 때문에 눈에 직접 보이지 않는 부분이 생긴다.


예를 들어 Hamburger3.java에는 builder() 메서드가 직접 작성되어 있지 않다.
그런데 Hamburger3App.java에서는 Hamburger3.builder()를 호출한다.
초보자 입장에서는 “코드에 없는 메서드를 왜 호출할 수 있지?”라고 헷갈릴 수 있다.


이때는 @Builder가 붙어 있는지 확인해야 한다.
@Builder가 붙어 있으면 Lombok이 builder()와 build() 흐름을 만들어 준다.


또 Lombok을 사용하려면 프로젝트에 Lombok 의존성이 있어야 하고, 개발 도구에서 Lombok 처리가 가능해야 한다.
이 설정이 되어 있지 않으면 코드가 정상적으로 인식되지 않을 수 있다.


정리하면 아래처럼 볼 수 있다.

  • @Builder가 있어야 builder()를 사용할 수 있다.
  • builder()는 코드에 직접 보이지 않아도 Lombok이 자동 생성한다.
  • 자동 생성 코드는 편리하지만, 처음에는 흐름이 눈에 잘 안 보일 수 있다.
  • 그래서 직접 만든 Builder 구조를 먼저 이해하면 Lombok @Builder도 쉽게 이해할 수 있다.

Lombok @Builder는 편리하지만, 자동 생성되는 구조를 이해하지 못하면 코드에 없는 메서드가 갑자기 사용되는 것처럼 보일 수 있다.


실행 흐름 정리

이 예제의 실행 흐름은 아래 순서로 진행된다.

  • Hamburger3App의 main() 메서드가 실행된다.
  • Hamburger3.builder()가 호출된다.
  • @Builder가 자동 생성한 Builder 객체가 시작된다.
  • .bun(), .patty(), .cheese() 같은 자동 생성 메서드로 재료 값을 설정한다.
  • 설정하지 않은 int 값은 기본값 0으로 남는다.
  • .build()를 호출하면 최종 Hamburger3 객체가 만들어진다.
  • System.out.printf()가 각 햄버거 객체를 출력한다.
  • @ToString이 자동 생성한 문자열 표현 덕분에 객체의 필드 값이 콘솔에 보인다.

흐름을 한 줄로 정리하면 다음과 같다.


@Builder 적용 → builder() 호출 → 재료 값 설정 → build() 호출 → Hamburger3 객체 생성 → @ToString으로 출력


Lombok @Builder는 직접 만든 Builder와 같은 객체 생성 흐름을 자동 생성 코드로 처리하게 해 준다.


결과물 뽑기

이 예제는 웹 화면이 아니라 Java 실행 예제이다.
따라서 결과물은 브라우저가 아니라 IntelliJ 콘솔에서 확인한다.


실행 방법은 아래와 같다.

  • Hamburger3App.java 파일을 연다.
  • main() 메서드 왼쪽의 실행 버튼을 누른다.
  • 또는 파일에서 마우스 오른쪽 클릭 후 Run 'Hamburger3App.main()'을 선택한다.
  • 실행 후 IntelliJ 하단 콘솔에서 출력 결과를 확인한다.

예상 출력 결과는 아래처럼 볼 수 있다.

// 출력결과
// 둘리의 햄버거 : Hamburger3(bun=1, patty=1, cheese=1, lettuce=2, tomato=1, bacon=1)
// 또치의 햄버거 : Hamburger3(bun=1, patty=1, cheese=0, lettuce=2, tomato=2, bacon=1)
// 도우너의 햄버거 : Hamburger3(bun=1, patty=2, cheese=1, lettuce=0, tomato=0, bacon=2)
// 고길동의 햄버거 : Hamburger3(bun=1, patty=2, cheese=3, lettuce=0, tomato=0, bacon=0)
// 마이콜의 햄버거 : Hamburger3(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

출력 결과에서 확인할 부분은 두 가지이다.
첫 번째는 Hamburger3.builder()로 만든 객체가 정상적으로 출력되는지이다.
클래스 안에 builder() 메서드를 직접 작성하지 않았지만, @Builder 덕분에 객체 생성이 가능하다.


두 번째는 설정하지 않은 값의 처리이다.
고길동은 빵, 패티, 치즈만 설정했으므로 양상추, 토마토, 베이컨은 0으로 출력된다.
마이콜은 빵과 베이컨만 설정했으므로 나머지 재료는 0으로 출력된다.


이 출력 결과는 @ToString이 자동 생성한 문자열 표현 덕분에 보인다.
직접 toString()을 작성하지 않았지만, 객체의 필드 이름과 값이 함께 출력된다.


캡처를 남긴다면 IntelliJ 콘솔에 출력된 다섯 줄 결과를 캡처하면 된다.
특히 Hamburger3 클래스에는 직접 만든 Builder 코드가 없는데도 Hamburger3App에서 builder() 흐름이 정상 실행된다는 점을 결과물로 확인하면 된다.


핵심 정리

이 예제의 핵심은 Lombok @Builder로 직접 만든 Builder의 반복 코드를 줄이는 방식이다.
Hamburger3 클래스는 필드와 애노테이션만 가지고 있지만, Hamburger3App에서는 Builder 방식으로 객체를 만든다.


이 흐름은 아래처럼 정리할 수 있다.

  • Lombok은 반복되는 코드를 애노테이션으로 줄여 주는 라이브러리이다.
  • @Builder는 Builder Pattern에 필요한 코드를 자동으로 만들어 준다.
  • @Builder를 사용하면 내부 Builder 클래스와 builder(), build() 메서드를 직접 작성하지 않아도 된다.
  • Hamburger3.builder()는 @Builder가 자동 생성한 메서드이다.
  • .bun(1), .patty(1), .cheese(1) 같은 설정 메서드도 자동 생성된다.
  • .build()를 호출해야 최종 Hamburger3 객체가 만들어진다.
  • 설정하지 않은 int 값은 기본값 0으로 남는다.
  • @ToString은 객체 출력 시 사용할 문자열 표현을 자동으로 만들어 준다.
  • 직접 만든 Builder와 Lombok Builder는 사용 방식은 비슷하다.
  • 차이는 Builder 관련 코드를 직접 작성하느냐, Lombok이 자동 생성하느냐이다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
Lombok @Builder는 직접 작성해야 하는 Builder 반복 코드를 자동으로 만들어 주면서도, 사용하는 쪽에서는 builder() → 값 설정 → build()라는 Builder 흐름을 그대로 사용할 수 있게 해 준다.
다음 예제에서는 생성자 방식, 직접 만든 Builder, Lombok Builder를 한 번에 비교해서 각각의 차이를 정리한다.



4. 생성자 방식, 직접 만든 Builder, Lombok Builder 비교 정리 응용예제

이 예제는 앞에서 만든 Hamburger1, Hamburger2, Hamburger3 객체 생성 방식을 한 번에 비교하는 흐름이다.
세 클래스는 모두 햄버거 객체를 만든다는 목적은 같다.
하지만 객체를 만드는 방식은 다르다.


Hamburger1은 생성자에 값을 순서대로 넣는 방식이다.
Hamburger2는 직접 만든 Builder를 사용해서 값을 이름이 있는 메서드로 설정하는 방식이다.
Hamburger3은 Lombok @Builder를 사용해서 직접 만든 Builder의 반복 코드를 자동 생성하는 방식이다.


이 예제의 핵심은 같은 햄버거 객체를 만들더라도 생성자 방식, 직접 만든 Builder, Lombok Builder는 코드의 읽기 쉬움과 작성해야 하는 코드 양이 다르다는 점이다.


비교에 사용할 핵심 코드

// Hamburger1App.java
// 생성자 방식은 값을 순서대로 전달한다.
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6);

System.out.printf("마이콜의 햄버거 : %s %n", micol); // 생성자 방식 결과 출력
// Hamburger2App.java
// 직접 만든 Builder 방식은 필요한 값만 이름이 있는 메서드로 설정한다.
Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // Hamburger2 객체 생성

System.out.printf("마이콜의 햄버거 : %s %n", micol); // 직접 만든 Builder 방식 결과 출력
// Hamburger3App.java
// Lombok Builder 방식은 자동 생성된 Builder 메서드를 사용한다.
Hamburger3 micol = Hamburger3.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // Hamburger3 객체 생성

System.out.printf("마이콜의 햄버거 : %s %n", micol); // Lombok Builder 방식 결과 출력

세 코드는 모두 마이콜 햄버거 객체를 만든다.
마이콜 햄버거는 빵 2개와 베이컨 6개만 의미 있게 설정한다.
나머지 패티, 치즈, 양상추, 토마토는 0이 된다.


하지만 작성 방식은 다르다.
Hamburger1은 생성자에 2, 0, 0, 0, 0, 6을 순서대로 넣는다.
Hamburger2는 .bun(2)와 .bacon(6)처럼 값을 이름이 있는 메서드로 설정한다.
Hamburger3도 사용하는 쪽에서는 Hamburger2와 거의 같은 방식으로 작성하지만, Builder 관련 코드는 Lombok이 자동으로 만들어 준다.


같은 데이터를 만들더라도 생성자 방식은 값의 순서를 읽어야 하고, Builder 방식은 메서드 이름을 보면서 값을 이해할 수 있다.


이 예제에서 확인할 핵심

  • Hamburger1은 생성자 오버로딩 방식으로 객체를 만든다.
  • Hamburger2는 직접 만든 Builder 방식으로 객체를 만든다.
  • Hamburger3은 Lombok @Builder 방식으로 객체를 만든다.
  • 세 방식 모두 최종적으로는 햄버거 재료 값을 가진 객체를 만든다.
  • 생성자 방식은 코드가 짧지만 값의 의미가 바로 보이지 않는다.
  • 직접 만든 Builder 방식은 값의 의미가 잘 보이지만 클래스 안에 작성해야 할 코드가 많다.
  • Lombok Builder 방식은 사용 방식은 Builder와 비슷하지만 반복 코드를 줄여 준다.
  • 설정하지 않은 int 값은 기본값 0으로 처리된다.
  • 출력 문자열 형태는 클래스마다 다를 수 있지만, 핵심은 필드 값이 어떻게 저장되었는지 확인하는 것이다.

이 예제는 세 가지 객체 생성 방식을 나란히 비교해서, 왜 Builder Pattern이 필요하고 왜 Lombok @Builder를 사용하는지 정리하는 예제이다.


생성자 방식은 값의 순서에 의존한다

// Hamburger1App.java
Hamburger1 dooly = new Hamburger1(1, 1, 1, 2, 1, 1); // 모든 재료를 순서대로 전달
Hamburger1 gogildong = new Hamburger1(1, 2, 3); // 빵, 패티, 치즈만 전달
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 중간 재료는 0으로 직접 전달

Hamburger1은 생성자에 값을 순서대로 전달한다.
생성자 방식은 가장 기본적인 객체 생성 방식이다.
new Hamburger1(...)을 실행하면 전달한 값의 개수에 맞는 생성자가 호출된다.


이 방식의 장점은 코드가 짧다는 것이다.
객체를 만들 때 new와 생성자만 사용하면 된다.
그리고 생성자 오버로딩을 사용하면 재료 6개를 받는 생성자, 재료 3개를 받는 생성자처럼 여러 형태의 객체 생성을 처리할 수 있다.


하지만 단점도 분명하다.
값이 모두 int라서 숫자만 나열된다.
그래서 new Hamburger1(2, 0, 0, 0, 0, 6)만 보면 첫 번째 2가 무엇인지, 마지막 6이 무엇인지 바로 알기 어렵다.


이 코드를 이해하려면 생성자의 매개변수 순서를 다시 확인해야 한다.
순서는 bun, patty, cheese, lettuce, tomato, bacon이다.
따라서 마이콜 햄버거는 빵 2개, 베이컨 6개를 가진 객체가 된다.


생성자 방식은 값의 순서가 곧 의미이기 때문에, 순서를 모르면 코드를 정확히 읽기 어렵다.


생성자 방식의 문제는 선택값이 많을 때 더 커진다

생성자 방식은 필수값이 적고, 값의 의미가 분명할 때는 단순하고 좋다.
하지만 선택값이 많아지면 불편함이 커진다.


햄버거 재료를 예로 보면 어떤 햄버거는 토마토가 없을 수 있다.
어떤 햄버거는 양상추도 없고 치즈도 없을 수 있다.
이런 경우 생성자 방식에서는 사용하지 않는 재료 자리에 0을 넣어야 할 수 있다.


마이콜 햄버거 코드를 보면 이 문제가 잘 보인다.

// Hamburger1App.java
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6); // 사용하지 않는 재료도 0으로 채움

이 코드는 빵과 베이컨만 의미 있게 설정한다.
하지만 생성자의 순서를 맞추기 위해 중간에 0을 여러 개 넣어야 한다.


이때 초보자가 실수로 0의 위치를 잘못 넣어도 모든 값이 int라면 문법 오류가 나지 않을 수 있다.
문법 오류는 없지만 객체의 값은 잘못 저장될 수 있다.


생성자 방식은 선택값이 많고 같은 타입의 값이 많을수록 실수 가능성이 커진다.
이 문제가 직접 만든 Builder 방식으로 넘어가는 이유이다.


직접 만든 Builder 방식은 값의 이름을 보여 준다

// Hamburger2App.java
Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // Hamburger2 객체 생성

Hamburger2는 직접 만든 Builder를 사용한다.
객체를 만들 때 new Hamburger2(...)로 생성자를 직접 호출하지 않는다.
대신 Hamburger2.builder()로 시작한다.


그다음 .bun(2), .bacon(6)처럼 재료 이름이 붙은 메서드를 호출한다.
이 방식은 생성자 방식보다 코드가 길어질 수 있다.
하지만 어떤 값이 어떤 재료에 들어가는지 바로 보인다.


마이콜 햄버거를 보면 빵과 베이컨만 설정한다는 의도가 분명하다.
패티, 치즈, 양상추, 토마토는 코드에 아예 나오지 않는다.
그래서 중간에 0을 줄줄이 넣지 않아도 된다.


설정하지 않은 값은 Builder 내부 기본값 0이 사용된다.
즉, 필요한 값만 명시하고 나머지는 기본값으로 두는 구조이다.


직접 만든 Builder 방식은 값의 순서를 외우는 대신, 메서드 이름을 보면서 어떤 값이 설정되는지 알 수 있게 해 준다.


직접 만든 Builder 방식은 구조가 잘 보이지만 코드가 많다

Hamburger2의 장점은 Builder 구조가 코드에 직접 보인다는 점이다.
builder() 메서드, 내부 Builder 클래스, 재료 설정 메서드, return this, build() 메서드가 모두 코드에 작성되어 있다.


이 덕분에 Builder가 어떻게 동작하는지 학습하기 좋다.
값이 어디에 저장되는지, 왜 메서드를 이어서 호출할 수 있는지, 언제 최종 객체가 만들어지는지 확인할 수 있다.


하지만 단점도 있다.
재료 필드가 많아질수록 Builder 클래스 안에도 필드가 많아진다.
각 필드마다 값을 설정하는 메서드도 만들어야 한다.
그리고 각 메서드마다 return this를 작성해야 한다.


즉, 직접 만든 Builder는 동작 원리를 이해하기에는 좋지만 반복 코드가 많다.
이 반복 코드를 줄이기 위해 Lombok @Builder를 사용할 수 있다.


정리하면 아래처럼 볼 수 있다.

  • 직접 만든 Builder는 동작 구조가 눈에 보인다.
  • 직접 만든 Builder는 학습용으로 이해하기 좋다.
  • 직접 만든 Builder는 필드가 많아질수록 반복 코드가 늘어난다.
  • 반복 코드가 많아지면 작성과 관리가 번거로워진다.

직접 만든 Builder는 구조를 이해하기에는 좋지만, 실제 작성해야 하는 코드가 많아지는 단점이 있다.


Lombok Builder 방식은 반복 코드를 줄인다

// Hamburger3.java
@Builder // Builder 패턴으로 객체를 만들 수 있게 관련 코드 자동 생성
@ToString // 객체 출력 시 필드 값을 문자열로 보여 줌
class Hamburger3 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}

Hamburger3은 Lombok @Builder를 사용한다.
클래스 안에는 필드와 애노테이션만 보인다.
직접 만든 Builder 클래스는 보이지 않는다.


하지만 Hamburger3App에서는 아래처럼 Builder 방식으로 객체를 만들 수 있다.

// Hamburger3App.java
Hamburger3 micol = Hamburger3.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // Hamburger3 객체 생성

이 코드가 가능한 이유는 @Builder 때문이다.
@Builder가 builder() 메서드, 값 설정 메서드, build() 메서드 같은 Builder 관련 코드를 자동으로 만들어 준다.


사용하는 쪽에서 보면 Hamburger2와 Hamburger3은 거의 같은 방식으로 보인다.
둘 다 builder()로 시작하고, 값을 설정하고, build()로 끝난다.


차이는 클래스 내부 구현이다.
Hamburger2는 개발자가 직접 Builder 코드를 작성했다.
Hamburger3은 Lombok이 Builder 코드를 자동 생성한다.


Lombok Builder는 Builder 방식의 읽기 쉬운 객체 생성 흐름은 유지하면서, 직접 작성해야 하는 반복 코드를 줄여 준다.


세 방식의 객체 생성 코드 비교

세 방식을 같은 마이콜 햄버거 기준으로 비교하면 차이가 더 분명하다.

// Hamburger1App.java
// 생성자 방식
Hamburger1 micol = new Hamburger1(2, 0, 0, 0, 0, 6);

생성자 방식은 코드가 가장 짧다.
하지만 2, 0, 0, 0, 0, 6이 각각 어떤 재료인지 바로 보이지 않는다.
생성자 매개변수 순서를 알아야 의미를 해석할 수 있다.

// Hamburger2App.java
// 직접 만든 Builder 방식
Hamburger2 micol = Hamburger2.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 객체 생성

직접 만든 Builder 방식은 코드가 조금 더 길다.
하지만 빵과 베이컨만 설정한다는 의도가 바로 보인다.
사용하지 않는 중간 재료를 0으로 채워 넣지 않아도 된다.

// Hamburger3App.java
// Lombok Builder 방식
Hamburger3 micol = Hamburger3.builder()
        .bun(2) // 빵 개수 설정
        .bacon(6) // 베이컨 개수 설정
        .build(); // 객체 생성

Lombok Builder 방식도 사용하는 쪽에서는 직접 만든 Builder와 거의 같다.
하지만 클래스 내부에 Builder 코드를 직접 작성하지 않아도 된다.


객체를 사용하는 코드만 보면 직접 만든 Builder와 Lombok Builder는 거의 같고, 차이는 Builder 코드를 직접 작성했는지 자동 생성했는지에 있다.


세 방식의 클래스 코드 비교

객체를 만드는 사용 코드뿐 아니라 클래스 코드도 비교해야 한다.
왜냐하면 Builder 방식의 장점과 단점은 클래스 내부 코드에서 더 분명하게 보이기 때문이다.


Hamburger1은 여러 생성자를 직접 만든다.

// Hamburger1.java
public Hamburger1(int bun, int patty, int cheese, int lettuce, int tomato, int bacon) {
    this.bun = bun; // 빵 개수 저장
    this.patty = patty; // 패티 개수 저장
    this.cheese = cheese; // 치즈 개수 저장
    this.lettuce = lettuce; // 양상추 개수 저장
    this.tomato = tomato; // 토마토 개수 저장
    this.bacon = bacon; // 베이컨 개수 저장
}

public Hamburger1(int bun, int patty, int cheese) {
    this.bun = bun; // 빵 개수 저장
    this.patty = patty; // 패티 개수 저장
    this.cheese = cheese; // 치즈 개수 저장
}

생성자 방식에서는 필요한 조합에 따라 생성자를 여러 개 만들 수 있다.
하지만 조합이 많아질수록 생성자 수도 늘어난다.


Hamburger2는 내부 Builder 클래스를 직접 만든다.

// Hamburger2.java
public static class Builder {
    private int bun = 0; // 빵 기본값
    private int bacon = 0; // 베이컨 기본값

    public Builder bun(int val) {
        this.bun = val; // 빵 개수 저장
        return this; // 같은 Builder 반환
    }

    public Builder bacon(int val) {
        this.bacon = val; // 베이컨 개수 저장
        return this; // 같은 Builder 반환
    }

    public Hamburger2 build() {
        return new Hamburger2(this); // 최종 객체 생성
    }
}

직접 만든 Builder 방식은 값의 의미와 객체 생성 흐름이 잘 보인다.
하지만 필드마다 설정 메서드를 직접 만들어야 한다.


Hamburger3은 @Builder를 사용한다.

// Hamburger3.java
@Builder // Builder 관련 코드 자동 생성
@ToString // 출력용 문자열 자동 생성
class Hamburger3 {
    private int bun; // 빵 개수
    private int patty; // 패티 개수
    private int cheese; // 치즈 개수
    private int lettuce; // 양상추 개수
    private int tomato; // 토마토 개수
    private int bacon; // 베이컨 개수
}

Lombok Builder 방식은 클래스 코드가 가장 짧다.
직접 만든 Builder와 같은 사용 흐름을 유지하면서 반복 코드를 줄인다.


정리하면 생성자 방식은 생성자가 늘어나고, 직접 만든 Builder는 설정 메서드가 늘어나며, Lombok Builder는 그 반복 코드를 자동 생성한다.


출력 결과 비교하기

세 방식의 출력 결과는 객체 안에 저장된 재료 값을 확인하기 위한 결과이다.
다만 출력 문자열 모양은 조금 다를 수 있다.


Hamburger1은 @ToString이 만든 문자열 표현을 사용한다.

// 출력결과
// 마이콜의 햄버거 : Hamburger1(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

Hamburger2는 직접 작성한 toString() 메서드를 사용한다.

// 출력결과
// 마이콜의 햄버거 : Hamburger [bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6]

Hamburger3은 @ToString이 만든 문자열 표현을 사용한다.

// 출력결과
// 마이콜의 햄버거 : Hamburger3(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

문자열 모양은 다르지만 핵심 값은 같다.
마이콜 햄버거는 세 방식 모두 빵이 2, 베이컨이 6이다.
나머지 패티, 치즈, 양상추, 토마토는 0이다.


여기서 중요한 것은 출력 형식이 완전히 같은지가 아니다.
세 방식 모두 원하는 객체 값을 만들 수 있는지 확인하는 것이다.


출력 문자열 모양은 toString()을 직접 작성했는지, @ToString이 자동 생성했는지에 따라 달라질 수 있지만, 비교해야 할 핵심은 객체 안의 필드 값이다.


언제 어떤 방식을 이해하면 좋은가

초보자 단계에서는 세 방식을 모두 외우려고 하기보다, 문제 상황을 기준으로 이해하는 것이 좋다.


생성자 방식은 객체 생성의 가장 기본 흐름이다.
값을 순서대로 전달하고 생성자가 그 값을 필드에 저장한다.
그래서 객체 생성의 기본 원리를 이해할 때 필요하다.


직접 만든 Builder 방식은 Builder Pattern의 동작 원리를 이해할 때 필요하다.
builder()가 무엇을 반환하는지, return this가 왜 필요한지, build()에서 최종 객체가 만들어지는 흐름을 볼 수 있다.


Lombok Builder 방식은 실제 코드에서 반복 코드를 줄이고 싶을 때 많이 쓰는 방식이다.
직접 만든 Builder의 원리를 알고 있으면 @Builder가 무엇을 대신 만들어 주는지 이해하기 쉽다.


정리하면 아래처럼 볼 수 있다.

  • 객체 생성의 기본 원리를 볼 때는 생성자 방식을 이해한다.
  • Builder Pattern의 내부 동작을 볼 때는 직접 만든 Builder 방식을 이해한다.
  • 반복 코드를 줄이는 실제 사용 흐름을 볼 때는 Lombok Builder 방식을 이해한다.

직접 만든 Builder를 먼저 이해하면, Lombok @Builder가 단순한 마법이 아니라 반복 코드를 대신 만들어 주는 도구라는 점을 이해할 수 있다.


실행 흐름 정리

세 방식의 실행 흐름은 최종적으로 객체를 만든다는 점에서는 같다.
하지만 객체를 만들기까지 거치는 단계가 다르다.


생성자 방식의 흐름은 아래와 같다.

  • new Hamburger1(...)을 실행한다.
  • 전달한 값의 개수에 맞는 생성자가 호출된다.
  • 생성자가 전달받은 값을 필드에 저장한다.
  • 객체가 만들어진다.

직접 만든 Builder 방식의 흐름은 아래와 같다.

  • Hamburger2.builder()를 호출한다.
  • 새 Builder 객체가 만들어진다.
  • .bun(), .bacon() 같은 메서드가 값을 저장한다.
  • 각 메서드는 return this로 같은 Builder 객체를 반환한다.
  • .build()가 호출된다.
  • private Hamburger2(Builder builder) 생성자가 실행된다.
  • Builder에 모인 값이 최종 객체에 저장된다.

Lombok Builder 방식의 흐름은 아래와 같다.

  • Hamburger3.builder()를 호출한다.
  • Lombok이 자동 생성한 Builder 흐름이 시작된다.
  • .bun(), .bacon() 같은 자동 생성 메서드로 값을 설정한다.
  • .build()가 호출된다.
  • 최종 Hamburger3 객체가 만들어진다.

흐름을 한 줄로 비교하면 다음과 같다.


생성자 방식 → 값을 순서대로 전달해서 바로 객체 생성


직접 만든 Builder → 값을 이름이 있는 메서드로 모은 뒤 직접 작성한 build()로 객체 생성


Lombok Builder → 값을 이름이 있는 메서드로 모은 뒤 자동 생성된 build()로 객체 생성


세 방식은 모두 객체를 만들지만, 생성자 방식은 순서 중심이고 Builder 방식은 이름 중심이다.


결과물 뽑기

이 비교 예제는 웹 화면이 아니라 Java 실행 예제이다.
따라서 결과물은 IntelliJ 콘솔에서 확인한다.


실행 방법은 아래와 같다.

  • Hamburger1App.java를 실행해서 생성자 방식 출력 결과를 확인한다.
  • Hamburger2App.java를 실행해서 직접 만든 Builder 방식 출력 결과를 확인한다.
  • Hamburger3App.java를 실행해서 Lombok Builder 방식 출력 결과를 확인한다.
  • 각 실행 결과에서 같은 이름의 햄버거가 어떤 필드 값을 가지는지 비교한다.

비교할 때는 특히 마이콜 햄버거를 보면 좋다.
세 방식 모두 마이콜 햄버거는 빵 2, 베이컨 6을 가진다.
다만 작성 방식이 다르다.


생성자 방식 결과는 아래처럼 볼 수 있다.

// 출력결과
// 마이콜의 햄버거 : Hamburger1(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

직접 만든 Builder 방식 결과는 아래처럼 볼 수 있다.

// 출력결과
// 마이콜의 햄버거 : Hamburger [bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6]

Lombok Builder 방식 결과는 아래처럼 볼 수 있다.

// 출력결과
// 마이콜의 햄버거 : Hamburger3(bun=2, patty=0, cheese=0, lettuce=0, tomato=0, bacon=6)

캡처를 남긴다면 각 실행 클래스의 콘솔 출력 결과를 각각 캡처하면 된다.
또는 세 콘솔 결과 중 마이콜 출력 줄만 비교해도 된다.


결과물에서 확인할 핵심은 출력 문자열의 모양이 아니라 객체 안의 값이다.
세 방식 모두 마이콜 햄버거의 핵심 값은 bun=2, bacon=6으로 같다.
하지만 그 값을 넣는 코드의 가독성은 다르다.


결과물은 같은 값을 만들 수 있음을 보여 주고, 코드 비교는 그 값을 만드는 방식의 차이를 보여 준다.


핵심 정리

이 예제의 핵심은 세 가지 객체 생성 방식을 비교하는 것이다.
Hamburger1, Hamburger2, Hamburger3은 모두 햄버거 객체를 만들지만, 객체를 만드는 방식과 코드의 읽기 쉬움이 다르다.


이 흐름은 아래처럼 정리할 수 있다.

  • Hamburger1은 생성자 오버로딩 방식으로 객체를 만든다.
  • 생성자 방식은 코드가 짧지만 값의 의미가 바로 보이지 않는다.
  • 생성자 방식은 같은 타입 값이 많을 때 순서 실수가 생기기 쉽다.
  • Hamburger2는 직접 만든 Builder 방식으로 객체를 만든다.
  • 직접 만든 Builder는 값의 의미가 메서드 이름으로 보인다.
  • 직접 만든 Builder는 return this와 build() 흐름을 직접 확인할 수 있다.
  • 직접 만든 Builder는 구조를 이해하기 좋지만 반복 코드가 많다.
  • Hamburger3은 Lombok @Builder 방식으로 객체를 만든다.
  • Lombok @Builder는 Builder 관련 반복 코드를 자동 생성한다.
  • Lombok Builder는 사용하는 쪽에서는 직접 만든 Builder와 거의 같은 흐름을 가진다.
  • 설정하지 않은 int 값은 기본값 0으로 처리된다.
  • 출력 형식은 다를 수 있지만, 객체 안에 저장된 핵심 값은 비교할 수 있다.

따라서 이 예제가 알려 주는 핵심은 이것이다.
생성자 방식은 값의 순서에 의존하고, 직접 만든 Builder는 값의 의미를 메서드 이름으로 드러내며, Lombok Builder는 그 Builder 흐름을 유지하면서 반복 코드를 줄여 준다.
이렇게 세 방식을 비교하면 Builder Pattern이 왜 필요한지, 그리고 Lombok @Builder가 어떤 코드를 대신 만들어 주는지 한 번에 정리할 수 있다.

0개의 댓글