JPA의 즉시 로딩과 지연 로딩을 이해하기 위해 프록시 개념을 먼저 이해해야 합니다.
프록시는 실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같습니다. 실제 값이 필요해질 때까지 조회를 미룰 수 있습니다. 프록시 객체는 실제 객체의 참조(target)를 보관하고 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출합니다.
em.find()
vs em.getReference()
em.find()
: 데이터베이스를 통해서 실제 엔티티 객체 조회em.getReference()
: 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회아래는 회원
연관관계에서 프록시 객체를 활용한 예시입니다.
Team team = new Team();
team.setName("teamA");
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(team);
em.persist(member);
em.flush(); // SQL 반영
em.clear(); // 영속성 Context 1차 캐시 초기화
Member referMember = em.getReference(Member.class, "id1"); // 프록시 객체
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// referMember.getId() 메모리에 있기 때문에 DB 조회를 하지 않는다.
// referMember.getName() DB에 SELECT query를 보내서 값을 가져온다.
Member 프록시 객체는 getName()
메소드를 호출하기 위해 실제 객체의 참조를 갖기 위해서 영속성 Context를 통해 DB에 조회를 합니다. DB에서 가져온 정보로 실제 Entity 를 생성하고, 프록시 객체가 해당 Entity를 참조하도록 설정합니다.
==
대신 instance of
를 사용합니다.em.getReference()
를 호출해도 실제 엔티티를 반환받습니다.LazyInitializationException
예외가 발생합니다.Member referMember = em.getReference(Member.class, "id1");
em.detach(referMember); // 영속성 Context에서 분리
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
프록시 인스턴스 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
: 프록시 클래스 확인 방법entity.getClass().getName()
출력 : 프록시 강제 초기화Member와 Team을 자주 함께 사용한다면 즉시 로딩 EAGER를 사용해서 함께 조회합니다. 반면 비즈니스 로직에서 Member 정보만 필요하고 Team 정보가 당장 필요 없는 경우 지연 로딩 LAZY를 사용해서 프록시로 조회 하고 동작하도록 구현합니다.(성능상 유리)
지연 로딩 LAZY
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Member member = em.find(Member.class, 1L);
// Member의 id, name 필드 가져옴
System.out.println("member = " + member.getId() + ": " + member.getName());
Team team = member.getTeam();
// 연관관계 값을 요청한 경우, 그 때 DB에 query를 보내서 team 필드를 가져옴
System.out.println("team = " + team.getId() + ": " + team.getName());
즉시 로딩 EAGER
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
주의 해야할 점은 실무에서 즉시 로딩 보다는 가급적 지연 로딩만 사용해야합니다.
즉시 로딩을 적용하면 예상하지 못한 거대한 SQL이 발생하고, 즉시 로딩은 JPQL에서 N+1 문제를 일으킵니다. 따라서 @ManyToOne
, @OneToOne
은 기본 값이 즉시 로딩이므로 지연 로딩으로 설정해서 사용해야합니다.
N + 1 문제
Member 전체 조회를 했을때 하나의 쿼리(select * from Member m) 로 인해 N개의 추가 쿼리가(각각 member의 team을 가져오는 select문) 발생하는 문제
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용합니다. 즉, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장합니다.
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없으며 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공합니다.
CASCADE의 종류
CASCADE 예제
Parent 엔티티 childList 필드에 영속성 전이 설정(cascade)을 하게 되면, parent 엔티티가 영속될 때 이와 연관된 childList도 함께 영속 상태가 됩니다.
Parent
엔티티@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList;
}
Child
엔티티@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
Child child1 = Child.of("James Son");
Child child2 = Child.of("James Daughter");
Parent parent = Parent.of("James");
parent.addChild(child1);
parent.addChild(child2);
parentRepository.save(parent);
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제합니다.
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE
처럼 동작한다.
orphanRemoval = true
@OneToOne
, @OneToMany
만 가능@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Child> childList;
}
Child child1 = Child.of("James Son");
Child child2 = Child.of("James Daughter");
Parent parent = Parent.of("James");
parent.addChild(child1);
parent.addChild(child2);
parentRepository.save(parent);
// Parent의 childList는 영속성 전이 설정이 되어있기 때문에, list에 속한 child도 영속화된다.
parent.getChildList().remove(0);
// 첫번째 자식 Entity 와의 연관관계를 끊었으므로 child1은 고아 객체가 된다.
// orphanRemoval = true 설정이 되어있기 때문에 고아 객체는 자동으로 삭제된다.
CascadeType.ALL + orphanRemoval=true
두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다. 이는 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용합니다.
👉 실전 예제 코드