본문으로 바로가기

[JPA] Entity 생성 방법

category Web/Spring, JPA 2023. 8. 29. 19:12

Setter

실무에서 엔티티를 만들 때 외부에서 값을 쉽게 변경할 수 없도록 @Setter를 사용하지 않는다. Setter는 그 의도가 분명하지 않으며 언제든지 변경할 수 있는 상태가 되기 때문에 안전성을 보장받기 어렵다. 또한, Setter를 사용하면 엔티티가 어디서, 왜 변경되었는지 추적이 어려워진다. 따라서 엔티티를 변경할 때는 변경 지점이 명확하도록 Setter 대신에 별도의 비즈니스 메서드(의미있는 메서드)를 제공해야 한다.

 

현재는 단순히 Setter를 이용하여 엔티티를 생성하고 있기 때문에 Setter를 이용하지 않고 엔티티를 생성하는 방법에 대해 알아보려고 한다. 어떤 방법이든 변경이 필요없는 필드에 추가적인 setter의 외부 노출를 줄이는 것이 중요하다!

 

Entity 생성 방법

  • 생성자
  • 정적 팩토리 메서드
  • Builder 패턴

엔티티를 생성할 때는 위의 3가지 방법 중에 사용하게 된다. 엔티티에 따라 방법들 중에 하나를 선택하고, 파라미터에 객체 생성에 필요한 데이터를 넘기는 방법을 사용한다. 정적 팩토리 메서드나, Builder 패턴을 사용할 때는 생성자를 private 처리한다. 객체 생성이 간단할 때는 단순히 생성자를 생성하고, 객체 생성이 복잡하고 의미를 가지는 것이 좋을 경우에 나머지 방법 중에 선택한다.

 

생성자

일반적으로 객체를 생성하는 방법

public Member(String memberId, String nickname) {
    this.memberId = memberId;
    this.nickname = nickname;
}

public Member(String memberId, String nickname, String password, String email) {
    this.memberId = memberId;
    this.nickname = nickname;
    this.password = password;
    this.email = email;
}
  • 매개변수가 많아질수록 힘들어지게 됨
  • 이때 점층적 생성자 패턴(Telescoping Constructor Pattern) 사용함
    • 선택 매개변수가 없는 생성자부터 선택 매개변수를 전부 받는 생성자까지 늘려나가는 방식
    • 이 경우 객체를 생성하고 변경하지 않는 장점이 있지만, 원치 않는 매개변수까지 값을 지정해야할 수도 있음
    • 또한 매개변수에 따라 생성자가 엄청나게 늘어나는 문제가 발생함

 

자바빈즈 패턴(JavaBeans Pattern)

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드로 원하는 매개변수 값을 설정하는 방식

Member member = new Member();
member.setMemberId(request.getMemberId());
member.setNickname(request.getNickname());
member.setPassword(request.getPassword());
member.setEmail(request.getEmail());
  • 점층적 생성자 패턴가 다르게 인자의 의미를 파악하기 쉽고, 여러 개의 생성자를 만들지 않아도 됨
  • 하지만 함수 호출 한 번으로 객체 생성을 끝내지 못하고, 여러번의 메서드를 호출해야 함
  • 또한, Setter는 사용하지 않는 것이 좋음 

 

정적 팩토리 메서드

클래스를 정의할 때 생성자가 아닌 다른 메서드로 객체 생성을 하는 방법

public static Member createMember(String memberId, String nickname, String password, String email) {
    Member member = new Member();
    member.setMemberId(memberId);
    member.setNickname(nickname);
    member.setPassword(password);
    member.setEmail(email);
    member.setAuthority(Authority.MEMBER);
    
    return member;
}

// setter 이용 x
public static Member createMember(String memberId, String nickname, String password, String email) {    
    return new Member(memberId, nickname, password, email, Authority.MEMBER);
}

장점

  • 정적 팩토리 메서드는 이름을 가진다.
    • 생성자의 경우, 자신의 클래스 명을 입력해야 함
    • 정적 팩토리 메소드의 경우, 자유롭게 메소드명을 작성할 수 있어 좀 더 직관적인 사용이 가능함
      • Member member = Member.createMember(...);
  • 정적 팩토리 메서드를 쓰면 호출할 때마다 새로운 객체(인스턴스)를 생성할 필요가 없다.
    • 객체를 재사용할 수 있음
    • 팩토리 메서드를 사용하면 매번 새로운 인스턴스를 반환할 수도 있지만, 미리 만들어둔 인스턴스를 반환할 수 있어 메모리 낭비를 줄일 수 있음
      • Booolean의 valueOf라는 정적 팩토리 메서드의 경우, 미리 만들어둔 TRUE와 FALSE 중 하나의 값(인스턴스)를 리턴함
  • 반환 타입의 하위 객체 타입을 반환할 수 있다.
    • 구현 클래스의 공개 없이 그 객체를 반환할 수 있으므로 API를 작게 유지할 수 있음
  • 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다
    • 생성자의 경우, 무조건 Member를 반드시 반환하게 됨
    • 정적 팩토리 메서드는 반환되는 객체의 클래스를 유연하게 결정할 수 있어 형변환이 자유로움
    • DTO ➡ Entity 또는 Parameter 값에 따른 객체 반환이 가능함

 

