본문 바로가기
프로그래밍/Spring

[Spring] Entity - 빌더 패턴 마스터하기: 유연하고 불변적인 코드 구조 구축

by 우주를놀라게하자 2024. 11. 4.
반응형
SMALL

왜 Builder 패턴을 사용해야하는가?


 

전체 코드는 Github에서 볼 수 있습니다.

 

Builder 패턴을 사용하면 다음과 같은 장점이 있습니다.

  1. 인자가 많을 경우 쉽고 안전하게 객체를 생성할 수 있습니다.
  2. 인자의 순서와 상관없이 객체를 생성할 수 있습니다.
  3. 적절한 책임을 이름에 부여하여 가독성을 높일 수 있습니다

Entity 설계시 어떤 어노테이션을 활용해야하고 왜 활용해야하는지 모르겠다면 아래 글을 참고하면 좋을듯하다.!!

 

 

[Spring] Entity를 설계할때 주의할점!! 어노테이션 뭘 써야하고 왜 쓰는거야???

글에서 나온 코드는 Github에서 확인 할 수 있습니다. 최근에 Spring을 시작하고 Entity를 설계하면서 아래와 같이 어노테이션을 활용하여 설계를 하였는데,문득 궁금해지기도하고 올바른 설계가 맞

dentuniverse.tistory.com

 

 


 

1. 불안전한 객체 생성 패턴


 

 

최근 Python에서 Java로 넘어오면서 Builder를 작성할때 아래와 같이 작성을 하곤 했다. 우선 데이터베이스의 칼럼이 not null인 경우에는 대부분의 엔티티의 멤버실의 값도 null이면 안된다. 그 뜻은 해당 객체를 생성할 경우에도 동일합니다.  

@Entity
@Table(name = "account")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class AccountEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @NotEmpty
    @Column(name = "bank_name", nullable = false)
    private String bankName;

    @NotEmpty
    @Column(name = "account_number", nullable = false)
    private String accountNumber;

    @NotEmpty
    @Column(name = "account_holder", nullable = false)
    private String accountHolder;

    // 불안전한 객채 생성 패턴
    @Builder
    public AccountEntity(String bankName, String accountNumber, String accountHolder) {
        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }
    
    @Override
    public String toString() {
        return "AccountEntity{" +
                "id=" + id +
                ", bankName='" + bankName + '\'' +
                ", accountNumber='" + accountNumber + '\'' +
                ", accountHolder='" + accountHolder + '\'' +
                '}';
    }
}

 

위 코드를 테스트 코드로 작성했을때를 확인해보자.


Test Code


    @Test
    @DisplayName(value = "불안전한 객채 생성 패턴")
    void createUnstableAccountEntity() {
        AccountEntity accountEntity = AccountEntity.builder()
                .accountHolder("")
                .accountNumber(ACCOUNTNUMBER)
                .bankName(BANKNAME)
                .build();
        assertThat(accountEntity.getBankName()).isEqualTo(BANKNAME);
        assertThat(accountEntity.getAccountNumber()).isEqualTo(ACCOUNTNUMBER);
        System.out.println("AccountEntity 객체 : " + accountEntity.toString());
    }

 

이렇게 작성했을때, 빌더 내부에 유효성 검사를 강제하지 않으면, Entity에선 @NotEmpty라고 선언한 필드의 값들에 Null값도 들어가서 에러의 발생이 뒷단으로 넘어갈수도 있게된다.

불안전한 객체 생성 결과값

때문에 @Builder 자체에서 유효성 검사를 넣는것이 더 좋다고 생각이 된다.

 


2. 안전한 객체 생성 패턴


 

아래 코드는 해당 @Builder에 유효성을 주입하여, Null 체크를 하는 검사를 진행한다.

@Entity
@Table(name = "account")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class AccountEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @NotEmpty
    @Column(name = "bank_name", nullable = false)
    private String bankName;

    @NotEmpty
    @Column(name = "account_number", nullable = false)
    private String accountNumber;

    @NotEmpty
    @Column(name = "account_holder", nullable = false)
    private String accountHolder;

    // 안전한 객채 생성 패턴
    @Builder
    public AccountEntity(String bankName, String accountNumber, String accountHolder) {
        Assert.hasText(bankName, "bankName 값이 누락되었습니다.");
        Assert.hasText(accountNumber, "accountNumber 값이 누락되었습니다.");
        Assert.hasText(accountHolder, "accountHolder 값이 누락되었습니다.");

        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }
    
    @Override
    public String toString() {
        return "AccountEntity{" +
                "id=" + id +
                ", bankName='" + bankName + '\'' +
                ", accountNumber='" + accountNumber + '\'' +
                ", accountHolder='" + accountHolder + '\'' +
                '}';
    }
}

 

