1. 지연로딩

JPA에서 지연 로딩(Lazy Loading)은 연관된 엔티티나 컬렉션을 즉시 로딩하지 않는 전략을 의미한다. 대신, 실제로 해당 데이터에 접근이 필요할 때 (예: 프로퍼티의 값을 가져오는 경우) 데이터베이스에서 해당 데이터를 로드한다.

 

연관관계에 FetchType.LAZY  속성을 추가

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
import lombok.extern.java.Log;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Member extends BaseEntity {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
@Column(name = "username")  
private String username;  
@ManyToOne(fetch = FetchType.LAZY)  
@JoinColumn(name = "team_id")  
private Team team;  
}

테스트 코드로 Team의 클래스명과, 쿼리를 확인해 보면

Team team = new Team();  
team.setName("teamA");  
em.persist(team);  
  
Member member1 = new Member();  
member1.setUsername("member1");  
member1.setTeam(team);  
em.persist(member1);  
  
em.flush();  
em.clear();  
  
Member member = em.find(Member.class, member1.getId());  
  
System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());

System.out.println("============================================================");  
member.getTeam().getName();  
System.out.println("============================================================");

Member 엔티티는 Team을 Join 하지 않고 조회했다가,

실제 Team의 데이터를 조회할 때 조회 쿼리가 나가는것을 알 수 있다.

클래스도 확인해보면 실제 객체가 아닌 프록시 객체를 반환한다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.member_id=?
member.getTeam().getClass() = class com.example.jpa.domain.Team$HibernateProxy$k43PADH3
============================================================
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
============================================================

비즈니스 로직 상 대부분 메인 엔티티만 사용하고 참조 엔티티는 많이 쓰지 않을때 쓰는게 좋다. 라고 하지만

실무에서는 대부분  FetchType.LAZY 만 쓰인다고 한다.

 

정리

  • 모든 연관관계에 지연 로딩을 사용하라
  • 실무에서 즉시 로딩을 사용하지 마라
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용하라
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

 

2. 즉시로딩

지연로딩은 조회 시 가짜 프록시 객체를 만들어 조회하지만

즉시 로딩은 모든것에 Join을 걸고 조회한다

 

이전 엔티티 코드에서 fetchType.LAZY -> fetchType.EAGER 로 변경해 준 후 동일한 테스트코드를 실행하면

지연로딩과는 다르게 최초 조회시 모든 데이터를 불러오고,

구분선(===...)사이에는 아무것도 조회하지 않는것을 알 수 있다.

데이터를 모두 가지고 오기 때문에 프록시 객체가 아닌 실제 객체를 반환한다.

Hibernate: 
    select
        m1_0.member_id,
        m1_0.created_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?
member.getTeam().getClass() = class com.example.jpa.domain.Team
============================================================
============================================================

엔티티와 참조 엔티티가 대부분 같이 사용된다고 한다면 즉시 로딩을 사용한다고 한다.

 

주의점

데이터베이스 입장에서 한두개 정도는  조인이라고 해서 크게 느리진 않다. 
하지만 만약에 수십개라면 완전 다른 차원의 이야기다.

다 즉시로딩으로 10개씩 되어있다고 생각해보자 
뭐 하나 할때마다 전부 조인되서 나간다.

그래서 실무에서 테이블이 복잡하게 얽혀있을 때에는 전부 지연 로딩 으로 설정해야 한다.

 

예를들어 JPQL로 테스트 코드를 작성하면

Team team = new Team();  
team.setName("teamA");  
em.persist(team);  
  
Member member1 = new Member();  
member1.setUsername("member1");  
member1.setTeam(team);  
em.persist(member1);  
  
em.flush();  
em.clear();  
  
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

 

처음에는 엔티티 객체에 대한 쿼리가 나오고, 그 후에 참조 객체에 대해서 한번 더 조회 쿼리가 나온다.
그렇기에, 조회하려는 엔티티 객체가 10개 라면, 엔티티 쿼리 1개 + 참조 엔티티 쿼리 10개 의 쿼리가 출력된다. 

em.find같은 것들은 JPA가 내부적으로 최적화를 해 주지만 JPQL은 쿼리 그대로 나가기 때문이다.

 

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?

Team과 Member를 하나씩 더 추가해보자

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.setUsername("member1");  
member1.setTeam(teamA);  
em.persist(member1);  
  
Member member2 = new Member();  
member2.setUsername("member2");  
member2.setTeam(teamB);  
em.persist(member2);  
  
em.flush();  
em.clear();  
  
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

쿼리를 보면 엔티티에 대한 쿼리 1개와 참조 엔티티에 대한 2개의 쿼리가 나가는것을 볼 수 있다.

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
Hibernate: 
    select
        t1_0.team_id,
        t1_0.created_by,
        t1_0.created_date,
        t1_0.last_modified_by,
        t1_0.last_modified_date,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?

