빌더 패턴이란?
복잡한 객체의 생성을 단순화하는 생성 디자인 패턴으로, 단계별로 객체를 만들 수 있다.
복잡한 객체를 만들 때 가독성과 일반적인 개발자 사용성이 크게 향상된다.
어떨 때 사용하면 좋은가?
빌더 패턴의 장점을 살릴 수 있는 가장 명확한 상황은 인자의 목록이 길거나, 많은 복잡한 매개변수를 입력으로 사용하는 생성자가 있는 클래스이다.
일반적으로 이러한 종류의 클래스들은 모두 완전하고 일관된 상태의 인스턴스를 만들기 위해, 너무 많은 매개변수들을 필요로 하기 때문에 고려해볼 필요가 있다.
문제의 클래스 예시
class Boat {
constructor(
hasMotor,
motorCount,
motorBrand,
motorModel,
hasSails,
sailsCount,
sailsMaterial,
sailsColor,
hullColor,
hasCabin
) {
this.hasMotor = hasMotor;
this.hasMotor = hasMotor;
this.motorCount = motorCount;
this.motorBrand = motorBrand;
this.motorModel = motorModel;
this.hasSails = hasSails;
this.sailsCount = sailsCount;
this.sailsMaterial = sailsMaterial;
this.sailsColor = sailsColor;
this.hullColor = hullColor;
this.hasCabin = hasCabin;
}
}
const myBoat = new Boat(true,2,'Best Motor Co. ','OM123',true,1,'fabric','white','blue',false);
이러한 생성자의 디자인을 개선하는 첫 번째 단계는 모든 인자를 하나의 객체 리터럴에 모으는 것이다.
class Boat {
constructor(params) {
this.hasMotor = params.hasMotor;
this.hasMotor = params.hasMotor;
this.motorCount = params.motorCount;
this.motorBrand = params.motorBrand;
this.motorModel = params.motorModel;
this.hasSails = params.hasSails;
this.sailsCount = params.sailsCount;
this.sailsMaterial = params.sailsMaterial;
this.sailsColor = params.sailsColor;
this.hullColor = params.hullColor;
this.hasCabin = params.hasCabin;
}
}
const myBoat = new Boat({
hasMotor: true,
motorCount: 2,
motorBrand: 'Best Motor Co. ',
motorModel: 'OM123',
hasSails: true,
sailsCount: 1,
sailsMaterial: 'fabric',
sailsColor: 'white',
hullColor: 'blue',
hasCabin: false,
});
하지만 하나의 객체 리터럴을 사용하여 모든 입력을 한 번에 전달하는 경우,
실제 입력이 무엇인지 알기 위해 클래스를 정의한 문서를 보거나, 클래스의 코드를 봐야 하는 문제점이 있다.
빌더 패턴 예시 1
class Boat {
constructor(allParameters) {
this.hasMotor = allParameters.hasMotor;
this.motorCount = allParameters.motorCount;
this.motorBrand = allParameters.motorBrand;
this.motorModel = allParameters.motorModel;
this.hasSails = allParameters.hasSails;
this.sailsCount = allParameters.sailsCount;
this.sailsMaterial = allParameters.sailsMaterial;
this.sailsColor = allParameters.sailsColor;
this.hullColor = allParameters.hullColor;
this.hasCabin = allParameters.hasCabin;
}
}
class BoatBuilder {
withMotors(count, brand, model) {
this.hasMotor = true;
this.motorCount = count;
this.motorBrand = brand;
this.motorModel = model;
return this;
}
withSails(count, material, color) {
this.hasSails = true;
this.sailsCount = count;
this.sailsMaterial = material;
this.sailsColor = color;
return this;
}
hullColor(color) {
this.hullColor = color;
return this;
}
withCabin() {
this.hasCabin = true;
return this;
}
build() {
return new Boat({
hasMotor: this.hasMotor,
motorCount: this.motorCount,
motorBrand: this.motorBrand,
motorModel: this.motorModel,
hasSails: this.hasSails,
sailsCount: this.sailsCount,
sailsMaterial: this.sailsMaterial,
sailsColor: this.sailsColor,
hullColor: this.hullColor,
hasCabin: this.hasCabin,
});
}
}
const myBoat = new BoatBuilder()
.withMotors(2, 'Best Motor Co.', 'OM123')
.withSails(1, 'fabric', 'white')
.withCabin()
.hullColor('blue')
.build();
console.log(myBoat);
추가로 만든 BoatBuilder 클래스의 역할은 일부 헬퍼 함수를 사용하여 Boat를 생성하는데 필요한 모든 매개변수를 모으는 것이다.
일반적으로 각 매개변수 또는 일련의 매개변수들을 위한 함수는 존재하지만 이에 대한 명확한 규칙은 없다.
입력 매개변수 수집을 담당하는 각 함수의 이름과 동작을 결정하는 것은 builder 클래스를 만드는 이에게 달려있다.
빌더 패턴을 구현하기 위한 몇 가지 일반적인 규칙
- 주요 목적은 복잡한 생성자를 더 읽기 쉽고 관리하기 쉬운 여러 단계로 나누는 것
- 한번에 관련된 여러 매개변수들을 설정할 수 있는 빌더 함수를 만들기
- setter 함수를 통해 입력받을 값이 무엇일지 명확히 하고, 빌더 인터페이스를 사용하는 사용자가 알 필요가 없는 파라미터를 세팅하는 더 많은 로직을 setter 함수 내에 캡슐화한다.
- 필요하다면, 클래스의 생성자에게 매개변수를 전달하기 전에 형 변환, 정규화 혹은 추가적인 유효성 검사와 같은 조작을 추가하여 빌더 클래스를 사용하는 사람이 수행해야 할 작업을 훨씬 더 단순화할 수도 있다.
빌더 패턴 예시 2
표준 URL의 모든 구성 요소를 저장하고 검증한 다음 문자열로 다시 형식을 만들 수 있는 URL 클래스 만들기.
export class Url {
constructor(protocol, username, password, hostname, port, pathname, search, hash) {
this.protocol = protocol;
this.username = username;
this.password = password;
this.hostname = hostname;
this.port = port;
this.pathname = pathname;
this.search = search;
this.hash = hash;
this.validate();
}
validate() {
if (!this.protocol || !this.hostname) {
throw new Error('Must specify at least a protocal and a hostname');
}
}
toString() {
let url = '';
url += `${this.protocol}://`;
if (this.username && this.password) {
url += `${this.username}:${this.password}@`;
}
url += this.hostname;
if (this.port) {
url += `:${this.port}`
}
if (this.pathname) {
url += this.pathname;
}
if (this.search) {
url += `?${this.search}`;
}
if (history.hash) {
url += `#${this.hash}`;
}
return url;
}
}
표준 URL은 여러 구성 요소로 구성되어 있어 모두 가져오려면 클래스의 생성자가 필연적으로 커야한다.
그리고 전달하는 매개변수가 이러한 구성요소 중 어떤 값인지를 알고 있어야 하기 때문에 생성자를 호출하는 것이 어려운 일이 될 수 있다.
빌더 클래스 만들기
import { Url } from './url.mjs';
export class UrlBuilder {
setProtocol(protocol) {
this.protocol = protocol;
return this;
}
setAuthentication(username, password) {
this.username = username;
this.password = password;
return this;
}
setHostname(hostname) {
this.hostname = hostname;
return this;
}
setPort(port) {
this.port = port;
return this;
}
setPathname(pathname) {
this.pathname = pathname;
return this;
}
setSearch(search) {
this.search = search;
return this;
}
setHash(hash) {
this.hash = hash;
return this;
}
build() {
return new Url(
this.protocol,
this.username,
this.password,
this.hostname,
this.port,
this.pathname,
this.search,
this.hash
);
}
}
위의 setAuthentication() 함수의 경우 두 개의 매개변수(사용자이름, 패스워드)를 하나의 함수로 결합하여 URL에 인증 정보를 지정하려는 경우 2개의 매개변수를 제공해야 한다는 사실을 명확하게 해 줄 수 있다.
빌더 패턴 사용 시 장점 중 하나라고 생각된다.
빌더 클래스 사용
import { UrlBuilder } from './urlBuilder.mjs';
const urlA = new UrlBuilder()
.setProtocol('https')
.setAuthentication('test', 'test')
.setHostname('localhost')
.setPort(9200)
.build();
console.log(urlA.toString());
각 setter 함수는 우리가 설정하는 매개변수에 대한 힌트를 명확히 제공한다.
실제 사용되는 곳
책에서 이 패턴에 대해 보고 난 후 현재의 나의 지식으론 이걸 실제로 어떻게 쓰일지 감이 잘 안 잡히긴 했다.
하지만 책에서 빌더 패턴은 복잡한 객체를 생성하거나 복잡한 함수를 호출하는 문제에 대한 해결책을 제공하기 때문에 Node.js 및 JavaScript에서 매우 일반적인 패턴이라고 한다.
예시 중 하나가 http와 https에 내장된 request() API로 HTTP(S) 클라이언트 요청을 생성하는 것.
관련 문서 https://nodejs.org/api/http.html#http_http_request_url_options_callback를 살펴보면 많은 옵션을 허용한다는 것을 알 수 있고, 빌더 패턴이 잠재적으로 더 좋은 인터페이스를 제공할 수 있다는 일반적인 예시라고 한다.
그리고 실제로 많이 사용하는 HTTP(S) 요청에 대한 래퍼 중 하나인 superagent가 빌더 패턴으로 구현해 새로운 요청의 생성을 단순화하는 것을 목표로 하며, 단계적으로 새로운 요청을 생성하는 인터페이스를 제공한다.
import superagent from 'superagent';
superagent
.post('https://example.com/api/person')
.send({ name: 'John Doe', role: 'user' })
.set('accept', 'json')
.then(response => {});
이 코드를 보면 위에 작성했던 예시 코드에서 보이던 build() 함수가 없고 new 연산자를 사용하지 않는다.
대신 then() 함수를 호출하여 실행한다.
마치며
그동안 그냥 쓰던 것들 중에서 빌더 패턴으로 만들어진 라이브러리를 쓰고 있었다는 걸 알게된 것 같다.
Node.js 디자인 패턴 바이블 책 Chapter7을 읽던 중 보게된 부분이고, 블로그에 남겨두면 좋을 것 같아 작성하게 되었다.
'Node.js' 카테고리의 다른 글
[NodeJS] nodemailer로 이메일 보내기 (0) | 2023.04.19 |
---|---|
[NodeJS] process.nextTick()과 setImmediate(), setTimeout() (0) | 2022.09.02 |
[NodeJS] 이벤트 디멀티플렉싱과 리액터패턴 (0) | 2022.08.15 |
[NodeJS] Error : Cannot overwrite users model once compiled (0) | 2022.06.24 |
[NodeJS] express+mysql2+transaction 데이터 처리 (0) | 2022.03.15 |