본문 바로가기

Spring/JPA

좋은 Entity 설계하는 방법들

Entity 설계시 주의할 점


  1. Setter 사용 금지
    • Setter가 모두 열려있으면 변경 포인트가 많아져서 유지 보수가 힘들어짐, 어디서 해당 값이 변경되었는지 추적하기 힘듬
    • Setter가 필요할 경우에는 추적하기 쉽도록 메서드를 따로 생성해야함
  2. 모든 연관관계는 지연로딩(LAZY)로 설정
    • 즉시로딩은 예측이 어렵고, 어떤 SQL이 실행될 지 예측하기가 어려움 특히 JPQL 실행 시 N+1 문제가 자주 발생하게 됨
    • OneToOne, ManyToOne 관계는 기본이 즉시로딩이므로, 꼭 지연로딩으로 설정해야한다.
    • 연관관계에 필요한 entity를 한번에 가져오고 싶은 경우에는 FETCH JOIN을 사용하면 된다.
  3. 컬렉션은 필드에서 바로 초기화 해야지 NullPointerException으로 안전함
    • NPE 문제에서 안전함
    • 하이버네이트는 엔티티 영속화할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경함. 만약 getOrders()처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수도 있음
  4. 필드에 Enum 타입 사용 시 유의점무조건 String을 사용하는게 좋다. 왜냐면 ORDINAL을 사용해 중간에 새로운 type을 추가할 경우, 기존에 만들어놨던 0,1,2 순서와 꼬이게 된다면 난리가 난다.
  5. @Enumberated(EnumType.ORDINAL) vs @Enumberated(EnumType.STRING)

 

연관관계


연관관계 매핑

  • Entity 클래스 간의 관계를 대상 Entity의 참조를 가지는 형태로 만들어 줄 수 있고, 양 Entity가 서로의 참조를 가지는 경우 양방향 연관관계, 한 Entity만 다른 Entity의 참조를 가지는 경우는 단방향 연관관계라고 한다.

양방향 연관관계

일대다, 다대일

  • “한 팀에는 여러 회원이 존재한다”는 경우에 회원 Entity 입장은 팀에 대해 다대일 관계가, 팀 Entity 입장에서는 회원에 대해 일대다 양방향 연관관계를 만들 수 있다.
  • 일대다 관계에서는 ‘다’부분에 FK가 존재해야하고, 즉 회원이 팀의 FK를 가진다.
  • 양방향 연관관계에서는 연관관계의 주인을 정하는 것이 중요한데, 객체에는 양방향 연관관계라는 것은 사실 없고, 서로 다른 단방향 연관관계 2개를 잘 묶어서 보이게 하는것이다. 반면 DB는 FK 하나로 연관관계가 성립되므로 객체와 DB 둘 간에 차이점이 생긴다. 이 때문에 JPA에서는 연관관계 중 한 객체를 정해서 FK를 관리하게 해야하고, 이를 연관관계의 주인이라고 한다.
  • FK가 있는 곳을 연관관계의 주인으로 정해야한다.
  • 연관관계의 주인이 FK를 관리(등록, 수정, 삭제)할 수 있고 연관관계의 거울 에서는 mappedBy 속성을 통해 읽기만 가능하다.
public class Member {
    /*...*/
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Team team;
}

// 연관관계의 거울
public class Team {
    /*...*/
    @OneToMany(mappedBy = "member") // Member의 team 필드와 매핑
    private List<Member> members = new ArrayList<>();
}
// 실제 team.getMembers().add(member) 를 하고 persist() 해도 null이 저장된다.
// (연관관계의 주인인 Member.team 에 들어가지 않았기 때문)

일대일

1:1 관계는 둘 중 어디든 FK를 둘 수 있다. 더 많이 조회하는 Entity에 둘 것

다대다

실무에서는 @ManyToMany를 사용하지 않는다.

 

값 타입(임베디드 타입)


Address 같은 객체를 값을 표현하기 위한 타입으로 만들 수 있다. 하지만 값 타입은 절대 변경하면 안된다는걸 명심해야한다.

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() {}
    public Address(String city, String street, String zipcode) { /*...*/}
}

public class Member {
    /*...*/
    @Embedded
    private Address address;
}
  • Setter를 쓰지 않고 생성자를 통해서만 생겨야 함
  • default 생성자는 protected 로 만들어서 조금 더 안전하게 해야함
    • 하이버네이트로 객체를 생성할 때 리플렉션을 사용할 수 있어야 해서 private로 만들수는 없고, protected로 만들어야 한다.

 

Collection은 필드에서 초기화


Collection은 생성자나 setter아닌 필드에서 초기화해야한다.

public class Member {
	/*...*/
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}
  • 객체의 초기화에 대한 고민을 안해도 됨
  • Hibernate가 Entity를 Persist 하는 순간 collection을 감싸서 hibernate용 내장 컬렉션으로 변경하는데, Hibernate가 collection의 변경사항을 추적하기 위함인데 이걸 setter등을 이용해 초기화를 시켜버릴 경우 원하는 메커니즘으로 동작하지 않을 수 있다.

 

Cascade 설정


모든 엔티티는 저장하고 싶으면 각각 persist를 해줘야한다.

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
  • 위와 같이 써주면 collection에 있는 orderItem을 각각 persist할 필요 없이 persist(order)로 한방에 insert 또는 delete 할 수 있다.

 

연관관계 편의 메서드


양방향 연관관게에서 setting을 할 경우, 양쪽 다 해줘야 하는 불편함이 있는데, 이를 원자적으로 처리할 수 있게 편의 메서드를 만들어 둘 수 있다.

public class Order {
    /*...*/
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
        // member에 대한 order setting 도 원자적으로 일어나게 함
    }
}

 

Naming


예약어

  • 주문을 나타내는 Order의 경우 예약어를 피하기 위해 객체는 Order로, table은 Orders로 나타내는게 관례이다.
  • Table 네이밍
    • 소문자의 snake_case가 일반적이다
  • Entity의 id
    • 객체의 필드는 id라고 만들고 @Column(name = “member_id” 이런식으로 만드는게 좋다.

 


참고 링크

좋은 Entity 를 설계하기 위한 개념들

[Spring Boot JPA] 테이블 설계 및 Entity 개발 시 주의사항

JPA - Entity 설계시 주의점

'Spring > JPA' 카테고리의 다른 글

JPA Repository 메서드  (0) 2022.07.29
JPA 소개, 특징, 장점 및 단점  (0) 2022.07.19