2017년 1월 31일 화요일

Builder Pattern에 대해서

자바 개발자라면 대부분 아는 Builder Pattern을 굳이 설명 할 필요는 없을 것 같고 단지 개발을 하면서 이 패턴이 매우 유용하게 쓰이는 부분들이 있어서 끄적여 본다.

위키피디아에 있는 예제를 보면 영문과 국문이 상이하다.
국문 위키피디아의 Builder Pattern java 예제는 PizzaBuilder를 상속받은 HawaiianPizza와 SpicyPizza를 Cook을 이용해서 Pizza를 만들어내는(??) 방법으로 빌더 패턴을 설명하고 있다.
(개발자라서 글재주가 없으니..;; 그냥 소스를 보도록 하자..ㅠㅠ)

영어 위키피디아의 Builder Pattern java 예제는 StreeMap이란 객체를 생성할 때 inner Class인 Builder를 이용하는 방법을 설명하고 있다.
일단.. 내가 이야기 할 부분은 후자인 영어 위키피디아에서 빌더패턴으로 객체를 만들어내는 부분이다.
이 패턴은 Value Object를 생성할 때 매우 유용하다.

과거 빌링 플랫폼을 개발하면서 정말 힘들었던 점은, 입력받는 파라미터의 숫자가 너무 많은데다가 변수 이름도 굉장히 어려웠다.
(payment와 settlement는 다른 의미고.. 현금성, 비현금성 금액을 영어로 하기도 어렵고..)
그리고 법은 자꾸 바뀌어서 이러랬다 저러랬다 해서 소스도 매우 더러운 편이었고,
돈을 다루는 곳이다보니 10원가지고 엄청난 CS도 들어오기도 했다.
이중에 제일 힘든 부분 12년이 넘게 운영되다보니 테이블의 컬럼도 무지 많은데 도대체 어떤게 쓰이고 안쓰이는지 잘 모르는 상태에다가 비즈니스 로직마다 입력을 해야 하는 값들이 아주 조금씩 달라서 배포하고 나면 장애가 종종 나기도 했다. 더군다나 빌링은 워낙 오랜 새월 여러 개발자들은 손을 거치다보니 왜 개발이 이렇게 되었는지 히스토리를 알수도 없었다.

해서 이런 문제들을 해결 하기 위한 하나의 방법으로 Value Object를 생성할 때 몇가지 룰을 적용하였고 이로 인해서 전보다는 가독성있는 소스가 만들어졌다.
PL을 할 때 팀원들과 정했던 룰인데 요기에 정리해본다.


StreetMap map = new StreetMap.Builder(new Point(50, 50), new Point(100, 100))
       .landColor(Color.GRAY)
       .waterColor(Color.BLUE.brighter())
       .build();

위의 위키피디아 예제를 가지고 설명하도록 하도록 하겠다.


public Builder(Point origin, Point destination) { 
        this.origin      = origin; 
        this.destination = destination; 
}

1. Builder 생성자에는 필수값 파라미터만 사용한다. 따라서 null을 허용하지 않는다.
이렇게 정한 가장 큰 이유는 Builder로 선언될 때 무조건 input value가 있어야 하기 때문에 객체를 생성 할 때 실수로 파라미터 누락이 생기는 걸 방지하기 위함이다. 만약 setter를 쓰게 되면 origin을 누락시키거나 destination을 누락시킬 수 있기 때문에 이를 객체 생성시 부터 막도록 한다.


public Builder landColor(Color color) {
         landColor = color; return this; 
}

2. Builder를 리턴하는 메소드는 각각의 비즈니스에 사용되는 파라미터임을 알 수 있도록 명명한다.
우리가 사용한 방법은 public Builder는 기본적으로 결제 시 사용하는 파라미터들을 담게 하고 정기결제 같은 경우 옵션형태이기 때문에 메소드로 빼서 객체를 생성 할 때 구분하도록 했다.


//일반 결제
public Builder (Payment payment, User user) {...} 

//정기결제 
public Builder subscription(int term, long item, String message) {...}

3. 객체에 Setter 형태로 내부 변수값을 변경 할 수 있는 것은 가능한 만들지 않는다.
객체가 한번 선언되면 내부 변수 값이 외부에 의해서 변경되지 않도록 하는 것은 결제에서 매우 중요하다.사실 변경될 여지도 거의 없을 뿐더러 변경이 가능하다면 분명한 이유가 존재해야 한다. Setter 자체를 생성하는 건 요즘 IDE가 너무 좋아서 그냥 짠 하고 만들어지지만 제공해선 안되는 변수까지도 개발자의 실수로 생성할 수 있다 . (최소한 아무나 만들 수 있고 실수로 만들 수 도 있으니..)이는 로직이 복잡하거나 빌링처럼 오만가지 파라미터나 난무하는 경우에 객체에 어떤 것이 접근 가능하고 불가능한지를 알려면 소스를 다 보거나 그 비즈니스를 모두 이해해야 한다.
해서 우리는 가능한 모든 변수는 private final로 선언하는 것을 원칙으로 삼고 상태를 나타내는 값이거나 변경되어도 객체가 갖는 비즈니스에 전혀 영향을 주지 않는 값이라고 생각되는 변수만 setter를 허용하였다.


