BACK/JPA

[JPA] JPA 즉시 로딩과 지연 로딩, 영속성 전이와 고아 객체

연듀 2024. 4. 2. 16:33

즉시 로딩과 지연 로딩

 

다대일 연관관계가 있는 멤버와 팀이 있을 때, 단순히 멤버 정보만 사용하는 비즈니스 로직이라서 멤버를 조회할 때 팀이 조회가 안되어도 된다고 가정하자.

이런 경우에는 지연 로딩을 쓸 수 있다. 

 

지연 로딩

 

⇒ 연관된 것을 proxy로 가져오고, 연관된 것의 실제 값을 사용하는 시점에 실제 쿼리가 나감

 

ex) 멤버를 사용할 땐 팀은 proxy로 가져오고, 팀의 실제 값을 사용할 때 DB에서 가져옴

 

 

 

Member

@ManyToOne(fetch = FetchType.LAZY) // proxy 객체로 조회한다. 
@JoinColumn
private Team team;

 

 

Member 클래스만 DB에서 조회한다.

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 m = em.find(Member.class, member1.getId()); // 멤버만 DB에서 조회
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // Team은 proxy로 조회

System.out.println("========");
m.getTeam().getName(); // 초기화: 실제 team 의 값에 접근할 때  프록시가 초기화되면서 쿼리가 날라가 DB에서 값을 가져옴 
System.out.println("========");

 

 

 

 

반면에 멤버와 팀을 자주 함께 사용한다면, 즉시 로딩을 쓸 수 있다. 

 

즉시 로딩

 

Member 조회 시 항상 Team 도 조회한다.

JPA 구현체는 가능하면 조인을 사용해 SQL 한번에 함께 조회한다. 

 

 

Member

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
Member m = em.find(Member.class, member1.getId()); // 멤버와 팀을 조인해 가져옴 
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass());  // class hellojpa.Team (실제 팀 객체)

 

 

실무에서는 즉시 로딩을 쓰면 안된다. 

즉시 로딩을 쓰면 예상하지 못한 SQL이 발생하기 때문이다. 

예를 들어 JPQL로 Member를 조회하게 되면, member와 team 따로 각각 select 문이 나간다.

 

List<Member> members = e.createQuery("select m from Member m", Member.class).getResultList(); 

 

 

만약 member1과 membr2에 각각 team1, team2를 넣고 위와 같이 멤버를 조회하면

team1, team2를 각각 select하는 쿼리가 나간다.

이걸 JPQL에서의 N+1 문제라고 한다.

 

처음에 멤버를 조회하는 쿼리를 하나 날렸는데, 추가 쿼리가 N개(멤버 조회시 나온 결과 값 개수만큼) 나간다는 것이다.

 

모든 관계를 지연 로딩으로 쓰고, 정말 꼭 같이 가져와야 하는 경우에는 3가지 방법을 쓰면 된다.

 

  1. 패치 조인(기본) - 런타임에 동적으로 원하는 것을 조인해 같이 가져오게 함
  2. 엔티티 그래프 어노테이션 사용
  3. 배치 사이즈

 

@ManyToOne, @OneToOne 은 기본이 즉시 로딩이므로 LAZY로 설정하자.

나머지는 기본이 지연로딩이다.

 

 

 

 


 

 

영속성 전이와 고아 객체

 

영속성 전이 : CASCADE 

 

특정 엔티티를 영속성 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때

cascade = CascadeType.ALL 를 사용한다.

 

ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

 

 

*영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.

엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다. 

 

 

CASCADE의 종류

 

ALL : 모두 적용

PERSIST : 영속

REMOVE : 삭제

MERGE : 병합

REFRESH : REFRESH

DETATCH : DETACH 

 

 

Parent

...

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
    
public void addChild(Child child){
    childList.add(child);
    child.setParent(this);
}

 

 

Child

@Id
@GeneratedValue
private Long id;

private String name;

@ManyToOne
@JoinColumn(name="parent_id")
private Parent parent;
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);
tx.commit();

 

parent를 persist했는데 casecade를 선언한 child들까지도 persist 된다.

 

 

고아 객체

고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // 자식 엔티티를 컬렉션에서 제거

 

 

컬렉션에서 빠진 객체는 삭제가 된다.

delete문이 나가는 것을 볼 수 있다.(DELETE FROM CHILD WHERE ID=?)

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.

 

참조하는 곳이 하나일 때 사용해야 한다.

특정 엔티티가 개인 소유할 때만 써야 한다.

 

@OneToOne, @OneToMany 만 가능하다. 

 

 

영속성 전이 + 고아 객체

 

CasecadeType.ALL + orphanRemoval = true

 

두 옵션을 활성화 하면 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있다.

도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

 

 

 

 

 

 

인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 글입니다.

https://www.inflearn.com/course/ORM-JPA-Basic