디자인 패턴(구)/생성 패턴

빌더(Builder) 패턴이란?

공대키메라 2022. 4. 6. 14:17

 

이번에는 빌더 패턴에 대해서 배울것이다. 

 

이번 빌더 패턴을 공부하면서 떠오른 생각은 전에 내가 작성했던 회사 코드에 이것을 적용했으면 더 좋앗을것 싶은 것이 있다.

 

그래서, 적용을 해보고 어떤식으로 적용을 했으면 더 좋았을지 고민하려고 한다. 

 

1. 빌더 패턴이란?

의도

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있다.

 

다시 말하면, 동일한 프로세스를 거쳐 다양한 구성의 인터페이스를 만드는 방법을 정의한다. 

 

이 빌더 패턴을 통해 우리는 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

사용 시기

  • 복합 객체의 생성 알고리즘이 이를 합성하는 요소 객체들이 무엇인지 이들의 조립 방법에 독립적일 때
  • 합성할 객체들의 표현이 서로 다르더라도 생성 절차에서 이를 지원해야 할 때

즉, 인스턴스가 다양한 구성을 만들어 질 수 있는데 이 인스턴스를 생성하는 동일한 프로세스를 만들어 생성할 수 있게끔 해주는 패턴이다. 

구조

 

  • builder : product 객체의 일부 요소들을 생성하기 위한 추상 인터페이스를 정의 
  • ConcreteBuilder : Builder클래스에 정의된 인터페이스를 구현하며, 제품의 부품들을 모아 빌더를 복합한다. 
  • Director : builder인터페이스를 사용하는 객체를 합성
  • Product : 생성할 복합 객체를 표현한다. 

 

장점

  • 만들기 복잡한 객체를 순차적으로 만들 수 있는 방법을 제공한다. 
  • 복잡한 객체를 만드는 과정으로 director를 통해 숨길 수 있다.
  • 불완전한 객체를 사용하지 못하게 안전장치가 있다. 온전한 객체인지 확인하게끔 하기 때문이다.

단점

  • client쪽에서 생성시에 diretor, builder까지 같이 만들어야 한다. 
  • 구조가 기존에 비해 복잡해진다. 

2. 구현하기

컨셉

공대 키메라가 방학 계획을 세우려한다. 매일 매일 계획을 작성하고 하는데 솔직히 정해진 범주는 존재한다.

공부를 한다던지, 밖에 놀라 나간다던지, 게임을 하던지, 운동을 한다던지 하는 등 말이다. 

 

이것을 코드로 한번 구현해서 생성해 보도록 하겠다.

적용 전

VacationPlan.java

import java.time.LocalDate;
import java.util.List;

public class VacationPlan {

    private String title;

    private int days;

    private LocalDate startDate;

    private List<VacationDetailPlan> plans;

    private String doPlace;

    public VacationPlan() {
    }

    public VacationPlan(String title, int days, LocalDate startDate, List<VacationDetailPlan> plans, String doPlace) {
        this.title = title;
        this.days = days;
        this.startDate = startDate;
        this.plans = plans;
        this.doPlace = doPlace;
    }

    //toString
	//getter & setter
    
    public List<VacationDetailPlan> getPlans() {
        return plans;
    }

}

 

VacationDetailPlan.java

public class VacationDetailPlan {

    private int day;

    private String plan;

    public VacationDetailPlan(int day, String plan) {
        this.day = day;
        this.plan = plan;
    }

    public int getDay() {
        return day;
    }

  //getter & setter
  //toString
}

 

Plan.java

import java.time.LocalDate;

public class Plan {

    public static void main(String[] args) {
        VacationPlan shortPlan = new VacationPlan();
        shortPlan.setTitle("방 콕 투어");
        shortPlan.setStartDate(LocalDate.of(2022,04,06));
        shortPlan.setDoPlace("집");

        VacationPlan longTripPlan = new VacationPlan();
        longTripPlan.setTitle("전국 일주");
        longTripPlan.setDays(3);
        longTripPlan.setStartDate(LocalDate.of(2022,04,10));
        longTripPlan.setPlans(0, "서울 여행");
        longTripPlan.setPlans(0, "남산 타워 가서 돈까스 먹기");
        longTripPlan.setPlans(1, "인천 송도 서킷가기");
        longTripPlan.setPlans(1, "인천 차이나타운 가기");
        longTripPlan.setPlans(1, "인천 월미도 가기");
        longTripPlan.setPlans(2, "노잼 도시 대전 방문");
        longTripPlan.setPlans(2, "노잼 도시 대전 둔산동 조지기");
		longTripPlan.setPlans(3, "shane's planet 방문 및 리뷰");
        longTripPlan.setPlans(3, "apple vs window 현피 관람");
    }

}

 

