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

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

by 우주를놀라게하자 2024. 11. 6.
반응형
SMALL
글에서 나온 코드는 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;
    }

}
반응형
LIST