
이 예제는 햄버거 객체를 생성자 방식으로 만드는 흐름을 보여 준다.
햄버거에는 빵, 패티, 치즈, 양상추, 토마토, 베이컨 같은 재료가 들어간다.
이 재료들은 모두 숫자로 표현되고, 객체를 만들 때 생성자에 순서대로 전달된다.
여기서 중요한 점은 값이 많아질수록 생성자에 전달하는 숫자의 의미가 잘 보이지 않는다는 것이다.
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방식으로 햄버거 객체를 생성한다.
이 예제는 생성자 오버로딩 대신 직접 만든
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가 어떤 반복 코드를 대신 만들어 주는지 더 쉽게 이해할 수 있다.
이 예제는 직접
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 thisbuild()메서드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를 한 번에 비교해서 각각의 차이를 정리한다.
이 예제는 앞에서 만든
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가 어떤 코드를 대신 만들어 주는지 한 번에 정리할 수 있다.