Builder Pattern

2023. 10. 9. 16:13Design Pattern

디자인 패턴 첫번째 포스팅이다. 내가 Builder Pattern을 처음으로 작성한 이유는 회사에서 적용한 첫 패턴이기 때문이다. 


[문제점]
회사에서 개발을 하다가 문득 문제점을 발견하였다. 그것은 불변객체로 만드려고 하다보니 생성자에 파라미터가 너무 많아서 순서대로 넣어야하는데 다행히 intellij는 각 필드의 값이 어떤 것을 의미하는지 보여주지만..이클립스를 커스텀한 B** 프레임워크는..그런것을 지원하지 않았다.. 그래서 테스트를 하는데 자꾸 이상한 값이 잘못 들어간 버그가 발견되었다.

예를 들면 아래와 같은 Account class가 있을때 생성자로 불변객체로 만들었다. 

package org.example.example;

public class Account {
    private final String accountNumber; //계좌번호
    private final String accountName; //예금주명
    private final String bankCode; //은행코드
    private final String accountKindCode; //계좌종류코드
    private final String depositCode; //입금종류코드

    public Account(String accountNumber, String accountName, String bankCode, String accountKindCode, String depositCode) {
        this.accountNumber = accountNumber;
        this.accountName = accountName;
        this.bankCode = bankCode;
        this.accountKindCode = accountKindCode;
        this.depositCode = depositCode;
    }

    @Override
    public String toString() {
        return "accountNumber= " + accountNumber + " accountName=" + accountName +
                " bankCode=" + bankCode + " accountKindCode=" + accountKindCode
                + " depositCode=" + depositCode;
    }
}

 

package org.example;

import org.example.example.Account;

class Main {

    public static void main(String[] args) {
        Account account = new Account("1234567890", "테스트사용자", "04", "01", "01");

        System.out.println(account);

    }
}

위에 생성자를 통해서 객체를 생성하였을 때 데이터를 파라미터로 넣었다. 이럴 경우 어떤 데이터를 넣었는지 한눈에 보기도 힘들고, 
순서를 변경해서 데이터 넣어도 같은 String type이기 때문에 오류가 나지 않고 잘못된 데이터가 초기화될 가능성이 높다. 

[해결 과정]
이런 문제를 해결하기 위해서 Builder Pattern을 도입하기로 하였다.  그 이유는 아래와 같다. 

1. 너무 많은 파라미터를 가독성있게 순서와 상관없이 정확하게 데이터를 초기화하기 위해서
2. 내가 넣고 싶은 필드의 정보들만 골라서 넣을 수 있기 때문에 

그렇기 때문에 Builder Pattern으로 객체를 생성하기로 하였다. 

Builder Pattern으로 두가지 방법으로 객체를 생성할 수 있다. 

1. Builder class를 만들어서 객체를 만들기 
2. Lombok에 있는 @Builder 어노테이션으로 객체 생성하기 

1. Builder class를 만들어서 객체를 만들기 
먼저 1번부터 보자면 아래처럼 Builder 클래스를 새로 만들어준다. 보통 클래스이름 + Builder 이름으로 생성해준다. 

package org.example.example;

public class AccountBuilder {

    private String accountNumber; //계좌번호
    private String accountName; //예금주명
    private String bankCode; //은행코드
    private String accountKindCode; //계좌종류코드
    private String depositCode; //입금종류코드

    public Account build() {
        return new Account(accountNumber, accountName, bankCode, accountKindCode, depositCode);
    }

    public AccountBuilder accountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
        return this;
    }

    public AccountBuilder accountName(String accountName) {
        this.accountName = accountName;
        return this;
    }

    public AccountBuilder bankCode(String bankCode) {
        this.bankCode = bankCode;
        return this;
    }

    public AccountBuilder accountKindCode(String accountKindCode) {
        this.accountKindCode = accountKindCode;
        return this;
    }

    public AccountBuilder depositCode(String depositCode) {
        this.depositCode = depositCode;
        return this;
    }
}