단점

  • 생성자 없이 정적 팩토리 메서드만 제공한다면 상속할 수 없다.
    • 상속은 public 혹은 protected 생성자가 필요하기 때문에 이러한 생성자 없이 정적 팩토리 메서드만 제공하면 상속할 수 없는 문제가 발생함
  • 개발자가 찾기 어렵다.
    • 일반적인 생성자는 javadoc를 통해서 쉽게 찾을 수 있지만 정적 팩토리 메서드는 쉽게 확인할 수 없음
    • 따라서 개발자는 코드를 보며 인스턴스화할 방법을 찾아야 함
      • 참고, 이러한 혼동을 막기 위해 정적 팩토리 메서드에는 일반적인 네이밍 규칙이 존재함

 

Builder 패턴

점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 합친 패턴

public class Member {
    // required parameters
    private String memberId;
    private String nickname;

    // optional parameters
    private String email;

    public static class MemberBuilder {
        private String memberId;
        private String nickname;
        private String email;

        // 필수 매개변수를 포함한 MemberBuilder 생성자
        public MemberBuilder(String memberId, String nickname){
            this.memberId = memberId;
            this.nickname = nickname;
        }

        public MemberBuilder email(int email) {
            this.email = email;
            return this;
        }

        public Member build(){
            return new Member(this);
        }
    }
    
    private Member(MemberBuilder builder) {
        this.memberId = builder.memberId;
        this.nickname = builder.nickname;
        this.email = builder.email;
    }

}
  • 구현 방법
    1. 빌더 클래스를 정적 중접 클래스(Static Nested Class)로 생성함
    2. 빌더 클래스의 생성자는 public으로 하며, 필수 값들에 대해 생성자의 파라미터로 받음
    3. 선택적인 값들에 대해서는 각각의 속성마다 메서드로 제공하며, 리턴값은 빌더 객체 자신이어야 함
    4. 빌더 클래스 내에 build() 메서드를 정의하여 객체를 리턴함
// 빌더 호출
Member member = new Member.MemberBuilder("testId", "testname")
        .email("testEmail")
        .build();
  • 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 리턴하는 방식
    • Builder의 Setter 메서드들은 빌더 자신을 반환하기에 연쇄적으로 호출이 가능함
  • 이처럼 빌더 패턴은 build()를 통해서만 객체 생성을 제공하기 때문에 생성 대상이 되는 클래스의 생성자는 private으로 정의해야 함
  • 또한, Member 클래스는 Setter 메서드 없이 Getter만 가지며, public 생성자가 없음

 

장점

  • 필요한 데이터만 설정할 수 있다.
  • 유연성을 확보할 수 있다.
  • 가독성을 높일 수 있다.
  • 변경 가능성을 최소화할 수 있다.

단점

  • 객체를 만들려면 빌더 클래스를 만들어야 한다.
  • 빌더 생성 비용이 크지는 않지만 민감한 상황에서는 문제가 될 수 있다. (빌더 객체를 계속 생성하기 때문)

 

Lombok의 @Builder 어노테이션 지원

 

1. 클래스 전체 Builder 적용

@Builder
public class Member {
    // required parameters
    private String memberId;
    private String nickname;

    // optional parameters
    @Builder.Default
    private String email = "";
}
  • 클래스 위치에 Builder를 적용하면 @AllArgsConstructor를 붙인 것 같은 효과를 봄
  •  @AllArgsConstructor: 모든 필드에 대한 매개변수를 받는 기본 생성자를 만듦
    • 인스턴스 멤버 변수의 순서를 바꾸면 입력 값 순서도 바뀌어 검출되지 않는 치명적인 오류가 발생할 수 있음
    • 따라서 사용하지 않는 것이 좋음

 

2. 특정 생성자에서만 Builder 적용

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    private String memberId;
    private String nickname;
    private String email;
    
    @Builder(builderMethodName = "createBuilder")
    public Member(String memberId, String nickname, String email) {
        this.memberId = memberId;
        this.nickname = nickname;
        this.email = email;
    }
}
Member member = Member.createBuilder()
    .memberId("testId")
    .nickname("testname")
    .email("testEmail")
    .build();
  • @NoArgsConstructor(access = AccessLevel.PROTECTED): 기본 생성자 접근 제한자를 protected로 바꿔줌
    • 무분별한 객체 생성에 대해 한 번더 체크할 수 있음
    • 외부에서 개발자는 직접 호출못하지만, JPA는 접근이 가능함
  • 위와 같이 기본 생성자를 제한해주고, 클래스가 아닌 생성자에 @Builder를 붙여주는 것이 좋음
    • 클래스 레벨에서 @Builder@NoArgsConstructor를 함께 컴파일하면 에러가 발생함
    • @AllArgsConstructor의 추가로 해결이 되지만, 위의 말한 문제로 사용하지 않는 것이 좋음
    • 따라서 생성자에 @Builder를 선언하면, 객체 생성 시 받지 않아야 할 매개변수들도 빌더에 노출되는 문제점을 해결할 수 있음
  • builderMethodName 속성: Builder 이름을 부여

 


참고

 

https://www.inflearn.com/questions/16235/%EC%83%9D%EC%84%B1-%EB%A9%94%EC%84%9C%EB%93%9C-setter-%EC%A7%88%EB%AC%B8

https://chaewsscode.tistory.com/178