안녕하세요. J4J입니다.
이번 포스팅은 빌더(builder) 패턴에 대해 적어보는 시간을 가져보려고 합니다.
Builder 패턴이란?
builder 패턴은 객체 생성을 유연하게 할 수 있도록 도와주는 디자인 패턴 중 하나입니다.
자바 관점에서 확인했을 때 객체 생성을 할 때 보통 사용될 수 있는 것은 생성자를 활용하는 것입니다.
파라미터 정보가 하나도 들어있지 않은 default 생성자부터 원하는 파라미터 정보로 구성된 생성자까지 활용하여 객체를 생성할 수 있습니다.
builder 패턴은 생성자와 유사하게 객체 생성을 도와주지만 생성하는 방법을 다양하게 구성해 볼 수 있습니다.
클래스 내부에 builder 패턴을 적용하기 위한 설정만 해둔다면 객체 인스턴스를 생성해야 되는 곳에서 메서드 체이닝을 활용하여 각 비즈니스 구간 별 자유롭게 객체 생성을 할 수 있도록 도와줍니다.
그렇기 때문에 객체가 보관해야 되는 필드 값이 추가되는 상황이 발생하거나 필드 값은 동일하지만 객체를 생성할 때 저장되는 필드 정보들을 구분해야 되는 상황에서 효과적으로 사용될 수 있습니다.
builder 패턴을 적용하기 위해서는 여러 가지 방법이 존재합니다.
이 중 특히 spring을 이용하여 개발을 할 때는 builder 패턴을 위한 설정을 각각 모두 관리하지 않고 lombok 활용을 많이 하는 편입니다.
lombok을 이용하면 부가적인 설정 없이 어노테이션 하나만으로 builder 패턴을 바로 사용할 수 있기에 특별한 이유가 없다면 lombok 활용을 추천드립니다.
생성자와의 차이점
builder 패턴과 생성자의 차이점을 알아보기 위해 먼저 동일한 목적의 객체를 생성하는 경우 어떻게 코드가 작성되는지 확인해 보겠습니다.
// builder
package com.jforj.builder;
import lombok.Builder;
@Builder
public class BuilderCase {
private long no;
private String name;
private String address;
}
// constructor
package com.jforj.builder;
public class ConstructorCase {
private long no;
private String name;
private String address;
public ConstructorCase(long no) {
this.no = no;
}
public ConstructorCase(long no, String name) {
this.no = no;
this.name = name;
}
// (long no, String address) 생성자와 오버로딩이 겹치기 때문에 사용 불가
// public ConstructorCase(long no, String address) {
// this.no = no;
// this.address = adress;
// }
}
// main
package com.jforj.builder;
public class Main {
public static void main(String[] args) {
long no = 100L;
String name = "myName";
String address = "myAddress";
checkBuilderCase(no, name, address);
checkConstructorCase(no, name, address);
}
public static void checkBuilderCase(long no, String name, String address) {
BuilderCase builderCase1 =
BuilderCase
.builder()
.no(no)
.build();
BuilderCase builderCase2 =
BuilderCase
.builder()
.no(no)
.name(name)
.build();
BuilderCase builderCase3 =
BuilderCase
.builder()
.no(no)
.address(address)
.build();
}
public static void checkConstructorCase(long no, String name, String address) {
ConstructorCase constructorCase1 = new ConstructorCase(no);
ConstructorCase constructorCase2 = new ConstructorCase(no, name);
// ConstructorCase constructorCase3 = new ConstructorCase(no, address); 오버로딩이 겹치기 때문에 사용 불가
}
}
코드를 보면 알 수 있는 것처럼 builder 패턴을 적용할 때와 생성자를 이용할 때 차이점이 간단하게 확인될 수 있습니다.
하지만 builder 패턴이 항상 정답은 아니기에 둘에 대해서 비교를 해보면 다음과 같이 정리할 수 있습니다.
- | Builder | 생성자 |
---|---|---|
객체 필드 정보가 많을 때 | 객체 생성이 필요한 곳에서 자유롭게 구성이 가능 | 객체 생성이 필요한 곳마다 필드 정보 조합을 위한 생성자를 매번 작성 및 관리 |
가독성 | 객체를 생성할 때 필드 정보를 직관적으로 확인하기 때문에 가독성이 좋음 | 파라미터에 값을 넣는 형식이기 때문에 가독성이 좋지 않음 |
파라미터 순서 | 순서에 영향을 받지 않음 | 순서가 다를 경우 잘못된 정보가 입력될 수 있음 |
오버로딩이 겹치는 경우 | 겹치는 경우가 발생하지 않음 | 겹치는 경우 에러가 발생 |
상황 별 생성되어야 하는 필드 정보 관리 | 재 사용되는 메서드와 같은 개념이 존재하지 않기 때문에 관리하기 힘듬 | 정의되어 있는 생성자를 사용하면 되기 때문에 관리하기 용이함 |
결론적으로 builder 패턴과 생성자 중 어떤 상황에 무엇을 사용해야 되는지 고민될 수 있습니다.
경험 상 간단하게만 서술한다면 일반적인 경우에는 builder 패턴을 활용하여 객체 생성을 해주시면 됩니다.
하지만 동일한 파라미터 정보가 입력되어야 하는 객체 생성이 빈번하게 발생되거나 매우 적은 양의 필드들이 관리되는 경우, 변경점이 거의 발생되지 않는 경우 등에서는 생성자를 사용하는 것이 더 효율적일 수 있습니다.
Setter와의 차이점
이번에는 setter와의 차이점을 보기 위해 코드 비교를 먼저 해보겠습니다.
// builder
package com.jforj.builder;
import lombok.Builder;
@Builder
public class BuilderCase {
private long no;
private String name;
private String address;
}
// setter
package com.jforj.builder;
import lombok.Setter;
@Setter
public class SetterCase {
private long no;
private String name;
private String address;
}
// main
package com.jforj.builder;
public class Main {
public static void main(String[] args) {
long no = 100L;
String name = "myName";
String address = "myAddress";
checkBuilderCase(no, name, address);
checkSetterCase(no, name, address);
}
public static void checkBuilderCase(long no, String name, String address) {
BuilderCase builderCase1 =
BuilderCase
.builder()
.no(no)
.build();
BuilderCase builderCase2 =
BuilderCase
.builder()
.no(no)
.name(name)
.build();
BuilderCase builderCase3 =
BuilderCase
.builder()
.no(no)
.address(address)
.build();
}
public static void checkSetterCase(long no, String name, String address) {
SetterCase setterCase1 = new SetterCase();
setterCase1.setNo(no);
SetterCase setterCase2 = new SetterCase();
setterCase2.setNo(no);
setterCase2.setName(name);
SetterCase setterCase3 = new SetterCase();
setterCase3.setNo(no);
setterCase3.setAddress(address);
}
}
코드로 확인되는 것처럼 setter 또한 builder 패턴과의 차이점을 간단하게 볼 수 있습니다.
setter와의 차이점도 비교를 해보면 다음과 같이 정리할 수 있습니다.
- | Builder | Setter |
---|---|---|
불변성 | 불변성으로 관리하기 용이함 | 불변성으로 관리할 수 없음 |
객체 생성과 필드 정보 입력 구분 | 객체 생성과 필드 정보 입력이 동시 발생 | 객체 생성이 발생된 이후 필드 정보를 입력 |
사실 setter의 사용은 개발적인 관점에서 지양되는 편입니다.
왜냐하면 setter가 설정되어 있는 경우 객체 생성과 별개로 자유롭게 내부 필드 정보들을 변경할 수 있어서 불변성을 제공하지 않기 때문입니다.
즉, builder 패턴과 setter에 대해서 고민을 하시는 경우라면 모든 상황에 대해 builder 패턴의 사용을 적극 권장드리는 편입니다.
Builder 패턴 특징
생성자와 setter와 비교해 보면서 어느 정도 builder 패턴의 특징들을 확인할 수 있었습니다.
다시 한번 더 builder 패턴의 특징만 정리해 본다면 다음과 같습니다.
[ 장점 ]
- 객체 내부에 관리되는 필드 정보가 많거나 복잡한 구조에서 객체 생성을 할 때 용이함
- 객체 내부 필드 순서는 영향을 주지 않음
- 불변성 처리를 할 수 있음
- 가독성이 좋음
[ 단점 ]
- 다양한 곳에서 동일한 필드 정보들이 사용되는 것을 관리하기 어려움
- 다른 곳에서는 사용되지만 특정 로직에서 필요 없는 필드 정보들의 builder 패턴 적용을 제한할 수 없음
이상으로 빌더(builder) 패턴에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'설계 > 디자인패턴' 카테고리의 다른 글
[디자인패턴] 추상 팩토리(Abstract Factory) 패턴 이해하기 (0) | 2024.04.28 |
---|---|
[디자인패턴] 템플릿 메서드(Template Method) 패턴 이해하기 (0) | 2024.04.22 |
[디자인패턴] 팩토리 메서드(Factory Method) 패턴 이해하기 (0) | 2024.04.22 |
[디자인패턴] 퍼사드(Facade) 패턴 이해하기 (0) | 2024.04.01 |
[디자인패턴] 싱글톤(Singleton) 패턴 이해하기 (0) | 2024.03.30 |
댓글