이렇게 만들어주면 build() method를 통해서 Account 객체를 생성할 수 있다. 사실 Account Class에서 static inner Class로 Builder를 정의해줘도 되지만, 현재 유지보수 하고 있는 소스에서 그 안에 넣어서 생성자를 private으로 하는것보다는 새로 Builder 객체를 생성해주는 것이 기존 소스에 영향을 가지 않고 더 안전하다고 판단하였다. 

이제 Main method에서 작성해보자 

package org.example;

import org.example.example.Account;
import org.example.example.AccountBuilder;

class Main {

    public static void main(String[] args) {
        Account account = new Account("1234567890", "테스트사용자",
                "04", "01", "01");

        System.out.println(account);

        Account account2 = new AccountBuilder()
                .accountNumber("1234567890")
                .accountName("테스트사용자")
                .bankCode("04")
                .accountKindCode("01")
                .depositCode("01")
                .build();

        System.out.println(account2);



    }
}


위에 보면 account와 account2가 있는데 account는 생성자를 통해서 객체를 생성하는 방법이고, account2는 builder객체를 통해서 Account객체를 생성하는 방법이다.

2. Lombok에 있는 @Builder 어노테이션으로 객체 생성하기 

내가 현재 있는 곳은 인터넷이 되어있지 않은 상태라 Lombok 라이브러리를 사용하지 못한다. 하지만 라이브러리 안써도 충분히 코드를 작성할 수 있다. 그래도 인터넷 되는 환경이라면 Lombok이 다른 기능들도 있기 때문에 사용하는 것도 좋을 거 같다. 

사용하기 위해서 gradle 프로젝트로 만들었기 때문에 lombok 라이브러리를 추가해줘야한다. 

https://projectlombok.org/setup/gradle

 

Gradle

 

projectlombok.org

위 사이트에 들어가보면 롬복 그래들로 어떻게 추가해야하는지 나와있다. 

Lombok 사이트

