Java/Spring & Spring Boot

JPA 엔티티간 연관 관계 매핑 (feat. Fetch전략, Cascade)

마손리 2023. 4. 26. 23:12

JPA 엔티티간 연관 관계 매핑

JPA에서 연관 관계 매핑은 단방향 연관 관계와 양방향 연관 관계가 있으며 방향성과 관계없이 일대다(1:N), 다대일(N:1), 다대다(N:N), 일대일(1:1)관계가 존재한다.

 

JPA 엔티티의 연관 관계를 매핑할때 보통 다대다(N:N)의 구조를 일대다(1:N)와 다대일(N:1)로 분리하고 분리된 관계들을 각각 단방향으로 다대일(N:1)로 연결해준 뒤 필요에따라 일대다(1:N)로 양방향 연결을 해준다.

 

엔티티간 관계도

위의 관계도를 보면 Order와 Coffee간 다대다 관계를 OrderCoffee로 일대다 다대일 관계로 나누었다.

또한 Order와 Member는 다대일 관계이며 Member와 Stamp는 일대일 관계이다.

 

 

다대일(N:1) 연관 관계

@Getter
@Setter
@Builder
@Entity(name = "ORDERS")
public class Order extends Auditable {
    ...
    
    @ManyToOne(fetch = FetchType.EAGER) // 1.
    @JoinColumn(name = "MEMBER_ID") // 2.
    private Member member; // 3.
    
    ...
}
  1. @ManyToOne : 에너테이션을 이용하여 다대일의 관계 선언 해준다. (fetch 전략은 밑에 설명)
  2. @JoinColumn : 연결할 '컬럼'의 이름을 지정해준다. 여기서 연결할 엔티티의 필드명이 아니라 DB에서의 컬럼명을 적어준다.
  3. 다대일 관계이후 필요에 의해 상대 엔티티에서도 양방향 관계를 맺게될때 필드명인 "member"가 필요하다.

위와 같이 단방향 연관 관계를 맺은 후, 상대 엔티티에서도 연결이 필요하다면 일대다(1:N) 연관 관계를 맺어 양방향으로 만들어 준다.

 

 

일대다(1:N) 연관 관계

@Getter
@Setter
@Builder
@Entity
public class Member extends Auditable {
    ...
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) // 1.
    private List<Order> orders = new ArrayList<>(); // 2.

    public void setOrder(Order order) { // 3.
        orders.add(order);
        if (order.getMember() != this) {
            order.setMember(this);
        }
    }
    ...
}
  1. @OneToMany : @JoinColumn으로 연결하는 다대일과는 달리 @OneToMany 에너테이션의 mappedBy 어트리뷰트로 관계를 형성한다. 이때 들어갈 키값은 다대일 관계에서 사용한 필드명으로 위의 다대일 예제에서의 'member'가 오게된다. 
  2. 테이블간의 관계에서는 일대다 관계를 사용하지 않지만 엔티티간의 관계에선 리스트를 이용하여 관계인 객체들을 넣어준다.
  3. 에너테이션 @Setter를 사용하게되면 매개변수로 리스트를 받아와야되므로 새로운 set메서드를 만들어 주어 이미 초기화된 리스트에 add()메서드로 상대 객체를 추가해서 연결하며 연결된 상대 엔티티에 자신의 엔티티도 넣어 서로 연결되도록 한다. 

 

 

일대일(1:1) 연관 관계

일대일 연관 관계도 다대일과 방법이 같다. 먼저 단방향 연관 관계로 맺은 후, 필요하다면 양방향으로 만들어 준다.

@Getter
@Setter
@Builder
@Entity
public class Stamp extends Auditable {
    ...
    @OneToOne(fetch = FetchType.EAGER) // 1.
    @JoinColumn(name = "MEMBER_ID") // 2.
    private Member member; // 3.

    public void setMember(Member member) { // 4.
        this.member = member;
        if (member.getStamp() != this) {
            member.setStamp(this);
        }
    }
    ...
}

다대일 관계에서 대부분 설명했기에 넘어가며 4번의 경우 자신의 엔티티 객체에 상대의 엔티티 객체를 참조를 한뒤 상대방의 엔티티 객체에도 자신의 엔티티객체를 참조 시킨다.

 

