JPA에서 또 다른 고비인, 프록시 에 대해서 알아보자.
먼저, 한 번 생각을 해보자.
과연 우리가 Member만 조회하고 싶을 때, 굳이 외래 키인 Team까지 다 끌고와야 할까?
아니다. 분명히 Member와 Team이 모두 필요한 경우가 있겠지만, Member만 필요한 경우도 있을 것이다.
그럴 때 JPA는 지연 로딩 과 프록시 라는 기능을 제공한다.
getReference
원래 우리는 em.find()
로 DB나 Context의 값을 조회했다.
하지만 위의 Method는 Find하는 시점에 Select Query를 바로 날려버린다.
하지만 getReference()
로 조회를 하게 되면, Query를 날리는 시점이 바뀐다.
이후에 내가 직접 값을 조회하거나 사용할 때에만 Queyr가 날아가게 된다!
JPA에서는 getReference()
에 의해 조회되는 객체를, 가짜 객체(Proxy) 라고 한다.
실제 Entity를 상속받아 겉모습만 동일하다.
Proxy
프록시 객체는 실제 객체의 참조(Target)를 보관하는 객체이다.
[사진 첨부]
getReference()
를 통해 프록시 객체를 가지고 온 뒤에, getName()
을 호출하면,
당연히 프록시 객체에는 값이 없기 때문에, 영속성 컨텍스트에 값 초기화를 요청한다.
그렇게 되면, Context와 DB를 조회해 실제 Entity를 생성해서 실제 값과 매핑을 해준다.
[사진 첨부]
Proxy의 특징
- 프록시 객체는 최초 사용 시 한번만 초기화 요청을 한다.
- 프록시 객체를 초기화 할 때, 실제 Entity로 바뀌는 것은 아니다.
- Proxy 객체가 초기화를 요청하면, 매핑을 받아서 본체에 접근이 가능한 것이다.
- 영속성 컨텍스트에 찾는 Entity가 이미 있다면?
getReference()
도 실제 Entity를 반환한다.- 반대의 경우도 마찬가지로, 프록시를 먼저 얻고 find를 하면 그 객체도 프록시다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태라면?
- 프록시를 초기화할 때 문제가 발생한다.
- LazyInitializationException
Proxy의 Method
- 프록시 인스턴스의 초기화 여부
- Entity Manager Factory에서
getPersistenceUnitUtil()
을 얻는다. - 이후
isLoaded(Proxy)
를 통해서 초기화 되었는지 아닌지 알 수 있다.
- Entity Manager Factory에서
- 프록시 클래스 확인 방법
Proxy.getClass()
를 통해서, 실제 Entity인지 Proxy인지 알 수 있었다.
- 프록시 강제 초기화
Hibernate.initialize(Proxy)
로 강제 초기화가 가능하다.- 당연히 초기화이기 때문에, Query가 강제로 나간다.
JPA의 지연 로딩
JPA의 Annotation으로도, 프록시 객체를 구현할 수 있다.
@ManyToOne(fetch = LAZY)
와 같이 연관관계에 속성을 부여하게 되면,
연관관계 요소를 지연 로딩으로 세팅을 하게 된다.
이렇게 되면, em.find(Entity)
를 할 때는 Member만 가져올 수 있으며,
이후에 실제 getTeam().getName()
로 Team을 사용 할 때에 초기화를 요청한다.
그럼, Member와 Team을 자주 사용한다면 어떻게 할 것인가?
###JPA의 즉시 로딩 위의 지연 로딩과 전혀 반대의 개념이다.
@ManyToOne(fetch = EAGER)
와 같이 속성을 부여하게 되면,
연관관계 요소를 즉시 로딩으로 세팅을 한다.
이렇게 되면 em.find(Entity)
를 할 때 모든 쿼리를 싹 보내버린다.
즉, 반환받는 객체가 Proxy가 아닌 실제 Entity가 되는 것이다.
하지만, 실무에서는 가급적이면 지연 로딩 만 사용하도록 한다!
즉시 로딩에서는, 예상치 못한 SQL문이 발생하게 된다. ==> 성능 저하 초래
이는 JPQL에서 N+1 문제를 발생시킨다.
- ex) ```createQuery(“select m from member”, Member.class)
- JPQL은 SQL이 그냥 내가 짠대로 들어가기 때문에, 문제가 발생한다.
- 나는 Member만 조회했지만, 즉시 로딩이기 때문에 Team 또한 필요해진다.
- 즉, Team에 대한 Query문이 한번 더 날아가게 되는 것이다.
@ManyToOne, @OneToOne
은 기본이 즉시 로딩이다.
따라서, LAZY로 꼭 설정해주어야 한다.
@OneToMany
는 기본이 지연 로딩이다.
Bonus : 영속성 전이(CASCADE)
특정 Entity를 영속 상태로 만들 때, 연관 Entity도 같이 영속시키고 싶다면?
예를 들어, 부모 Entity를 저장할 때 자식 Entity도 같이 영속시키는 것!
부모 Entity의 연관 관계에 Cascade 속성을 추가해줌으로써 사용할 수 있다.
OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
와 같이 사용한다.
단순히, 영속성을 전이하는 것이지 연관 관계 매핑과는 전혀 관계가 없다.
단지 편의성을 제공할 뿐!
- ALL : 모두 적용
- PERSIST : 영속
- 보통 ALL과 PERSIST까지만 사용한다.
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : 리프레시
- DETACH : DETACH
사용 시, 주의할 점은 자식들을 관리하는 부모가 단 하나일 때만 사용하도록 한다!
Bonus : 고아 객체
고아 객체란, 부모 Entity와 연관 관계가 끊긴 자식 Entity를 말한다.
OneToMany(mappedBy = "parent", orphanRemoval = true)
와 같이 부모에 사용한다.
부모의 자식 Collection에서 자식을 지우게 되면, 자동으로 DB에 Delete Query가 날아간다.
그리고 부모를 em.remove(parent)
하게 되면, 자식들은 자동으로 싹 다 지워지게 된다.
이것도 마찬가지로 자식을 소유한 부모가 하나일 때만 사용해야 한다!!
@OneToMany, @OneToOne
`관계만이 사용 가능하다.
CASCADE와 OrphanRemoval의 장점은, 두 속성을 모두 사용하게 된다면
부모의 생명주기를 통해서 자식의 생명주기까지 동시에 관리가 가능하다는 점이다.
이는 DDD(도메인 주도 설계)의 Aggragate Root 개념을 구현할 때 유용하다.
- 사진 및 자료 출처 : 김영한의 스프링 JPA 기초