이렇게 작성했을때, 우리가 의도하는 Null값에 대한 유효성을 주입하여, @NotEmpty로 선언한 필드들에 대해서 안정성도 보장된다.


Test Code


    @Test
    @DisplayName(value = "안정적인 객채 생성 패턴")
    void createstableAccountEntity() {
        AccountEntity accountEntity = AccountEntity.builder()
                .accountHolder("")
                .accountNumber(ACCOUNTNUMBER)
                .bankName(BANKNAME)
                .build();
        assertThat(accountEntity.getBankName()).isEqualTo(BANKNAME);
        assertThat(accountEntity.getAccountNumber()).isEqualTo(ACCOUNTNUMBER);
        System.out.println("AccountEntity 객체 : " + accountEntity.toString());
    }

 

여기서 조금 더 나아가서 지금까지 Builder를 사용할땐, 아래와 같이 사용했는데,,,,,,알아보니 좀 더 직관적으로 사용할 수 있는 방법이 있었다.

 

.builder().build();의 방식으로 사용하는것이 아니라 builder()에 이름을 부여할 수 있다는 사실,,,!!

AccountEntity accountEntity = AccountEntity.builder()
                .accountHolder("")
                .accountNumber(ACCOUNTNUMBER)
                .bankName(BANKNAME)
                .build();

 

방법은 아래와 같다


3. 안정적인 객체 생성 + 빌더 이름 부여


 

    @Builder(builderClassName = "CreditAccountBuilder", builderMethodName = "creditAccountBuilder")
    public AccountEntity(String bankName, String accountNumber, String accountHolder) {
        Assert.hasText(bankName, "bankName 값이 누락되었습니다.");
        Assert.hasText(accountNumber, "accountNumber 값이 누락되었습니다.");
        Assert.hasText(accountHolder, "accountHolder 값이 누락되었습니다.");

        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }

 

여기저기서 글을 읽을때 builderClassName, builderMethodName을 한쌍으로 반드시 선언해줘야만 작동을 하는줄 알았는데, builderMethodName만 선언해줘도 작동을 하였다,,,

 

이유는 이러하였다.

 

 @Builder 어노테이션을 사용할 때 builderClassName을 생략하고 builderMethodName만 지정하면, Lombok은 기본적으로 클래스를 위한 빌더 클래스를 생성하고, 지정된 메서드 이름을 사용하여 빌더 인스턴스를 반환한다. 

 

Lombok은 기본 빌더 클래스를 <클래스 이름>Builder 형식으로 생성하고, creditAccountBuilder라는 정적 메서드를 통해 빌더 인스턴스를 반환하였다.

 

한번 더 수정한 안정적인 객체 생성 + 빌더 이름 부여 코드는 아래와 같다.

 

    @Builder(builderMethodName = "creditAccountBuilder")
    public AccountEntity(String bankName, String accountNumber, String accountHolder) {
        Assert.hasText(bankName, "bankName 값이 누락되었습니다.");
        Assert.hasText(accountNumber, "accountNumber 값이 누락되었습니다.");
        Assert.hasText(accountHolder, "accountHolder 값이 누락되었습니다.");

        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }

 

이제 마지막으로 테스트 코드를 작성해보고 이번 글을 마무리하려고 한다.


Test Code


중점적으로 볼 코드는, AccountEntity.builder() -> AccountEntity.creditAccoutBuilder()로 변경되었다는 부분이다.

    @Test
    @DisplayName(value = "안정적인 객채 생성 빌더 이름 명명")
    void createAccountEntityBuilderName() {
        AccountEntity accountEntity = AccountEntity.creditAccountBuilder()
                .accountHolder("")
                .accountNumber(ACCOUNTNUMBER)
                .bankName(BANKNAME)
                .build();
        assertThat(accountEntity.getBankName()).isEqualTo(BANKNAME);
        assertThat(accountEntity.getAccountNumber()).isEqualTo(ACCOUNTNUMBER);
        System.out.println("AccountEntity 객체 : " + accountEntity.toString());
    }

 

 

테스트 결과는 과연??? 두구두구!!

 

 

생략해도 유효성검사가 잘 되는것을 확인하였다.


 

오늘의 회고


 

기존에 Python+Django, FastAPI를 주로 사용하다가 Java + Spring으로 넘어오려니,,,,,이것저것 공부할것들이 많아져 재미도 있지만 다시 바빠져 앞으로 계속 글을 작성하고 굉장히 초보적인 글들도 많이 적게 될거같다,,,,🥲🥲🥲🥲🥲

 

 

반응형
LIST