JPA (8) 프록시와 연관 관계 관리
12 Mar 2022김영한님의 자바 ORM 표준 JPA 프로그래밍 강의 정리
Proxy
em.find()
: DB에서 실제 엔티티 객체 조회em.getReference()
: DB에서 바로 조회하는 것을 미루고 프록시 엔티티 객체 조회- HibernateProxy
- 실제 엔티티 클래스를 상속받아 겉모양이 같음
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용(이론)
- 실제 객체의 참조 보관(target)
- 프록시 객체를 호출하면 실제 객체의 메소드 호출
- 프록시 객체 초기화
- 엔티티 클래스의 메소드 호출
- 프록시 객체가 초기화되어 있지 않아 target이 null
- 영속성 컨텍스트에 초기화 요청
- 영속성 컨텍스트에서 DB 조회
- 실제 엔티티 생성
- 프록시 객체의 target이 처음 호출했던 엔티티 클래스의 메소드 호출
- 프록시 확인
- 초기화 확인:
emf.getPersistenceUnitUtil().isLoaded(proxy);
- 프록시 클래스 확인:
proxy.getClass()
- 강제 초기화:
Hibernate.initialize(proxy);
- 초기화 확인:
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 초기화 후에 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며 프록시 객체를 통해 실제 엔티티에 접근 가능
-
프록시 객체는 실제 엔티티를 상속받은 것이며 타입 체크할 때는
==
이 아닌instanceof
로 해야 함 - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 도 실제 엔티티 반환
- 이미 영속성 컨텍스트에 있기 때문에
- JPA에서는 하나의 영속성 컨텍스트에서 조회한 엔티티를
==
으로 비교하면true
가 나오게 됨 - getReference 후에 find 하면 프록시 반환
- 준영속 상태일 때, 프록시를 초기화 하면
org.hibernate.LazyInitializationException
예외 발생
Book 객체를 생성한 후, 영속성 컨텍스트를 초기화 한 후에 find 메소드로 엔티티 객체를 가져왔다. (1)에서는 class class basic.domain.Book으로 출력되면서 엔티티 클래스인 것을 확인할 수 있다. 다시 영속성 컨텍스트를 초기화 한 후에 프록시 객체를 가져와서 출력한 (2)를 보면 (1)과 비슷하지만 class basic.domain.Book$HibernateProxy$cPTVewPO 이렇게 출력되면서 하이버네이트 프록시 객체라는 것을 알 수 있다. (3), (5)에서 초기화 여부를 출력하도록 하고 (4)에서 프록시 객체를 초기화 하면 (3)에서는 false, (5)에서는 true가 출력되면서 true가 출력되기 전에 영속성 컨텍스트가 DB에서 조회하는 쿼리 또한 확인할 수 있다.
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Book book = new Book();
book.setName("book1");
em.persist(book);
em.flush();
em.clear();
Book findBook = em.find(Book.class, book.getId());
System.out.println("findBook.getClass() = " + findBook.getClass()); // (1)
em.flush();
em.clear();
Book reference = em.getReference(Book.class, book.getId());
System.out.println("reference.getClass() = " + reference.getClass()); // (2)
System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference)); // (3)
Hibernate.initialize(reference); // (4)
System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference)); // (5)
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
즉시 로딩과 지연 로딩
플레이어와 팀이 서로 다대일 관계로 연결되어 있을 때 (1)처럼 플레이어를 조회하면 팀 테이블도 조인해서 함께 조회하게 된다. @ManyToOne의 fetch의 기본 설정이 FetchType.EAGER
이기 때문이다. 이미 팀 데이터도 DB에서 조회해서 영속성 컨텍스트에서 관리하고 있기 때문에 (2)에서 팀의 클래스를 출력하면 프록시 클래스가 아닌 엔티티 클래스로 나오게 된다.
@Entity
public class Player {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
// main
Team team = new Team("teamA");
em.persist(team);
Player player = new Player("player1", team);
em.persist(player);
em.flush();
em.clear();
Player findPlayer = em.find(Player.class, player.getId()); // (1)
System.out.println(findPlayer.getTeam().getClass()); // (2)
플레이어를 조회할 때마다 항상 팀도 조회하는 경우가 아니라면 굳이 매번 팀을 조회할 필요가 없을 것이다. 그럴 때는 fetch 설정을 @ManyToOne(fetch = FetchType.LAZY)
로 해주면 된다. 그런 다음 위의 코드와 똑같이 조회하면 플레이어 테이블에서만 조회하고 (2)에서는 프록시 클래스인것을 알 수 있다.
즉시 로딩 주의
- 가급적 지연 로딩을 사용
- 즉시 로딩을 사용할 때 예상하지 못한 쿼리 발생
- JPQL에서 N + 1 문제 발생
@ManyToOne
,@OneToOne
은 기본이 즉시 로딩이므로 지연 로딩으로 설정
즉시 로딩을 사용할 때 JPQL에서 발생할 수 있는 N+1 문제는 아래 같은 상황에서도 확인할 수 있다. 두 개의 팀과 플레이어를 만들고 각 플레이어는 각각 다른 팀과 연관 관계를 가지도록 했다. 그 다음 JPQL을 이용해 모든 플레이어 데이터를 조회하도록 했다. 작성한 쿼리만 보면 ‘select * from player’라는 쿼리가 나갈 거 같지만 즉시 로딩으로 설정되어 있다면 플레이어를 조회한 후에 각 플레이어와 연관된 팀의 개수만큼 팀 데이터를 조회하는 쿼리가 나가게 된다. 이렇게 예상하지 못하는 쿼리가 발생할 수 있기 때문에 즉시 로딩은 사용하지 않는 것이 좋다.
Team teamA = new Team("teamA");
em.persist(teamA);
Player player = new Player("player1", teamA);
em.persist(player);
Team teamB = new Team("teamA");
em.persist(teamB);
Player player2 = new Player("player1", teamB);
em.persist(player2);
em.flush();
em.clear();
List<Player> players = em.createQuery("select p from Player p", Player.class).getResultList();
지연 로딩 활용
- 모든 연관 관계를 지연 로딩 사용
- JPQL fetch join이나 엔티티 그래프 기능 사용
영속성 전이
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
@OneToMany
에서 cascade 설정에서 적용@OneToMany(mappedBy = "parent", cacade = CascadeType.ALL)
- 영속성 전이와 연관 관계 매핑은 관련 없음
- cascade 종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: 부모 엔티티가 리프레시 될 때 자식 엔티티도 DB로부터 리로딩
- DETACH: 부모 엔티티가 영속성 컨텍스트에서 지워질 때 자식 엔티티도 지워짐
- 사용하는 경우
- 하나의 부모 엔티티가 자식 엔티티를 관리해 종속적인 관계일 때 사용
- 부모 엔티티와 자식 엔티티의 라이프 사이클이 유사할 때
- 다른 엔티티와도 연관 관계가 있을 때는 사용하지 않아야 함
고아 객체
-
부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 설정
@OneToMany
에서 orphanRemoval 설정에서 적용@OneToMany(mappedBy = "parent", orphanRemoval = true)
- 참조하는 곳이 하나일 때만 사용해야 함
@OneToOne
,@OneToMany
사용 가능- 고아 객체 기능을 활성화 하면 부모 엔티티를 삭제할 때 자식 엔티티도 삭제하게 되므로
CascadeType.REMOVE
처럼 동작하게 됨 - 영속성 전이와 고아 객체 삭제 함께 사용
cascade = CascadeType.ALL
&orphanRemoval = true
- 부모 엔티티를 통해 자식 엔티티의 생명 주기 관리 가능
- 도메인 주도 설계의 Aggregate Root 개념 구현할 때 유용