[JPA] Entity 객체 생성 시 주의사항

LDB·2025년 1월 14일
0

JPA 기본

목록 보기
10/10
post-thumbnail

작성계기

JPA를 사용했다면 무조건 Entity 객체를 만들어봤을 것이다. 그런데 Entity객체를 생성하는데 주의사항이 존재했다. 나 또한 예전에는 내가 사용한 방식이 문제점인지 모르고 사용했었는데 지금은 하나하나 주의하면서 생성하고 있다. 그런 의미에서 Entity객체를 생성하는데 있어서 어떤점을 주의해야하는지 복습하고자 작성해본다.

주의사항들

시작하기전에 주의사항들을 나열하자면 이렇게 존재한다.

  • Entity 객체에서 가급적 Setter를 사용하면 안된다.
  • 연관관계 매핑시 가급적 지연로딩으로 세팅해야한다.
  • 컬렉션을 사용한다면 필드에서 초기화 해야한다.
  • 조회 후 반환시 DTO클래스 형태로 반환하는 것을 권장한다.
  • 기본 생성자의 접근 제어 설정
  • AllArgsConstructor 어노테이션은 쓰지 않기
  • ToString 어노테이션을 쓸때 연관관계 매핑된 엔티티 필드는 제거

Entity 객체에서 가급적 Setter를 사용하면 안된다.

Setter는 객체의 필드 값을 변경할 때 사용하는 기능인데 Setter를 쓰면 객체를 언제든지 변경이 가능하기에 객체의 불변성이 보장받기 힘들어진다. 추가로 @Setter라고 lombok에서 지원하는 어노테이션을 사용하면 변경 가능성이 어디서 누구에 의해 발생했는지 추적이 힘들어진다.

그렇기에 값 변경이 필요하면 의미있는 메서드를 생성하여 사용하는 것이 좋다.

연관관계 매핑시 가급적 지연로딩 으로 세팅

Entity객체에서 연관관계를 세팅할 수 있는데 기본적으로 @ManyToOne / @OneToMany의 기본속성은 EAGER 그러니까 즉시로딩으로 세팅되어있다. 즉시로딩으로 세팅하면 N+1문제가 자주 발생하기 때문에 즉시로딩이 아닌 지연로딩(LAZY)으로 세팅해야한다.

N+1문제
일종의 연관 관계에서 발생하는 이슈로 연관관계가 설정된 엔티티를 조회하면 조회된 데이터의 개수(n)만큼 연관관계의 조회 쿼리가 추가로 실행되어 데이터를 읽는다. 필자는 이 문제를 개인적으로 N+1이 아닌 1+N으로 표현하고 싶다.


컬렉션을 사용한다면 필드에서 초기화

컬렉션을 사용한다면 필드에서 초기화하는 것이 안전한데 다음과 같은 이유가 있다.
1. NullPointerException 문제에서 안전하다.
2. 하이버네이트는 Entity를 영속화할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만약 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다, 그러니 필드 레벨에서 생성하는 것이 가장 안전하고 코드가 간결해진다.

조회 후 반환시 DTO클래스 형태로 반환하는 것을 권장

기술적으로는 Entity객체를 그대로 반환해도 문제가 없다. 하지만 Entity객체를 그대로 반환하면 추후 Entity에 변경사항이 생기면 API의 스펙이 변하는데 여러곳에서 문제가 발생 할 수 있다, 그리고 무엇보다 DB Table의 구조가 노출 될 위험이 생긴다.

기본 생성자의 접근 제어 설정

기본 생성자 즉 @NoArgsConstructor를 사용하고 접근제어 설정을 PROCTECTED로 설정하면 의미 없는 객체 생성을 막아준다. 즉 무분별한 Entity객체 생성에 대해 한번 더 체크가 가능해진다.

이떄 의미있는 객체를 생성한다면 @Builder를 사용하면 된다.

@Builder를 사용하는 방법은 2가지가 있다.
1. 클래스에 @Builder 어노테이션 붙이기
2. 생성자에 @Builder 어노테이션 붙이기