plugins {
    id 'java'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    //Lombok 설정
    compileOnly 'org.projectlombok:lombok:1.18.30'
    annotationProcessor 'org.projectlombok:lombok:1.18.30'

    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

 

이런식으로 gradle 설정에 Lombok을 추가해주면 된다. 

@Builder 어노테이션을 가보면 클래스와 생성자에 붙일 수 있다. 간단한 설명으로 차이점을 말한다면 클래스에 붙이면 모든 필드에 대한 빌더를 생성하는 것이고, 특정 생성자에 붙인다면 특정생성자의 파라미터만 필더로 생성 할 수 있다.
더 자세한 것을 알고 싶다면 아래 블로그 글을 참고하도록 하자! 

@Target({TYPE, METHOD, CONSTRUCTOR})
@Retention(SOURCE)
public @interface Builder {
...생략
}


https://velog.io/@park2348190/Lombok-Builder%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

package org.example;

import lombok.Builder;

@Builder
public class Account {

    private final String accountNumber; //계좌번호
    private final String accountName; //예금주명
    private final String bankCode; //은행코드
    private final String accountKindCode; //계좌종류코드
    private final String depositCode; //입금종류코드


    public Account(String accountNumber, String accountName, String bankCode, String accountKindCode, String depositCode) {
        this.accountNumber = accountNumber;
        this.accountName = accountName;
        this.bankCode = bankCode;
        this.accountKindCode = accountKindCode;
        this.depositCode = depositCode;
    }

    @Override
    public String toString() {
        return "accountNumber= " + accountNumber + " accountName=" + accountName +
                " bankCode=" + bankCode + " accountKindCode=" + accountKindCode
                + " depositCode=" + depositCode;
    }

}

Lombok의 빌더를 사용하는것은 간단하다. Account 위에 @Builder라는 어노테이션을 붙였다. 

package org.example;

public class Main {
    public static void main(String[] args) {
        Account account = new Account("1234567890", "테스트사용자",
                "04", "01", "01");

        System.out.println(account);

        Account account2 = new Account.AccountBuilder()
                .accountNumber("1234567890")
                .accountName("테스트사용자")
                .bankCode("04")
                .accountKindCode("01")
                .depositCode("01")
                .build();

        System.out.println(account2);



    }
}

Main method를 가면 account2에다가 Account.AccountBuilder() 클래스를 불러와서 초기화를 시켰다. 

이것으로 추측해본건데 @Builder Lombok이 아마도 InnerClass로 빌더 클래스를 만들어주는것 같다. 그래서 인텔리 제이에서 .class파일을 보았다. 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.example;

public class Account {
    private final String accountNumber;
    private final String accountName;
    private final String bankCode;
    private final String accountKindCode;
    private final String depositCode;

    public Account(String accountNumber, String accountName, String bankCode, String accountKindCode, String depositCode) {
        this.accountNumber = accountNumber;
        this.accountName = accountName;
        this.bankCode = bankCode;
        this.accountKindCode = accountKindCode;
        this.depositCode = depositCode;
    }

    public String toString() {
        return "accountNumber= " + this.accountNumber + " accountName=" + this.accountName + " bankCode=" + this.bankCode + " accountKindCode=" + this.accountKindCode + " depositCode=" + this.depositCode;
    }

    public static AccountBuilder builder() {
        return new AccountBuilder();
    }

    public static class AccountBuilder {
        private String accountNumber;
        private String accountName;
        private String bankCode;
        private String accountKindCode;
        private String depositCode;

        AccountBuilder() {
        }

        public AccountBuilder accountNumber(String accountNumber) {
            this.accountNumber = accountNumber;
            return this;
        }

        public AccountBuilder accountName(String accountName) {
            this.accountName = accountName;
            return this;
        }

        public AccountBuilder bankCode(String bankCode) {
            this.bankCode = bankCode;
            return this;
        }

        public AccountBuilder accountKindCode(String accountKindCode) {
            this.accountKindCode = accountKindCode;
            return this;
        }

        public AccountBuilder depositCode(String depositCode) {
            this.depositCode = depositCode;
            return this;
        }

        public Account build() {
            return new Account(this.accountNumber, this.accountName, this.bankCode, this.accountKindCode, this.depositCode);
        }

        public String toString() {
            return "Account.AccountBuilder(accountNumber=" + this.accountNumber + ", accountName=" + this.accountName + ", bankCode=" + this.bankCode + ", accountKindCode=" + this.accountKindCode + ", depositCode=" + this.depositCode + ")";
        }
    }
}

 봤는데 정말 AccountBuilder class를 생성하였다. 이렇게 많은 코드가 필요한데 어노테이션 한번만 붙이면 해결되니 Lombok이 정말 편리한 라이브러리인거 같다. 

[builder pattern으로 객체를 생성하면 좋은 점] 

1. 파라미터가 많아도 파라미터에 맞는 데이터를 가독성있게 볼수가 있어서 파라미터 순서에 신경을 쓸 필요가 없다. 
2. 객체 필드에 어떤 데이터가 들어가있는지 한눈에 알수 있다. 

[단점]

1. 생성자로 만들 때는 필수값으로 파라미터를 넣지 않았을 때 컴파일 에러가 나지만, builder 클래스로 생성을 한다면 에러가 나지 않고, 깜빡잊고 필수 파라미터를 까먹고 안넣을 가능성이 있다. 

[결론]
깜빡 잊고 안넣었다면 테스트 하면서 보완하면 될거 같고 무엇보다 생성자를 통해 초기 값을 많이 넣는 것이 내 입장에서는 신경쓸게 너무 많았다. 그래서 현재 빌더 객체로 잘 생성하면서 운영반영까지 끝냈고 현재 잘 돌아가고 있다!