위에서의 다대일 관계에서는 일반적인 setter만 사용하였는데 다대일 구조에서 상대방의 객체에 자신의 객체를 참조 시키기 위해서는 해당 필드인 리스트에 반복문을 돌려 자신의 객체가 있는지 먼저 확인을 해줘야 하기 때문이며 그런 과정이 필수는 아니기 때문에 다대일 관계에서는 일반적인 setter를 사용해주었다.

 

단방향 관계이후 필요에 따라 양방향 관계로 만들어 준다.

@Builder
@Getter
@Setter
@Entity
public class Member extends Auditable {
    ...
    @OneToOne(mappedBy = "member", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)// 1.
    private Stamp stamp;
    
    public void setStamp(Stamp stamp) {
        this.stamp = stamp;
        if (stamp.getMember() != this) {
            stamp.setMember(this);
        }
    }
    ...
}

일대다 관계와 마찬가지로 mappedBy 어트리뷰트에 상대 엔티티와의 관계에 주체가되는 필드명인 'member'를 넣어준다.

 

(cascade 어트리뷰트는 아래에서 설명)

 

 

Fetch 전략

위와 같이 Entity간 연관 관계를 맺으면 객체 그래프 탐색을 통해 연결된 모든 객체에 접근할 수 있다. 문제는 이런 과정에서 불필요한 데이터들까지 처리하는데 많은 비용이 소모되며 이를 해결하기 위해 Fetch 전략을 이용한다.

  • Eager
    연관 관계로 매핑된 엔티티의 데이터까지 즉시 가져온다.
  • Lazy
    연관관계로 매핑된 엔티티를 가져오는 메서드를 호출하는 시점에 데이터를 요청한다. 

위와 같이 두가지 전략이 있으며 @ManyToOne과 @OneToOne 에너테이션의 경우 연관된 엔티티가 한개일 뿐이므로 Eager 전략이 디폴트이다.

@OneToMany와 @ManyToMany의 경우 리스트로 여러 엔티티가 연관되 있기 때문에 Lazy 전략이 디폴트이다.

 

 

영속성 전이(Cascade)

영속성 전이란 어떠한 엔티티 객체에서 생성, 수정, 삭제등 특정한 작업을 할때 이와 연결된 다른 엔티티 객체들도 똑같은 작업을 적용시키는 것이다.

 

영속성 전이 타입

  • CascadeType.PERSIST
    엔티티 생성시 연결된 다른 엔티티 또한 Persist Context에 저장한다. 이때 자바에서, 생성하려는 엔티티 객체와 연결할 엔티티 객체는 개발자가 '직접' 생성하여 연결해주어야하며 Cascade는 그저 Persist Context에 저장, 즉, EntityManager.persist() 메서드에만 반응하며 일단 Persist Context에만 해당 객체들을 저장시킨다. 이후EntityTransaction.flush() 혹은 .commit()으로 모든 데이터가 저장된다.
  • CascadeType.REMOVE
    삭제를 원하는 엔티티와 연결된 다른 엔티티들의 데이터를 DB와 Persist Context에서 모두 삭제한다.
  • CascadeType.SAVE_UPDATE
    생성한 Entity객체가 DB에 존재하지 않다면 생성, 존재한다면 수정을 진행시키는 작업을 연결된 다른 Entity와 함께 Persist Context에 저장한다. Hibernate ORM에서 save(), update() saveOrUpdate()메서드들을 호출할시 CascadeType.SAVE_UPDATE가 수행된다. 
  • 이외에도 다른 타입들이 존재한다.

 

Cascade 사용법

만약 단방향 관계일경우 연관 관계 에너테이션(ex. ManyToOne)에 cascade 어트리뷰트와 함께 CascadeType을 작성하면된다. 

양방향의 경우 mappedBy 어트리뷰트가 있는 연관 관계 에너테이션에 에너테이션에 cascade를 작성하거나 양쪽 모두의 연관 관계에 작성하면 된다.

 

 

(자료 출처 : https://www.baeldung.com/jpa-cascade-types )