필자는 계획을 세우는 class인 VacationPlan을 만들었다. 여기서 세부 일정을 정의할 수 있는 VacationDetailPlan을 선언해서 설정이 가능하도록 했다. 

 

여기서 문제점은 계획을 작성하는데 있어서 일관된 프로세스가 없다는 점이다. 또한, 어떤 특정 설정에 대해서 강제하고 싶은 경우도 있을 수 있는데 그러한 것도 제어가 불가능하다. 

 

즉, 우리가 최소한으로 지켜야 하는 규약이 있는데 그것을 충족하지 못한 채로 불완전한 객체가 생성될 수 있다.

 

또한, VacationPlan을 만드는 생성자를 사용할 때, 여러가지 상황이 생기는 경우 굉장히 장황해질 수 있다는 점이다. 

 

VacationPlan을 생성자로 만들게 되면 생성자 파라미터에 따라서 모든 경우를 만들어 줘야하니 그것도 문제다.

 

어떤 여행은 당일치기라서 장소 혹은 여행 일수를 설정 안해도 될 수도 있고, 다른 여행은 장기 투숙 여행이라 목적지에 대한 계획이 필요할 수도 있고, 여러 상황이 존재하지만, 현재 구조로는 이것을 깔끔하게 여행 플랜을 구성하기 힘들다. 

 

여기서 다시 빌더 패턴의 의도를 보자.

 

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있다.

 

우리가 말한 복잡한 객체 생성 방법에서 동일한 절차를 제공할 수 있어야 위의 문제를 해결할 수 있을 것 같다. 

 

적용 후

VacationPlanBuilder.java


import java.time.LocalDate;

public interface VacationPlanBuilder {

    //VacationPlanBuilder를 통해서 메소드 체인이 되도록 함
    VacationPlanBuilder title(String title);

    VacationPlanBuilder days(int days);

    VacationPlanBuilder startDate(LocalDate startDate);

    VacationPlanBuilder doPlace(String doPlace);

    VacationPlanBuilder addPlan(int day, String plan);

    VacationPlan getPlan();

}

DefaultVacationBuilder.java

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class DefaultVacationBuilder implements VacationPlanBuilder{

    private String title;

    private int days;

    private LocalDate startDate;

    private List<VacationDetailPlan> plans;

    private String doPlace;

	/*
    	동일한 property를 선언하는 것이 싫다면 
        다음처럼 사용할 수 있다.
        private VacationPlan vacationPlan;

        public VacationPlanBuilder newInstance(){
            this.vacationPlan = new VacationPlan();
            return this;
        }

    */

    @Override
    public VacationPlanBuilder title(String title) {
        this.title = title;
        return this;
    }

    @Override
    public VacationPlanBuilder days(int days) {
        this.days = days;
        return this;
    }

    @Override
    public VacationPlanBuilder startDate(LocalDate startDate) {
        this.startDate = startDate;
        return this;
    }

    @Override
    public VacationPlanBuilder doPlace(String doPlace) {
        this.doPlace = doPlace;
        return this;
    }

    @Override
    public VacationPlanBuilder addPlan(int day, String plan) {
        if (this.plans == null){
            this.plans = new ArrayList<>();
        }
        this.plans.add(new VacationDetailPlan(day, plan));
        return this;
    }

    @Override
    public VacationPlan getPlan() {
        return new VacationPlan(title, days, startDate, plans, doPlace);
    }
}

DefaultVacationBuilder에서 값을 세팅해주는 메소드들이 this를 return 하게 된다면 VacationPlanBuilder의 구현체인 DefaultVacationBuilder가 return된다. 그렇게 되면 VacationPlanBuild가 제공하는 다른 메소드를 다시 활용할 수 밖에 없다. 

Plan.java

import java.time.LocalDate;

public class Plan {

    public static void main(String[] args) {
        VacationPlanBuilder builder = new DefaultVacationBuilder();
        builder.title("전국 투어");
        VacationPlan plan = builder.days(3)
                .startDate(LocalDate.of(2021, 12, 12))
                .doPlace("daejeon station")
                .addPlan(0, "호텔 잡기")
                .addPlan(0, "성심당 빵먹기")
                .getPlan();

        VacationPlan shortPlan = builder.title("방 콕 투어")
                .startDate(LocalDate.of(2022, 04, 01))
                .getPlan();

    }

}

 

이렇게 builder가 없엇으면 일일이 설정된 생성자를 맞추기 위해 null을 억지로 넣어주는 경우가 생길 수도 있고, 만일 짧은 여행같은 경우에 자주 가게 된다면 diretor넣어서 재사용을 할 수 있는 여지도 열어둘 수 있다.

