글에서 나온 코드는 Github에서 확인 할 수 있습니다.
최근에 Spring을 시작하고 Entity를 설계하면서 아래와 같이 어노테이션을 활용하여 설계를 하였는데,
문득 궁금해지기도하고 올바른 설계가 맞는지에 의문이 들어서,
학습을 하던 도중 좋은 설계방향이 아니여서 글을 작성하게 되었다.
리팩토링 전 코드
@AllArgsConstructor // 객체 내부의 인스턴스 멤버들을 모두 가지고 있는 생성자를 생성, 불필요한 멤버들까지 매번 생성
@Builder // 모든 필드에 빌더 클래스 적용이 아니라 필수 필드만 빌더에서 설정하도록 하여 불변성을 보장
@Entity
@Table(name = "account")
@NoArgsConstructor(access = AccessLevel.PROTECTED)// 무분별한 객체 생성에 대해 한번 더 체크[기본 생성자의 접근 수준이 'public']
@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;
@Override
public String toString() {
return "AccountEntity{" +
"id=" + id +
", bankName='" + bankName + '\'' +
", accountNumber='" + accountNumber + '\'' +
", accountHolder='" + accountHolder + '\'' +
'}';
}
}
이것저것 찾아보니 좋은 설계 방법은 아닌것같아,,, 각 어노테이션을 어떻게 쓰는것이 조금더 안정적인지에 대해서 한번 기록해보려고 한다.
위 코드들에서 사용되어야할 방향으로 리팩토링부터 한번 해볼 예정이다. 코드는 아래와 같다
리팩토링 코드
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "account")
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;
}
}
왜 이렇게 수정을 하였을까?
1. @Builder 어노테이션을 클래스 레벨 -> 생성자 레밸로 수정
2. @NoArgsConstructor -> @NoArgsConstructor(access = AccessLevel.PROTECTED)
3. @AllArgsConstructor 제거
1. @Builder 어노테이션을 클래스 레벨 -> 생성자 레밸로 수정
- 클래스 레벨에서 @Builder를 사용하면 모든 필드에 대해 빌더 메서드가 생성, 경우에 따라 일부만 생성되어야할 필요가 있기 때문에 필수적으로 생성되어야할것들만 적용시킨다.
- 개별 필드에 대해 유효성 검사를 쉽게 수행할 수 없습니다. 모든 필드를 설정한 후 유효성을 체크해야 합니다.
2. @NoArgsConstructor -> @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 같은 패키지나 자식 클래스에서 사용할 수 있도록 하기 때문에 무분별한 생성자 사용을 억제할 수 있다.
- 접근 권한을 Private로 하면 프록시 객체 생성에 문제가 생기고, 접근 권한을 Public으로 하면 무분별한 객체 생성 및 Setter를 통한 값 주입을 할 수 있기에 접근 권한을 Protected로 작성 하는 것이다.
기본적으로 NoArgsConstructor는 public 접근 권한이기 때문에 다른 access 옵션을 주지 않으면 다른 패키지에서도 언제든지 객체를 생성할 수 있다.
아래 코드를 통해서 access 옵션의 유무 차이를 확인해보자!
아래와 같이 다른 패키지의 테스트안에서 코드를 실행시켰을때, PROTECTED의 옵션으로 인해 에러가 발생하게 된다.
public class ServiceTest {
@Test
void TestNoArgsConstructor() {
/*@NoArgsConstructor(access = AccessLevel.PROTECTED)
* 어노테이션을 사용했을 경우 다른 패키지에서 인스턴스를 선언하면,
* reason: AccountEntity() has protected access in AccountEntity
*
*/
AccountEntity accountEntity = new AccountEntity();
System.out.println("AccountEntity 객체: " + accountEntity.toString());
}
}
에러 발생!!
즉, 무분별한 생성자 사용을 억제할 수 있다.
3. @AllArgsConstructor 제거
@AllConstructor는 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성하는데, 인스턴스 멤버 변수의 순서를 바꾸면 입력 값 순서도 바뀌어 검출되지 않는 치명적인 오류를 발생시킬 수 있기 때문에 쓰지 않는 것이 좋다.
아래와 같이 변수를 초기에 세팅하였다고 가정하였을때,
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AccountEntity {
private Long id;
@NotEmpty
private String bankName;
@NotEmpty
private String accountNumber;
@NotEmpty
private String accountHolder;
}
@AllArgsConstructor는 모든 필드를 순서대로 생성자에 넣게 된다.
아래와 같이 생성을 하게 될것이다.
public AccountEntity(Long id, String bankName, String accountNumber, String accountHolder) {
this.id = id;
this.bankName = bankName;
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
}
이 경우 처음에 객체를 생성할때, id, bankName, accountNumber, accountHolder으로 생성하게 된다면 문제가 없겠지만!
public class AccountEntityTest {
@Test
AccountEntity accountEntity = new AccountEntity(
1L, "BANK_NAME", "ACCOUNT_NUMBER", "ACCOUNT_HOLDER"
);
}
Entity의 필드 순서를 아래와 바꿨다면???
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AccountEntity {
@NotEmpty
private String bankName;
@NotEmpty
private String accountNumber;
@NotEmpty
private String accountHolder;
private Long id;
}
@AllArgsConstructor에 의해 생성되는 생성자의 파라미터 순서도 바뀝니다.
public AccountEntity(String bankName, String accountNumber, String accountHolder, Long id) {
this.bankName = bankName;
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.id = id;
}
결국 기존에 작성해두었던 테스트코드는 잘못된 필드들이 들어갔다고 에러를 발생하게 된다,,,,
public class AccountEntityTest {
// bankName이 "1"로, 나머지 필드들이 잘못된 값들을 가지게 되어서 의도한 대로 동작하지 않음
@Test
AccountEntity accountEntity = new AccountEntity(
1L, "BANK_NAME", "ACCOUNT_NUMBER", "ACCOUNT_HOLDER"
);
}
최종적으로 다시 내가 생각했을때 가장 적절한 어노테이션 구성을 한번 더 강조하고 이번 글을 마무리하려고 한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED) // PROTECT 옵션 활용!
@Getter // Setter는 사용하지 않는다.
@Table(name = "account")
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 // Builder는 각 Entity마다 필수적인 필드만 사용하여 필요시 유효성도 추가할 수 있게한다.
public AccountEntity(String bankName, String accountNumber, String accountHolder) {
this.bankName = bankName;
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
}
}
'프로그래밍 > Spring' 카테고리의 다른 글
[Spring] Entity - 빌더 패턴 마스터하기: 유연하고 불변적인 코드 구조 구축 (0) | 2024.11.04 |
---|