[강의] 자바 ORM 표준 JPA 프로그래밍 - 기본편 08 프록시와 연관관계 관리
자바 ORM 표준 JPA 프로그래밍 - 기본편 강의
김영한님의 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편) 을 수강하면서 강의 내용을 일부 발췌해 요약한 글.
섹션 8 프록시와 연관관계 관리
멤버와 팀이 서로 연관관계가 있는 지난 예제에서, 멤버를 조회하는 경우를 생각해보자. 멤버를 조회할 때마다 팀도 같이 조회해야 할까? 멤버를 조회할 때마다 팀도 함께 출력되어야 하는 경우도 있을 것이고 멤버의 특정 속성 하나만을 조회하고자 하는 경우도 있을 것인데, 모든 경우에 팀을 조회하여야 하는 것은 큰 낭비일 수 있다.
JPA에서는 이 문제를 지연 로딩, 프록시를 통해 해결할 수 있다.
프록시
em.find() 는 데이터베이스를 통해서 실제 엔티티 객체를 조회하는 반면 em.getReference() 는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다. 이는 DB에 쿼리가 나가지 않고 조회하는 것을 의미한다.
프록시는 실제 클래스를 상속받아서 만들어지며 실제 클래스와 겉모양이 같다. 프록시 객체는 실제 객체의 참조를 보관하고, 프록시 개체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 된다. 자세한 매커니즘은 다음과 같다.
- 클라이언트가 프록시 객체에 메서드를 호출한다.
- 프록시 객체는 영속성 컨텍스트에 초기화를 요청한다. (프록의 타겟은 원래 null임. 값이 없을 때 DB를 통해 실제 엔티티를 만들어내는 과정을 초기화라고 한다)
- 영속성 컨텍스트는 DB를 조회하고 실제 Entity를 생성해 프록시 객체의 타겟과 연결해준다.
- 타겟의 메서드로 클라이언트의 요청이 동작한다.
특징
프록시 객체는 처음 사용할 때 한번만 초기화된다.
프록시 객체를 초기화할 때,프록시 객체가 실제 엔티티로 바뀌는 것이 아니라, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.
프록시 객체는 원본 엔티티를 상속받으므로 타입 체크시 주의해야 한다. 프록시 객체는 실제 엔티티 객체가 아니므로 == 비교가 실패하는 경우가 있다. 실제 로직에서는 프록시 객체가 들어올 지 실제 객체가 들어올 지 모르므로 instance of 를 사용해야 한다.
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환하고, 반대도 마찬가지이다.(프록시 객체가 있으면 find 호출하더라도 프록시 객체 반환) 원본을 반환하는 것이 성능 측면에서 유리하기도 하고, JPA 매커니즘에서 한 영속성 컨텍스트에 있는 것은 == 비교 true를 항상 보장해주기 때문이다. 핵심은 개발 시에 프록시 여부와는 무관하게 설계해야 한다!
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트 - org.hibernate.LazyInitializationException 예외)
확인
PersistenceUnitUtil.isLoaded(Object entity): 프록시 인스턴스의 초기화 여부를 확인한다. ex) emf.getPersistenceUnitUtil().isLoaded()
entity.getClass(): 프록시 클래스를 확인한다. 출력(..javasist.. or HibernateProxy…)
org.hibernate.Hibernate.initialize(entity): 프록시를 강제 초기화한다. 참고로 강제 초기화는 Hibernate에서 제공하는 것이며 JPA 표준은 강제 초기화가 없고 강제 호출: member.getName() 하여야 한다.
즉시 로딩과 지연 로딩
다시 앞의 질문으로 돌아가보자. 멤버에서 팀을 지연 로딩 LAZY로 세팅하면 팀을 프록시로 조회하여 팀을 실제로 사용하는 시점에 초기화할 수 있다! 반면, 멤보와 팀을 자주 함께 사용할 때는 즉시 로딩 EAGER로 세팅하면 조회 시 항상 멤버와 팀을 모두 조회하고, 프록시가 아닌 실제 객체를 호출한다. (fetch = FetchType.XXX에서 LAZY는 지연 로딩, EAGER는 즉시 로딩)
@ManyToOne, @OneToOne은 default가 즉시 로딩이며, @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
즉시 로딩을 적용하면 성능 상 좋지 못할 뿐더러 예상하지 못한 SQL이 발생하기도 한다. 특히 JPQL에서는 N+1 문제(1개의 최초 쿼리로 N개의 쿼리가 추가로 발생하는 문제)가 발생하기도 한다. (추후 JPQL에서 다룸) 실무에서는 가급적 지연 로딩만 사용하는 것이 좋다. 모든 로딩을 지연 로딩으로 바꾸자!
영속성 전이: CASCADE
연관관계, 즉시 로딩, 지연 관계와는 무관하다. 예를 들어 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고자 하는 경우처럼 특정 엔티티를 영속 상태로 만들 때, 이와 연관된 엔티티도 함께 영속 상태로 만들고자 할 때 영속성 전이를 사용한다.
@OneToMany(mappedBy = “~”, cascade = CascadeType.XXX)
- All: 모두 적용
- PERSIST: 영속
- REMOVE(삭제), MERGE(병합), REFRESH, DETACH 등이 있다.
함께 영속시키고자 하는 엔티티의 소유주가 하나일 때만(단일 엔티티에 종속적인 경우, 두 엔티티의 라이프사이클이 동일한 경우에만) 영속성 전이를 사용해야 함을 주의하자.
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라고 한다. orphanRemoval = true 로 설정하면 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능을 한다. 즉, CascadeType.REMOVE와 같은 기능을 한다. 이 옵션 또한 참조하는 곳이 하나일 때만 사용해야 함을 주의하자.
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거할 수 있다. CascadeType.ALL, orphanRemoval = true 두 옵션을 모두 활성화하면 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있는데 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.