(Still has room for the improvement!)

 

Director 구현 - VacationDirector.java

import java.time.LocalDate;

public class VacationDirector {

    private VacationPlanBuilder vacationPlanBuilder;

    public VacationDirector(VacationPlanBuilder vacationPlanBuilder) {
        this.vacationPlanBuilder = vacationPlanBuilder;
    }

    public VacationPlan roomPlan(){
        return vacationPlanBuilder.title("방 콕 투어")
                .startDate(LocalDate.of(2022, 04, 01))
                .getPlan();
    }

    public VacationPlan countryPlan(){
        VacationPlan plan = vacationPlanBuilder.title("전국 투어").days(3)
                .startDate(LocalDate.of(2021, 12, 12))
                .doPlace("daejeon station")
                .addPlan(0, "호텔 잡기")
                .addPlan(0, "성심당 빵먹기")
                .getPlan();
    }

}

 

이렇게 되면 우리는 자주 애용하는 계획을 다시 사용할 수 있다.

 

Plan.java - modified

import java.time.LocalDate;

public class Plan {

    public static void main(String[] args) {
        VacationDirector director = new VacationDirector(new DefaultVacationBuilder());
        VacationPlan roomPlan = director.roomPlan();
        VacationPlan countryPlan = director.countryPlan();
    }

}

처음 우리가 생각한 구조와 작성한 코드와 비교를 해보자.

  • builder : product 객체의 일부 요소들을 생성하기 위한 추상 인터페이스를 정의 
  • ConcreteBuilder : Builder클래스에 정의된 인터페이스를 구현하며, 제품의 부품들을 모아 빌더를 복합한다. 
  • Director : builder인터페이스를 사용하는 객체를 합성
  • Product : 생성할 복합 객체를 표현한다. 

builder에 해당되는 class는 VacationPlanBuilder이다. 객체 요소를 생성하기 위한, 즉 우리의 방학 계획을 세우기 위한 추상 interface를 정의하고 있다.

 

ConcreteBuilder는 builder의 구현체이며 이것이 DefaultVacationBuilder이다. 

 

그리고 Builder를 의존하는 Directo는 VacationDirector이다.

 

우리가 사용하길 원하는 builder를 director를 통해 특정 기능을 수행하록 작성하면, client는 이 director를 의존하여 원하는 패턴을 method chaning을 통해 작성하면 된다.

 

이번 builder pattern은 무언가 깔끔하게 정리가 잘 되는 것 같다.

 

그만큼 난이도가 높다는 것은 아니겟지...?

장점

만들기 복잡한 객체를 순차적으로 만들 수 있는 방법을 제공할 수 있다. 

동일한 프로세스를 동해서 각기 다르게 구성된 객체를 다룰 수 있다.

단점

client에서는 director 혹은 director와 함께  builder까지 같이 만들어야 한다.

그리고, 구조도 복잡해지는 단점이 있다.

 


과거에 필자는 입사 초기 회사 업무에서 xml tag로 이루어진 파일을 작성해야 했다. 

 

그 코드를 다시 한번 들여다보자.

 

참고 : https://tech-monster.tistory.com/48

 

package com.osp.util;

import com.osp.util.doixmlvo.AbstactVO;
import com.osp.util.doixmlvo.BodyVO;
import com.osp.util.doixmlvo.ContributorsVO;
import com.osp.util.doixmlvo.DOIXml;
import com.osp.util.doixmlvo.DataLinkListVO;
import com.osp.util.doixmlvo.DataLinkVO;
import com.osp.util.doixmlvo.DataLink_TitleVO;
import com.osp.util.doixmlvo.DepositorVO;
import com.osp.util.doixmlvo.DoiDataVO;
import com.osp.util.doixmlvo.HeadVO;
import com.osp.util.doixmlvo.PersonNameVO;
import com.osp.util.doixmlvo.PublisherVO;
import com.osp.util.doixmlvo.ResearchDatasetVO;
import com.osp.util.doixmlvo.ResourceVO;
import com.osp.util.doixmlvo.SubjectVO;
import com.osp.util.doixmlvo.TitleVO;

import java.io.StringWriter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

public class MYUtil {