AllArgsConstructor 어노테이션은 쓰지 않기

AllArgsConstructor 어노테이션은 클래스의 모든 필드를 인자로 받는 생성자를 생성한다. 위에서 설명한 "클래스에 @Builder 어노테이션 붙이기" 방법을 사용하면 @Builder 어노테이션에서도 생성자가 생기는데 결과적으로

두 가지의 생성자 떄문에 컴파일러의 혼란을 야기 할 수 있다.

그래서 클래스에 @Builder를 사용하기보다는 생성자를 생성하고 @Builder를 사용하는 것이 바람직한 방법이다.


ToString 어노테이션을 쓸때 연관관계 매핑된 엔티티 필드는 제거

결론부터 이야기하면 연관 관계 필드에 toString을 사용하면 무한루프 문제가 발생할 수 있다. 이 것은 Entity뿐만 아니라 ToString만 사용한 경우에도 발생하는데 어째서 무한루프가 생기는지 과정을 보면

Main Class

public class Main {
    public static void main(String args[]) {
    	Room room = new Room();
        room.name = "normalRoom";

        Member member = new Member();
        member.name = "투숙객1";
        member.age = 25;
        member.roomName = room;

        room.addMember(member);
        System.out.println(member);
    }
}

Room Class

@ToString
public class Room{
	public List<Member> members = new ArrayList<>();
    publuc String name;

    public void addMember(Member member){
    	members.add(member);
    }
}

Member Class

@ToString
public class Member{
	public Room roomName;
    public int age;
    public String name;
}

위의 코드는 문제가 있는데 순서로 나타내면 이렇다.

  1. room에 member를 추가하고 member 출력을 시도한다.
  2. 출력하기 위해 Member로 이동한다.
  3. Member에 Room이 있기에 참조하기 위해 Room으로 이동한다.
  4. Room에서는 List에 있는 Member를 문자열로 출력하기 위해 Member로 이동한다.

다음과 같이 3,4번을 무한 루프가 발생하고 StackOverFlowError가 발생한다.

다행히도 @ToString은 클래스의 필드에서 대상을 제외할 수 있는기능이 있다.

@ToString
public class Member{
	@ToString.Exclude // 이렇게 사용하면 된다.
    public Room roomName;
    public int age;
    public String name;
}

최종정리

그렇다면 위의 규칙을 전부지켜서 Entity를 생성한다면 다음과 같은 형태가 나온다.

@Entity

// Entity 객체에서 가급적 Setter를 사용하면 안된다.
@Getter

// ToString 어노테이션을 쓸때 연관관계 매핑된 엔티티 필드는 제거
@ToString(exclude = {"member"}) 
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자의 접근 제어 설정
public class Room {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "seq")
    @Comment(value = "일련번호")
    private int seq;

    @Column(name = "roomname")
    @Comment(value = "방이름")
    private String roomname;
	
  	@Column(name = "price")
    @Comment(value = "가격")
    private int price;
    
    // 연관관계 매핑시 가급적 지연로딩으로 세팅해야한다.
    @OneToMany(fetch = FetchType.LAZY)
    @JoinColum(name = "member_id")
    @Comment(value = "멤버변호")
    private List<Member> member = new ArrayList<>(); 
    // 컬렉션을 사용한다면 필드에서 초기화 해야한다.
	
    // 기본 생성자의 접근 제어 설정
    @Builder
    public Concert(String roomname, int price, Member member){
        this.roomname = roomname;
        this.price = price;
        this.member = member;
    }
    
}

참고 사이트

https://velog.io/@haron/JPA-엔티티-설계시-주의점

https://velog.io/@mooh2jj/올바른-엔티티-Builder-사용법

https://velog.io/@buffet/TOString-과-TOString.Exclude에-대해서

https://codingnojam.tistory.com/85

https://chaewsscode.tistory.com/178

https://jddng.tistory.com/321

(항상 감사합니다.)

profile
가끔은 정신줄 놓고 멍 때리는 것도 필요하다.

0개의 댓글

관련 채용 정보