private final Payment payment;
private String message;
...

public void setMemo(String message){
    this.message = message
}


이런 빌더패턴을 가지고 VO 객체를 생성하는 방법은 빌링 처럼 오래된 시스템에 적용하기 참 좋다. 가독성을 높히는 부분도 있으며 개발자의 실수를 줄일 수 있고 시스템을 크게 변화시키기 않는다. 그리고 복잡한 비즈니스 로직 일부를 가시성있게 만들어 줄 수도 있다.
즉 정답이라고 볼 수는 없지만 현실적으로 좋은 방법이라고 이야기 하고 싶다.

기본 결제에 대한 예제 Value Object 예제


package io.daumkakao.fennec.vo;

import java.util.Date;

/**
 * Created by elijah17
 * 직접 결제
 */
public class DirectPayment{

    private final User user;
    private final Item item;
    private final Price price;
    private final PaymentType paymentType;

    private final Subscription subscription;
    private final int period;
    private final Date startDate;

    private final FreeType freeType;
    private final int freePeriod;

    private final ReceiptType receiptType;
    private final Receipt receipt;

    private String memo;



    public static class Builder {

        private final User user;
        private final Item item;
        private final Price price;
        private final PaymentType paymentType;

        private Subscription subscription;
        private int period;
        private Date startDate;
        private FreeType freeType;
        private int freePeriod;

        private ReceiptType receiptType;
        private Receipt receipt;
        private String memo;



        public Builder(PaymentType paymentType, User user, Item item, Price price){
            this.paymentType = paymentType;
            this.user = user;
            this.item = item;
            this.price = price;
        }

        /**
         * 자동 결제
         * @param subscription 정기결제
         * @param period 기간
         * @param startDate 정기결제 시작일
         * @return
         */
        public Builder autoPay(Subscription subscription, int period, Date startDate){
            this.subscription = subscription;
            this.period = period;
            this.startDate = startDate;
            return this;
        }

        /**
         * 무료 사용기간
         * @param freeType 무료 타입
         * @param freePeriod 무료 기간
         * @return
         */
        public Builder freePolicy(FreeType freeType, int freePeriod){
            this.freeType = freeType;
            this.freePeriod = freePeriod;
            return this;
        }

        /**
         * 영수증 증빙 처리
         * @param receipt 영숭증 정보
         * @return
         */
        public Builder receipt(ReceiptType receiptType, Receipt receipt){
            this.receiptType = receiptType;
            this.receipt = receipt;
            return this;
        }

        public Builder memo(String memo){
            this.memo = memo;
            return this;
        }

        public DirectPayment build(){
            return new DirectPayment(this);
        }
    }

    private DirectPayment(Builder builder){
        user = builder.user;
        item = builder.item;
        price = builder.price;
        paymentType = builder.paymentType;
        subscription = builder.subscription;
        period = builder.period;
        startDate = builder.startDate;
        freeType = builder.freeType;
        freePeriod = builder.freePeriod;
        receiptType = builder.receiptType;
        receipt = builder.receipt;
        memo = builder.memo;
    }

    public User getUser() {
        return user;
    }

    public Item getItem() {
        return item;
    }

    public Price getPrice() {
        return price;
    }

    public PaymentType getPaymentType() {
        return paymentType;
    }

    public Subscription getSubscription() {
        return subscription;
    }

    public int getPeriod() {
        return period;
    }

    public Date getStartDate() {
        return startDate;
    }

    public FreeType getFreeType() {
        return freeType;
    }

    public int getFreePeriod() {
        return freePeriod;
    }

    public ReceiptType getReceiptType() {
        return receiptType;
    }

    public Receipt getReceipt() {
        return receipt;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }
}


Value Object를 가지고 객체 생성하는 예제

// 직접결제
DirectPayment directPayment = new DirectPayment.Builder(PaymentType.CREDIT, user, item, price).build();

//정기 결제 인 직접 결제
DirectPayment directPayment = new DirectPayment.Builder(PaymentType.CREDIT, user, item, price).autoPay(subscription, 12, new Date()).build();


댓글 없음:

댓글 쓰기