8장_프록시와 연관관계 관리
비즈니스 로직에 따라 객체 그래프를 탐색할 수도 있고 그렇지 않을 수도 있다. 그렇기 때문에 처음부터 모든 데이터를 조회해서 저장해 놓는 것은 비효율적이다. 이런 문제를 해결하기 위해 JPA는 지연로딩을 지원한다. 지연로딩은 실제 객체를 사용할 때 조회를 하는 것을 의미한다. 그리고 이런 지연 로딩을 위해서는 실제 엔티티 대신 데이터베이스 조회를 지연시킬 수 있는 가짜 객체를 등록해 사용하는데 이를 프록시 객체라고 한다.
프록시 기초
EntityManager.find()
메소드는 하나의 엔티티를 데이터베이스에서 조회하는 메소드로 객체를 사용하든 사용하지 않든 데이터베이스에서 조회하게 된다. 객체를 사용 시점까지 데이터베이스에서 조회를 미루고 싶다면 EntityManager.getReference()
메소드를 호출하면 된다.
이 메서드는 데이터베이스에서 조회도 실제 엔티티를 생성하지도 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
프록시 객체 역시 엔티티 객체를 상속 받아서 생성되기 때문에 개발자는 이를 구분하지 않고 사용해도 된다. 프록시 객체는 실제 엔티티 클래스의 참조 값을 가지고 있어 메소드 호출 시 실제 객체의 메소드가 호출되며 이 때 데이터베이스에서 조회하여 실제 엔티티 객체를 생성하는데 이를 프록시 객체의 초기화라고 한다.
프록시의 초기화
1) 프록시 객체에 getName()
과 같은 메서드가 호출된다.
2) 프록시 객체는 실제 엔티티가 생성되어 있지 않다면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이를 초기화라고 한다.
3) 영속성 컨텍스트는 데이터베이스를 조회해 실제 엔티티를 생성한다.
4) 이렇게 얻은 엔티티의 참조값을 프록시 target 필드에 저장한다.
5) 실제 엔티티의 참조값을 통해 getName()
메서드를 호출하여 결과를 반환한다.
프록시의 특징
- 프록시 객체는 사용 시 한 번만 초기화된다.
- 프록시 객체는 초기화 이후에도 실제 엔티티로 바뀌는 것이 아닌 프록시를 통해 실제 엔티티의 메소드를 호출하게 된다.
- 프록시는 원본 엔티티를 상속받은 객체이기 때문에 타입 체크 시에 주의해서 사용해야 한다.
- 실제 엔티티가 있다면
getReference()
를 사용해도 실제 객체가 반환된다. - 영속성 컨텍스트의 도움을 받기 때문에 준영속 상태에서 프록시를 초기화하면 오류가 발생한다.
프록시와 식별자
엔티티를 프록시로 조회할 때 식별자 값을 같이 파라미터로 전달하기 때문에 접근 레벨을 메서드로 설정해두면 getId()
와 같이 식별자 값을 가져오는 경우에는 프록시를 초기화하지 않는다. 필드로 둘 경우 식별자 외에 다른 값을 사용할 수 있기 때문에 이를 초기화한다. 연관관계를 설정할 때는 접근 레벨을 필드로 두더라도 프록시를 초기화하지 않는데 이는 연관 관계를 설정할 때는 식별자 값만을 사용하기 때문이다. 이를 통해 데이터베이스의 접근 횟수를 줄일 수 있다.
즉시 로딩과 지연 로딩
JPA는 개발자가 데이터베이스 조회 시점을 선택할 수 있도록 em.find()
[즉시 로딩]와 em.getReference()
[지연 로딩]을 지원한다.
즉시 로딩
@ManyToOne
에서 fetch를 FetchType.EAGER
로 설정한다.
JPA에서는 쿼리 최적화를 위해 조인을 사용해 한 번의 조회만으로 Member와 Team 두 엔티티 모두 조회한다.
이 때 만약 Team 필드가 nullable = true
라면 JPA는 외부 조인을 사용한다. 회원의 팀이 없을 수도 있기 때문에 내부 조인을 사용하게 된다면 팀은 커녕 회원도 찾을 수가 없기 때문이다. 하지만 성능은 내부 조인이 좀 더 좋기 때문에 이를 성능 최적화를 위해서는 team을 @ManyToOne(fetch = FetchType.EAGER, nullable = false)
로 두거나 @ManyToOne(fetch = FetchType.EAGER, optional = false)
로 null 값을 허용하지 않게 하여 내부 조인을 사용하게 할 수 있다.
정리하자면 JPA는 nullable을 허용한 선택적 연관관계의 경우 외부 조인을 허용하지 않는 필수적 연관관계의 경우 내부 조인을 사용한다.
지연 로딩
fetch
를 FetchType.LAZY
로 두면 된다.
Member member = em.find(Member.class, "id1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 이 때 조회 후 프록시 초기화
지연 로딩을 선택하면 위와 같이 getTeam()
을 호출 했을 때는 프록시 객체를 넣어둔다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미루고 이를 지연 로딩이라고 한다.
즉시 로딩, 지연 로딩 정리
엔티티를 조회할 때 연관된 모든 엔티티를 영속성 컨텍스트에 저장하는 것은 현실적이지 않고 필요할 때마다 조회하는 것도 애플리케이션 최적화 관점에서 그리 바람직하지 않다. 따라서 상황에 따라 다르게 사용한다.
프록시와 컬렉션 래퍼
하이버네이트에는 영속성을 관리하고 추적할 목적으로 영속성 컨텍스트에 엔티티를 저장할 때 엔티티 내부 컬렉션을 내장 컬렉션으로 변경하는데 이를 컬렉션 래퍼라고 한다. 엔티티는 프록시 객체가 지연 로딩을 시켜주고 컬렉션의 경우 컬렉션 래퍼가 지연로딩을 도와준다. 그리고 member.getOrders()
가 아닌 member.getOrders().get(0)
과 같이 실제 사용할 때 컬렉션을 초기화한다.
JPA 기본 페치 전략
JPA에서는 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 기본으로 한다. 컬렉션의 경우 너무 많은 데이터를 한 번에 불러올 수 있는 위험이 있기 때문이다. 추천은 먼저 모든 연관관계에 지연 로딩을 사용하고 개발이 완료되었을 때 최적화를 하면서 필요한 곳에 즉시 로딩을 사용하는 것이다.
컬렉션에 FetchType.EAGER 사용 시 주의점
- 두 개 이상의 컬렉션에 사용하지 않는다.
- 외부 조인을 사용한다.
영속성 전이 : CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 영속 상태로 만들고 싶을 때 사용한다.
JPA에서 엔티티를 영속 상태로 만들 때 연관된 모든 엔티티는 영속 상태여야 한다.
영속성 전이 : 저장
연관 관계 설정 시에 cascade = CascadeType.PERSIST
를 넣어주면 된다. JPA의 영속성 전이는 연관관계 매핑과는 관계가 없어 직접 연관관계 설정을 해줘야 한다. 다만 엔티티를 영속화할 때 연관 엔티티도 영속화하는 편리함을 제공할 뿐이다.
영속성 전이 : 삭제
CascadeType.REMOVE
를 사용한다. 이후 부모 엔티티를 삭제하면 자식 엔티티도 삭제가 된다.
저장(persist)과 삭제(remove)는 플러시가 발생할 때 전이된다.
고아 객체
JPA에서는 부모 엔티티와의 연결 관계가 끊긴 자식 엔티티를 자동으로 삭제해주는 기능을 갖고 있는데 이를 고아 객체 제거라고 한다.
이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자동으로 자식 엔티티를 삭제할 수 있다.
고아 객체 삭제 기능은 자식 엔티티의 참조가 한 곳에서만 사용될 때만 쓸 수 있다. 여러 곳에서 사용될 경우 참조가 사라져 문제가 발생한다. 따라서, orphanRemoval = true
는 @OneToMany
, @OneToOne
에서만 사용할 수 있다. 또한 CascadeType.REMOVE
와 같이 부모 엔티티가 제거되면 자식 엔티티는 고아가 되는 것과 같아 자식 엔티티 역시 삭제된다.
영속성 전이 + 고아 객체, 생명주기
일반적으로 엔티티는 em.persist()
를 통해 영속화 하고 em.remove()
를 통해 제거된다. 이는 엔티티가 스스로 생명 주기를 관리한다는 뜻이다. 이 때 CascadeType.ALL
과 orphanRemoval = true
를 같이 주면 부모 엔티티를 통해 자식 엔티티의 생명 주기를 관리할 수 있다.
'JPA' 카테고리의 다른 글
12장_스프링 데이터 JPA (0) | 2024.04.02 |
---|---|
11장_웹 애플리케이션 제작 (0) | 2024.04.01 |
Java ORM 표준 - JPA 프로그래밍 10장 (0) | 2024.03.24 |
Java ORM 표준 - JPA 프로그래밍 9장 (0) | 2024.01.19 |