빌더 패턴이란?
- 객체 생성에 연관된 패턴이다.
- 복잡한 객체들을 단계별로 생성할 수 있게 만들어 복잡도를 줄여준다.
- 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법을 제공한다.
해결하려는 문제
- 객체에 아주 많은 필드가 들어있어 복잡할 때 객체 생성이 어려운 문제가 있다.
- 생성자에 필드를 몰아놓으면 처음 객체를 생성하기가 매우 어렵다.
- Setter 를 이용하려고 하면, 필수값과 연계값 등 제약조건을 넣기 힘들다.
복잡한 객체를 생성할 때 빌더 패턴을 사용하면 조금 더 쉽게 생성할 수 있다.
빌더 패턴 다이어그램으로 살펴보기
- 복잡한
Product
를 만드는 경우,Builder
에 추상 메서드를 추가하고,ConcreteBuilder
가 이를 구현하도록 위임하여 복잡한 객체를 만드는 프로세스를 독립적으로 분리시킬 수 있다. Client
를 이용하지 않고,Director
를 이용하여 빌더를 사용하면, 빌더를 통해Product
를 만드는 과정을Client
와 더 멀어지도록 분리시킬 수 있다.
예제로 살펴보기
- 월세를 구할 수 있는 부동산 앱을 만들려고 한다.
- 집주인은 자신의 집을 올릴 수 있는데, 다양한 세부 정보를 설정할 수 있다.
- 세부 정보는 선택사항이며, 전부 입력하지 않아도 된다.
- 집 이름, 주소, 월세, 관리비만 필수이다.
House
객체 만들기
public class House {
private String houseName;
private String address;
private float size;
private int rooms;
private int floor;
private int topFloorOfBuilding;
private int monthlyRent;
private int maintenanceCost;
private boolean hasParkingLot;
private boolean hasAirConditioner;
private boolean hasLaundryMachine;
}
- 집 이름, 주소, 월세, 관리비는 필수이다.
- 월세만 넣는 것은 불가능하고 항상 관리비를 같이 넣어야 한다.
HouseBuilder
인터페이스 만들기
public interface HouseBuilder {
HouseBuilder name(String houseName);
HouseBuilder address(String address);
HouseBuilder totalCost(int monthlyRent, int maintenanceCost);
HouseBuilder houseSize(float size, int rooms);
HouseBuilder floor(int floor, int topFloorOfBuilding);
HouseBuilder parkingLot(boolean hasParkingLot);
HouseBuilder airConditioner(boolean hasAirConditioner);
HouseBuilder laundryMachine(boolean hasLaundryMachine);
House build();
}
DefaultHouseBuilder
구현체 만들기
public class DefaultHouseBuilder implements HouseBuilder {
private String houseName;
private String address;
private float size;
private int rooms;
private int floor;
private int topFloorOfBuilding;
private int monthlyRent;
private int maintenanceCost;
private boolean hasParkingLot;
private boolean hasAirConditioner;
private boolean hasLaundryMachine;
@Override
public HouseBuilder name(String houseName) {
this.houseName = houseName;
return this;
}
@Override
public HouseBuilder address(String address) {
this.address = address;
return this;
}
@Override
public HouseBuilder totalCost(int monthlyRent, int maintenanceCost) {
this.monthlyRent = monthlyRent;
this.maintenanceCost = maintenanceCost;
return this;
}
@Override
public HouseBuilder houseSize(float size, int rooms) {
this.size = size;
this.rooms = rooms;
return this;
}
@Override
public HouseBuilder floor(int floor, int topFloorOfBuilding) {
this.floor = floor;
this.topFloorOfBuilding = topFloorOfBuilding;
return this;
}
@Override
public HouseBuilder parkingLot(boolean hasParkingLot) {
this.hasParkingLot = hasParkingLot;
return this;
}
@Override
public HouseBuilder airConditioner(boolean hasAirConditioner) {
this.hasAirConditioner = hasAirConditioner;
return this;
}
@Override
public HouseBuilder laundryMachine(boolean hasLaundryMachine) {
this.hasLaundryMachine = hasLaundryMachine;
return this;
}
@Override
public House build() {
return new House(
houseName
, address
, size
, rooms
, floor
, topFloorOfBuilding
, monthlyRent
, maintenanceCost
, hasParkingLot
, hasAirConditioner
, hasLaundryMachine);
}
}
HouseBuilder
의 메서드들을 구현해두었다.- 반환 타입이
HouseBuilder
라 메서드 체인 형태로 사용할 수 있다.
클라이언트에서 사용하기
public class App {
public static void main(String[] args) {
HouseBuilder houseBuilder = new DefaultHouseBuilder();
House house = houseBuilder
.address("북가좌동")
.name("이랜드 해가든 아파트")
.totalCost(2000000, 100000)
.airConditioner(true)
.laundryMachine(false)
.parkingLot(true)
.build();
System.out.println(house);
}
}
- 선택적으로 넣을 값들을 메서드 체인으로 연결하여 객체를 생성했다.
- 메서드 이름이 명확해 가독성도 좋은 편이다.
Director
객체 만들어보기
public class HouseDirector {
private final HouseBuilder houseBuilder;
public HouseDirector(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
public House gardenApartment() {
return houseBuilder
.address("북가좌동")
.name("이랜드 해가든 아파트")
.totalCost(2000000, 100000)
.airConditioner(true)
.laundryMachine(false)
.parkingLot(true)
.build();
}
}
HouseBuilder
를 주입받아 이용하였다.
클라이언트 코드 변경하기
public class App {
public static void main(String[] args) {
HouseDirector houseDirector = new HouseDirector(new DefaultHouseBuilder());
House house = houseDirector.gardenApartment();
System.out.println(house);
}
}
- 클라이언트에는 더욱 구체적인 내용을 숨겼다.
- 클라이언트는
houseDirector.gardenApartment()
메서드를 호출하면 해당 아파트 매물이 나온다는 것만 알면 된다.
빌더 패턴의 장점 알아보기
- 이미 처음에 언급했듯, '만들기 복잡한 객체'를 순차적으로 만들 수 있는 방법을 제공해줄 수 있다.
인터페이스 반환 타입을 통해 순서 강제하기
public interface HouseBuilder {
// 순서를 강제하는 예제
HouseBuilderStep1 name(String houseName);
HouseBuilderStep2 address(String address);
HouseBuilderStep3 totalCost(int monthlyRent, int maintenanceCost);
// 나머지 스텝들 ...
House build();
}
- 순서를 강제하면, 객체 설계자가 의도한대로 객체를 생성할 확률이 높아진다.
build()
단계에서 데이터 검증하기
@Override
public House build() {
if (this.houseName == null) {
throw new IllegalArgumentException("집 이름은 필수적으로 입력해야 하는 항목입니다.");
}
if (this.monthlyRent == 0 || this.maintenanceCost == 0) {
throw new IllegalArgumentException("월세와 관리비는 필수적으로 입력해야 하는 항목입니다.");
}
// ...
}
- 이 검증은 불완전한 객체를 생성하지 못하게 하는 안전장치가 될 수 있다.
build()
메서드에 검증을 추가하면 마지막 생성 시점에 올바르게 생성했는지 검증 가능하다.- 빈 객체를 만들고 Setter 를 이용하는 방법은 보통 값 하나씩만 검증이 가능한 반면,
build()
메서드에서는 전체 값을 연계하여 값 검증이 가능하다.- ex) 주차장이 없는 경우, 월세와 관리비의 합이
5000000
을 넘으면 안된다고 가정했을 때, Setter 만을 이용해 검증한다면setHasParkingLot()
,setMonthlyRent()
,setMaintenanceCost()
메서드 모두에 검증 장치를 심어두어야 한다.
- ex) 주차장이 없는 경우, 월세와 관리비의 합이
Director
객체를 통해 구현 숨기기
Director
를 이용하여 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.- 구체적 과정을 숨기지 않는다면, 클라이언트 코드의 전체적인 흐름보다 이쪽에 눈길이 가는 가독성이 떨어지는 코드가 탄생할 수 있다.
불변 객체를 편하게 만들 수 있다.
- 불변 객체를 만들기 위해
final
필드를 많이 깔아두면, 생성자에서final
필드를 모두 설정해주어야 하는 불편함이 있다.- 생성자는 오버로딩을 통해 여러개를 만드는 것이 가능하지만, 생성자 특징상 뒤에 넣어야 하는 인자가 너무 많아지며 가독성이 매우 떨어진다.
- 빌더 패턴을 이용하면,
build()
전, 모든final
값이 들어왔는지 검증 후 객체를 만들면 간단하다.
public class House {
private final String houseName;
private final int monthlyRent;
private final int maintenanceCost;
// ...
}
public House gardenApartment() {
return houseBuilder
.address("북가좌동")
.name("이랜드 해가든 아파트")
.totalCost(2000000, 100000)
.airConditioner(true)
.laundryMachine(false)
.parkingLot(true)
.build();
}
final
로 선언된 필드가 있음에도 직접 생성자를 이용하지 않아도 된다.- 생성자는 메서드 이름에 의도를 나타낼 수도 없고 무조건 클래스 이름과 같아야 하기 때문에 파라미터가 길어질수록 가독성이 매우 떨어진다.
HouseBuilder
인터페이스를 통해 확장성을 꾀할 수 있다.
HouseBuilder
는 인터페이스기 때문에 어떤 클래스나 인터페이스에서도 상속이 가능하다.- 나중에 예제의 부동산 앱이 고도화된다면, 매물 별로
ApartmentBuilder
,OfficetelBuilder
,VillaBuilder
등을 생성하여build()
시 필수 값들을 다르게 설정하여 확장할 수도 있다.
Lombok 을 사용하면 @Builder
애노테이션으로 쉽게 구현할 수 있다.
@Builder
애노테이션은 빌더 형태로 객체를 만드는 것을 도와준다.
@AllArgsConstructor
@Builder
public class House {
@NonNull
private final String houseName;
private String address;
private float size;
private int rooms;
private int floor;
private int topFloorOfBuilding;
private final int monthlyRent;
private final int maintenanceCost;
private boolean hasParkingLot;
private boolean hasAirConditioner;
private boolean hasLaundryMachine;
}
public class App {
public static void main(String[] args) {
House house = House.builder()
.houseName("해가든 아파트")
.monthlyRent(100)
.maintenanceCost(10)
.build();
System.out.println(house);
}
}
- 객체에
.builder()
라는 메서드가 생기고 이를 통해 쉽게 빌드 패턴을 이용할 수 있게 된다.
빌더 패턴의 단점 알아보기
- 객체를 생성하는 과정이 너무 장황해진다.
- 구조가 복잡해진다
- 부가적으로 생성해야 될 인터페이스와 클래스들이 생긴다.
Builder
인터페이스도 만들고 구현체도 만들고Director
도 만들어야 할 수 있다.
자바 표준 라이브러리에서 사용하는 예제
자바 표준 라이브러리에서는 빌더 패턴을 어떻게 사용하고 있을까?
StringBuilder
StringBuilder
클래스는toString()
을 호출하기 전까지 문자열에 대한 정보만 쌓아두다가toString()
을 호출하면 마침내String
타입의 객체를 생성한다.- 아래의 코드는 내부 코드 중 일부를 가져온 것이다.
public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
public StringBuilder append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}
- 반환 타입이 전부
StringBuilder
이다. - 비슷한 예로
StringBuffer
가 있는데,StringBuilder
와는synchronized
를 사용하냐 안하냐의 차이를 가지고 있다.StringBuffer
는synchronized
를 사용한다.StringBuilder
는synchronized
를 사용하지 않는다.
- 또 비슷한 예로
StreamBuilder
도 있다.
Stream<String> names = Stream
.<String>builder()
.add("HELLO")
.add("HELLO")
.build();
메서드가 제네릭 타입을 반환한다면, 제네릭 타입에 넣어줄 인자 타입을 앞에 적어주어야 한다.
스프링에서 제공하는 UriComponentsBuilder
public class SpringExample {
public static void main(String[] args) {
UriComponents blogPath = UriComponentsBuilder.newInstance()
.scheme("https")
.host("jake-seo-dev.tistory.com")
.path("/category/알고리즘 이론/배열과 문자열")
.build().encode(); // https://jake-seo-dev.tistory.com/category/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%20%EC%9D%B4%EB%A1%A0/%EB%B0%B0%EC%97%B4%EA%B3%BC%20%EB%AC%B8%EC%9E%90%EC%97%B4
System.out.println(blogPath);
}
}
- 스프링에서는 URI 컴포넌트를 쉽게 빌드할 수 있는 빌더를 제공한다.
- 정적 메서드로
newInstance()
메서드 외에fromPath()
,fromUri()
등 많은 정적 팩토리 메서드를 제공한다.
레퍼런스
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
어댑터 패턴 (Adapter Pattern) 이란? (1) | 2023.01.29 |
---|---|
프로토타입 패턴 (Prototype Pattern) 이란? (2) | 2023.01.28 |
추상 팩토리 패턴 (Abstract Factory Pattern) 이란? (0) | 2023.01.24 |
팩토리 메서드 패턴 (Factory Method Pattern) 이란? (0) | 2023.01.23 |
싱글톤 패턴 (Singleton Pattern) 이란? (0) | 2023.01.20 |