왜 Builder 패턴을 사용해야하는가?
전체 코드는 Github에서 볼 수 있습니다.
Builder 패턴을 사용하면 다음과 같은 장점이 있습니다.
- 인자가 많을 경우 쉽고 안전하게 객체를 생성할 수 있습니다.
- 인자의 순서와 상관없이 객체를 생성할 수 있습니다.
- 적절한 책임을 이름에 부여하여 가독성을 높일 수 있습니다
Entity 설계시 어떤 어노테이션을 활용해야하고 왜 활용해야하는지 모르겠다면 아래 글을 참고하면 좋을듯하다.!!
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으로 넘어오려니,,,,,이것저것 공부할것들이 많아져 재미도 있지만 다시 바빠져 앞으로 계속 글을 작성하고 굉장히 초보적인 글들도 많이 적게 될거같다,,,,🥲🥲🥲🥲🥲
'프로그래밍 > Spring' 카테고리의 다른 글
[Spring] Entity를 설계할때 주의할점!! 어노테이션 뭘 써야하고 왜 쓰는거야??? (7) | 2024.11.06 |
---|