JPA 올바른 Entity, @Builder 사용법(@Builder.Default)

devdo·2022년 4월 21일
21

JPA

목록 보기
3/13
post-thumbnail

@Entity 란?

@Entity가 붙은 클래스는 JPA가 관리하는 객체

주의
• 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
• final 클래스, enum, interface, inner 클래스 사용X
• 저장할 필드에 final 사용 X


매핑 어노테이션 정리

@Column 어노테이션 설명

속성설명기본값
nameMapping할 Column의 이름을 지정.객체 Field 이름
insertableEntity 저장 시 해당 Field도 저장, false로 읽기 전용 설정 가능.true
updatableEntity 수정 시 해당 Field도 수정,false로 읽기 전용 설정 가능.true
table하나의 Entity설정에서 두 개이상 Table에 매핑할 때 사용현재 Class가 매핑된 Table
nullable(DDL)true/false로 null 허용 여부 설정true
unique(DDL)true/false로 Unique 제약 조건 설정
length(DDL)Column 속성 길이 설정255
columnDefinition(DDL)DB Column 정보를 직접 설정Java Type과 설정 DB 방언으로,적절한 Column Type 생성
precision, scale(DDL)BigDecimal, BigInteger Type에서 사용, precision은 소수점 포함 전체 자릿수, scale은 소수 자릿수, double, float Type에는 적용되지 않음. 아주 큰 숫자나 정밀한 소수를 다룰때 사용.precision=19, scale=2

여기서 name, insertable, updatable, table을 제외한 나머지 속성들은 DDL 생성 기능을 사용할 때만 사용되는 속성들로,

JPA 실행 로직에는 영향을 끼치지 않는 속성들이다.

직접 DDL을 설정하여 DB Table을 구성할 경우 사용할 이유가 없다.
Entity만으로 개발자가 DB Table 구조 파악이 가능하다는 장점

위 속성 중 nullable의 경우 Java의 기본 타입(int, long, ...)은 null 값 입력이 불가능 하므로,

false를 통해 DB Column에 Not Null 제약 조건을 지정해 두는것이 안전하다.
혹은 직접 DB Column에 Not Null 제약 조건 추가

정리

  • @Column 컬럼 매핑
  • @Temporal - 날짜 타입 매핑(이제 거의 안쓰임!) LocalDate, LocalDateTime을 사용할 때는 생략 가능하기에!(최신 하이버네이트 지원)
  • @Enumerated - enum 타입 매핑
  • @Lob - BLOB, CLOB 매핑
  • @Transient - 특정 필드를 컬럼에 매핑하지 않음(매핑 무시), 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용

실전

Entity 클래스를 작성할 때 어노테이션을 많이 씁니다.

아래와 같이 많이 쓰게 되는데요.

@Getter
@Setter // 문제 1. 객체가 무분별하게 변경될 가능성 있음
@NoArgsConstructor // 문제 2. 기본 생성자의 접근 제어자가 불명확함
@Builder
@AllArgsConstructor // 문제3. 객체 내부의 인스턴스멤버들을 모두 가지고 있는 생성자를 생성 (매우 위험)
@Entity
public class Member

이 어노테이션에서 문제 3가지가 보입니다. 이를 개선하기 위해 해결방법을 정리해봅니다.

해결 1. @Setter를 사용하지 않기

Setter는 그 의도가 분명하지 않고 객체를 언제든지 변경할 수 있는 상태가 되어서 객체의 안전성이 보장받기 힘듭니다. 특히 엔티티에서는 @Setter를 사용 시 해당 변경 가능성이 어디서 누구에 의해 발생했는지 추적하기가 힘들어진다.

때문에 값 변경이 필요한 경우 의미 있는 메서드를 생성하여 이를 사용하는 것이 좋습니다.

해결법 : 의미있는 메서드로 생성하자.


해결 2.@NoArgsConstructor(access = AccessLevel.PROTECTED)로 변경

기본 생성자(NoArgsConstructor)의 접근 제어를 PROCTECTED 로 설정하면 아무런 값도 갖지 않는 의미 없는 객체의 생성을 막게 됩니다. 즉 무분별한 객체 생성에 대해 한번 더 체크할 수 있습니다.

//@NoArgsConstructor(access = AccessLevel.PROTECTED) 