	public static String getDOIXml(String title, String explain, String publication_date, String doiData, String getURL, String managerCode) {
		DOIXml doixml = new DOIXml();
		BodyVO body = new BodyVO();
		HeadVO head = new HeadVO();
		String xmlContent = "";
		String doiUrl = getURL + managerCode;
		
		//xml - head 생성
		head.setDoi_batch_id("");
		head.setRegistrant("");
		head.setTimestamp("");
		head.setDepositor(new DepositorVO("",""));
		
		//xml - body 부분 생성
		ResearchDatasetVO researcherData = new ResearchDatasetVO();
		researcherData.setTitles(new TitleVO(title));
		researcherData.set_abstracts(new AbstactVO(explain));
		researcherData.setSubject(new SubjectVO("", ""));
		researcherData.setPublisher(new PublisherVO(""));
		researcherData.setKeywords("");
		researcherData.setResource(new ResourceVO("", "", ""));
		researcherData.setRights("");
		researcherData.setPublication_date(publication_date);
		researcherData.setDoi_data(new DoiDataVO(doiData, doiUrl));
		researcherData.setContributors(new ContributorsVO(new PersonNameVO("","","","","","","")));
		researcherData.setData_link_list(new DataLinkListVO(new DataLinkVO(new DataLink_TitleVO("", ""))));
		body.setResearch_dataset(researcherData);
		
		//xml에 생성된 head, body 넣기
		doixml.setVersion("1.0");
		doixml.setHead(head);
		doixml.setBody(body);

		try {
			// Create JAXB Context
			JAXBContext jaxbContext = JAXBContext.newInstance(DOIXml.class);
			
			// Create Marshaller
			Marshaller marshaller = jaxbContext.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_ENCODING, "utf-8");
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
			marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);

			StringWriter stringWriter = new StringWriter();
			stringWriter.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");

			// Write XML to StringWriter
			marshaller.marshal(doixml, stringWriter);
			
			// Verify XML Content
			xmlContent = stringWriter.toString();
		} catch (JAXBException e) {
			e.printStackTrace();
		}
		return xmlContent;
	}
	
}

어떻게 xml태그 파일을 생성하는지가 중요한 것이 아니라, 여기서는 과연 어떤 방식이 더 깔끔한가 이다. 

 

builder pattern을 적용하게 되면 tag가 현재 반환이 되는지 아닌지 헷갈일 일도 없고 

 

아무래도 코드의 양도 줄어들 것 같다. 

 

무엇보다도 director를 이용하게 되면 정해진 틀이 있으니 메소드 체인을 이용해서 사용하면 가독성이 좋아지리라 예상된다. 

 

여기서 문제는 정해진 틀에서 xml tag를 반복적으로 작성하는게 여기서 Builder Pattern을 적용했으면 어땟을까 하는 생각이 든다.

 

필자가 builder pattern을 다시 적용하면 xml의 header, body, 를 생성하는 builder를 따로 받아서 조합하는 방식을 생각할 것 같다.

 

 


이렇게 builder패턴을 알아보았다. 

 

생각보다 많이 들어본 패턴이기도 하고 lombok에서 우리가 @Builder 어노테이션을 사용하면 위의 구조처럼 메소드 체이닝처럼 객체를 생성할 수 있도록 기능을 제공한다.

 

이 기회에 builder 패턴의 의도를 다시 되새겨 보자. 

 

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있다.

 

모든 일련의 작업이 이 의도에 함축적으로 들어가 있는 것이다. 

 

서로 다른 표현이라도 이를 생성하는데 어느정도 동일한 절차를 사용하는것은 분명하다. 

 

조금 더 이해를 위해 필자는 다음 유튜브를 또 참고했다.

 

참고 : https://www.youtube.com/watch?v=M7Xi1yO_s8E 

 

BuilderPattern.js

class User{
    constructor(name) {
        this.name = name;
    }
}

class UserBuilder {
    constructor(name) {
        this.user = new User(name)
    }

    setAge(age){
        this.user.age = age
        return this
    }

    setPhone(phone){
        this.user.phone = phone
        return this
    }

    setAddress(address){
        this.user.address = address
        return this
    }

    build(){
        return this.user
    }

}

// 어떤 값을 넣어주던지 간에, 마지막에는 같은 객체를 반환해준다. 

let user = new UserBuilder('test1').build()
console.log(user)

let user1 = new UserBuilder('test2').setAge(10).build()
console.log(user1)

let user2 = new UserBuilder('test2').setAge(10).setAddress("seoul").build()
console.log(user2)

 

BuilderPattern2.js

class Address {
    constructor(zip, street) {
        this.zip = zip
        this.street = street
    }
}

class User {
    constructor(name, {age, phone, address} = {}) {
        this.name = name
        this.age = age
        this.phone = phone
        this.address = address
    }
}

let user = new User('Test222', {age : 10})
console.log(user);

 

결국 어떤 코드를 짜던지 간에 다 구현이 가능하다는 것이다. 

 

builder pattern에 대해 이해가 되길 바라며 여기서 글을 마친다.

 

잘못되거나 무언가 이상한 부분은 피드백을 주면 감사하겠습니다 꾸벅