Programming-[Backend]/JPA

[JPA기본] 10. 즉시/지연 로딩과 CASCADE

컴퓨터 탐험가 찰리 2021. 10. 17. 21:39
728x90
반응형

 

1. 즉시/지연 로딩 : FetchType-EAGER, LAZY

 

 

FetchType.LAZY

특정 엔티티를 불러올때, 연관된 엔티티를 무조건 가져온다면 EAGER, 미리 가져오지 않는다면 LAZY FetchType으로 지정할 수 있다. 아래와 같이 Member 객체에서 연관관계를 갖는 Team 필드의 @ManyToOne에 fetch 속성을 걸어주면 Team 프록시를 초기화하지 않는 이상, Team 테이블에는 쿼리를 보내지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Getter
@Setter
public class Member {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private String name;
 
  private String city;
 
  private String street;
 
  private Integer zipcode;
 
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
 
      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
 
      Member member1 = new Member();
      member1.setName("사용자");
      member1.setTeam(teamA);
 
      em.persist(member1);
 
      em.flush();
      em.clear();
 
      Member member = em.find(Member.class, member1.getId());
      System.out.println("member = " + member.getName());
 
      tx.commit();
cs

 

 

 

FetchType.EAGER

EAGER를 적용하면, member만 조회해도 연관 관계를 갖는 Team 객체를 자동으로 조회해온다.

 

 

연관관계 어노테이션별 기본 설정

@ManyToOne, @OneToOne은 기본이 EAGER 이고, @ManyToMany, OneToMany는 기본이 LAZY이다. 다음 섹션에서 배우겠지만 모든 연관관계에 LAZY를 적용하는 것이 권장되므로 @ManyToOne, @OneToOne에는 반드시 fetch 속성을 LAZY로 지정해주도록 한다.

 

 

 


 

2. N+1 문제

 

fetchType은 항상 LAZY를 기본으로 하자

이론적으로는 EAGER, LAZY를 설정하는 기준이, 위 예시처럼 member 객체를 불러올때 상당히 많은 경우가 반드시 Team 객체를 가져와서 사용하는 경우에는 EAGER를 사용하고, 그렇지 않은 경우는 LAZY를 사용하면 된다. EAGER로 불러오는 경우 2번의 쿼리를 요청해야되는데 1번으로 줄일 수 있어서 성능상의 이점을 얻을 수도 있다. 그러나,

 

반드시 fetch를 LAZY로 설정하여 사용해야한다.

 

이것은 N+1 문제를 야기하기 때문인데, 아래 예시를 통해 알아볼 수 있다. member1, member2와 각각에 해당하는 teamA, teamB가 있는 상황에서 EAGER를 적용했을 때 나가는 쿼리를 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
tx.begin();
 
