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 |