하지만 지연로딩으로 설정한다면 아래와 같이 하나의 쿼리로 끝이 난다.

 /* select
        m 
    from
        Member m */ select
            m1_0.member_id,
            m1_0.created_by,
            m1_0.created_date,
            m1_0.last_modified_by,
            m1_0.last_modified_date,
            m1_0.team_id,
            m1_0.username 
        from
            member m1_0

 

정리

  • 가급적 지연 로딩만 사용(실무에서)
  • 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany 는 기본이 지연로딩

3. CASCADE(영속성 전이)

CASCADE특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 기능이다.

 

Parent 와 Child라는 엔티티가 있을 때 연관관계에 cascade 속성을 추가한다.

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Parent {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)  
private List<Child> childList = new ArrayList<>();  
  
public void addChild(Child child){  
childList.add(child);  
child.setParent(this);  
}  
}



package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
@Entity  
@Getter  
@Setter  
public class Child {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
  
@ManyToOne(fetch = FetchType.LAZY)  
@JoinColumn(name = "parent_id")  
private Parent parent;  
}

Child 엔티티를 영속시키지 않고 Parent 엔티티만 영속시킨다.

 
Child child1 = new Child();  
child1.setName("child1");  
Child child2 = new Child();  
child2.setName("child2");  
  
Parent parent = new Parent();  
parent.addChild(child1);  
parent.addChild(child2);  
  
em.persist(parent);

실제 쿼리를 보면 Parent 엔티티만 영속시켰지만 Child에 대한 쓰기 쿼리가 출력된다.

Hibernate: 
    /* insert for
        com.example.jpa.domain.Parent */insert 
    into
        parent (name,member_id) 
    values
        (?,?)
Hibernate: 
    /* insert for
        com.example.jpa.domain.Child */insert 
    into
        child (name,parent_id,member_id) 
    values
        (?,?,?)
Hibernate: 
    /* insert for
        com.example.jpa.domain.Child */insert 
    into
        child (name,parent_id,member_id) 
    values
        (?,?,?)

이처럼 부모 엔티티를 저장할때 부모 엔티티에 저장되어있는 자식 (정확하게는 부모 엔티티의 컬렉션 안에 있는)엔티티도 전부 저장하는 기능이다.

주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

 

4. 고아객체

고아 객체부모 엔티티와 연관관계가 끊어진 자식 엔티티를 의미하는데 

연관관계 속성 중 orphanRemoval라는 옵션을 true로 하면, 고아 객체들을 자동 삭제해준다.

package com.example.jpa.domain;  
  
import jakarta.persistence.*;  
import lombok.Getter;  
import lombok.Setter;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
@Getter  
@Setter  
public class Parent {  
@Id  
@GeneratedValue  
@Column(name = "member_id")  
private Long id;  
private String name;  
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL, orphanRemoval = true)  
private List<Child> childList = new ArrayList<>();  
  
public void addChild(Child child){  
childList.add(child);  
child.setParent(this);  
}  
}

childList에서 첫번째 child를 제거하는 테스트코드를 작성

Child child1 = new Child();  
child1.setName("child1");  
Child child2 = new Child();  
child2.setName("child2");  
  
Parent parent = new Parent();  
parent.addChild(child1);  
parent.addChild(child2);  
  
em.persist(parent);  
  
em.flush();  
em.clear();  
  
Parent findparent = em.find(Parent.class, parent.getId());  
findparent.getChildList().remove(0);

쿼리를 확인해보면 delete 쿼리가 나가고 있다.

Hibernate: 
    select
        p1_0.member_id,
        p1_0.name 
    from
        parent p1_0 
    where
        p1_0.member_id=?
Hibernate: 
    select
        c1_0.parent_id,
        c1_0.member_id,
        c1_0.name 
    from
        child c1_0 
    where
        c1_0.parent_id=?
Hibernate: 
    /* delete for com.example.jpa.domain.Child */delete 
    from
        child 
    where
        member_id=?

실제 DB

 

parent

child

id가 1번인 데이터가 삭제된것을 볼 수 있다.

 

주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때사용해야함!
  • 특정 엔티티를 개별 소유할 때 사용

영속성 전이 + 고아 객체

  • CascadeType.ALL + orphanRemovel=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 AggreGrate Root 개념을 구현할 때 유용하다고 한다.

 

CascadeType.ALL 혹은 REMOVE와 orphanRemovel이 기능적으로 비슷해보이지만

orphanRemoval은 연관관계가 끊어진 엔티티를 삭제하는 반면, CascadeType.REMOVE는 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제한다.

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

JPA - 값 타입  (0) 2023.10.12
JPA - 프록시  (0) 2023.10.10
JPA - 상속관계 매핑, @MappedSuperclass  (0) 2023.10.04
JPA - 연관관계  (0) 2023.10.01
JPA - 기본 키 매핑 : @GeneratedValue  (0) 2023.09.23

+ Recent posts