    try {
 
      Team teamA = new Team();
      teamA.setName("teamA");
      em.persist(teamA);
 
      Team teamB = new Team();
      teamB.setName("teamB");
      em.persist(teamB);
 
      Member member1 = new Member();
      member1.setName("사용자");
      member1.setTeam(teamA);
      em.persist(member1);
 
      Member member2 = new Member();
      member2.setName("사용자2");
      member2.setTeam(teamB);
      em.persist(member2);
 
      em.flush();
      em.clear();
 
      //JPQL 쿼리문
      List<Member> results = em.createQuery("select m from Member m", Member.class)
              .getResultList();
      System.out.println("results = " + results);
 
      tx.commit();
cs

 

 

학습하고 있는 예시처럼 JPA를 이용하여 em.find를 사용하면 JPA가 알아서 한번에 데이터를 조회하는 쿼리를 내보내지만, 단순히 JPA를 적용하기가 어려워서(group by 등을 적용해야 하는 등 복잡한 쿼리가 나가야하는 경우) 위 코드와 같이 JPQL 문법을 사용하는 경우 SQL로 즉시 변환하여 member만 가져오고(1), 연관된 team 객체를 가져오기 위해서 member의 갯수(N)만큼 SQL을 실행하게 된다.

 

 

 

따라서 모든 부분에 LAZY 타입의 fetch 속성을 적용하고, 연관 관계를 갖는 객체를 한번에 불러오기 위해서 fetchJoin을 적용하는 방법 등이 있다. 이것은 나중에 JPQL의 문법과 함께 배우게 된다.

 

 

 


 

3. CASCADE

 

 

영속성 전이가 필요할 때

CASCADE는 하나의 엔티티에 의존하고 있는 다른 엔티티가 영속성 전이를 갖도록 하고 싶을 때 사용한다. 다만 위에서 배운 연관관계와는 전혀 상관이 없고, 부모-자식 엔티티의 관계가 있을 때 두 엔티티 모두가 동시에 영속화 되도록 해주는 기능이다.

 

다음과 같이 Parent 엔티티와 Child 엔티티가 존재한다고 가정하자. 양방향 연관관계로 1:N의 관계를 갖도록 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Getter
@Setter
public class Parent {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private String name;
 
  @OneToMany(mappedBy = "parent")
  private List<Child> childList = new ArrayList<>();
 
  public void addChild(Child child) {
    childList.add(child);
    child.setParent(this);
  }
 
}
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Getter
@Setter
public class Child {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private String name;
 
  @ManyToOne
  @JoinColumn(name = "parent_id")
  private Parent parent;
}
 
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tx.begin();
 
    try {
 
      Child child1 = new Child();
      Child child2 = new Child();
 
      Parent parent = new Parent();
      parent.addChild(child1);
      parent.addChild(child2);
 
      em.persist(parent);
      em.persist(child1);
      em.persist(child2);
 
      tx.commit();
cs

 

이 경우 실행 코드에서 em.persist를 반복적으로 실행해줘야한다. 이런 과정이 번거롭고, parent 중심으로 코드를 작성하고 있는 중이기 때문에 parent가 child를 자동으로 관리하도록 하기 위한 것이 CASCADE이다.

 

 

1
2
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  private List<Child> childList = new ArrayList<>();
cs

 

이렇게 하면 em.persist(parent)로 parent만 영속성 컨텍스트에 등록해주어도, child 엔티티도 영속성 컨텍스트에 등록이 된다.

 

 


 

CASCADE 사용시 주의점

앞서 언급했듯이, CASCADE는 연관관계를 매핑하는 것과 아무런 관계가 없다. 그리고 만약 위 예제와 같은 상황에서 Child 엔티티를 다른 엔티티에서 참조한다면 CASCADE를 사용하면 안된다! 다른 엔티티가 child 엔티티와 관계가 있는 상황에서 CASCADE를 적용하면 잘못된 쿼리가 나가는 오류가 발생할 수 있다.

 

 


 

4. orphanRemoval : 고아 객체 자동 처리

 

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하고 싶을 때 사용하는 옵션이다.

 

parent.getChildList().remove(0);

 

와 같이 parent의 childList 컬렉션에서 자식 엔티티를 제거했을 때, 실제 Child 엔티티의 0번째 요소를 테이블에서 삭제한다.

 

 

1
2
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<Child> childList = new ArrayList<>();
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tx.begin();
 
    try {
 
      Child child1 = new Child();
      Child child2 = new Child();
 
      Parent parent = new Parent();
      parent.addChild(child1);
      parent.addChild(child2);
 
      em.persist(parent);
 
      em.flush();
      em.clear();
 
      Parent parent1 = em.find(Parent.class, parent.getId());
      parent1.getChildList().remove(0);
 
      tx.commit();
cs

 

 

물론 orphanRemoval 기능은 삭제하는 기능이므로 상당히 조심해서 사용해야한다. parent 자체를 삭제하면 child가 모두 지워지게 될뿐 아니라, child와 관련된 다른 엔티티가 있는 경우 에러가 발생할 수 있다.

 

 

다만, CASCADE와 orphanRemoval 옵션을 적절히 사용하면, 부모 엔티티 중심으로 편하게 영속성 관리를 할 수 있다. 개인적인 의견으로는 그래도... 이런 옵션들 때문에 다른 개발자들이 혼선을 겪을 수 있고 모호한 지점이 생길 수 있는데다 리스크가 생기기 때문에 굳이 사용을 안하는 것이 좋을 것 같다.


 

참조

 

1. 인프런_자바 ORM 표준 JPA 프로그래밍 - 기본편_김영한 님 강의

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21735?tab=curriculum

728x90
반응형