Member member = new Member(); //컴파일 에러 발생

이때, '의미있는 객체' 생성을 위해서 @Builder을 사용할 수 있습니다.

@Builder를 사용하는 방법은 총 2가지인데,
1) 클래스에 @Builder를 붙이기
2) 생성자에 @Builder를 붙이기

이는 다음 @AllArgsConstructor을 쓰지 않아야 하는 것과 관련이 있습니다.


해결 3. @AllArgsConstructor는 쓰지 않기

만약 해결 2의 방법1(클래스에 @Builder를 붙이기)을 사용, 클래스 레벨에서 @Builder와 @NoArgsConstructor를 함께 쓰면 오류가 발생합니다.

이를 해결하기 위해서는 모든 필드를 가지는 생성자를 만들어주어야 하는데 @AllArgsConstructor도 같이 써주게 됩니다.

하지만 @AllArgsConstructor 는 위험합니다.

왜 충돌하는가?

왜 충돌이 발생할 수 있는가?
@AllArgsConstructor는 모든 필드를 인자로 받는 생성자를 생성합니다.

@Builder는 빌더 패턴을 적용하는데, 빌더 클래스와 함께 각 필드를 초기화하는 생성자를 생성합니다.

이 두 어노테이션이 함께 사용되면, 두 가지 생성자가 생성되고 이로 인해 컴파일러가 혼란스러워할 수 있습니다. 그리고,

클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성하는데, 인스턴스 멤버의 선언 순서에 영향을 받기 때문에 변수의 순서를 바꾸면 생성자의 입력 값 순서도 바뀌게 되어 검출되지 않는 치명적인 오류를 발생시킬 수 있습니다.

그래서
해결 2의 방법2(생성자에 @Builder를 붙이기)를 사용해서

@AllArgsConstructor를 쓰는 일이 없도록 합니다.


해결 4. @ToString 쓸때 연관관계 매핑된 엔티티 필드는 제거하기

연관 관계 필드는 toString()에서 사용하지 않는 것이 좋습니다.

연관 관계 필드를 toString()에서 사용하면 무한루프 문제가 발생할 수 있습니다. 이는 연관된 엔티티 객체가 다시 현재 엔티티를 참조하게 되어 무한 루프가 발생하기 때문입니다. 이 문제를 해결하기 위해서는 연관 관계 필드를 제외하거나, 연관 엔티티 객체를 식별 가능한 값으로 대체하는 것이 좋습니다.

@ToString(exclude = "...") 어노테이션을 사용하여 제외할 필드를 명시적으로 지정하는 것이 좋습니다.


리팩토링 결과

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString(exclude = {"team"})
public class Member {

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private String picture;
    
    @ManyToOne(fetch = FetchType.Lazy)
    @JoinColum(name = "team_id")
    private Team team;

    @Enumerated(EnumType.STRING)	// Enum타입 객체를 쓸때 활용! 반드시 String타입으로 바꿔주고 DB 필드에 널어주어야 한다! 
    private Role role;

    public Member update(String name, String picture) {
        this.name = name;
        this.picture = picture;
        return this;
    }

// 생성자에 @Builder 적용
    @Builder
    public Member(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }
}

Builder default 사용법

초기화시, 특정 필드를 초기화하고 싶다면 사용하는 @Builder 속성 어노테이션 내 .Default를 붙여줍니다!

@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pojo {

    @Builder.Default
    private String name = "dsg";
    private String nickname;
    @Builder.Default
    private List<PojoTwo> pojoTwos = new ArrayList<PojoTwo>();

}
Pojo(name=dsg, nickname=프랜드, pojoTwos=[])
Pojo(name=dsg, nickname=null, pojoTwos=[])

참고

https://dev-jhl.tistory.com/entry/Lombok-%EC%98%AC%EB%B0%94%EB%A5%B8-Lombok-%EC%82%AC%EC%9A%A9%EB%B2%95-Builder

profile
배운 것을 기록합니다.

4개의 댓글

comment-user-thumbnail
2023년 7월 10일

궁금했던게 싹 해결되었어요 감사합니다 잘보고갑니다

1개의 답글
comment-user-thumbnail
2024년 7월 22일

감사합니다 도움이 되었어요

1개의 답글