김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
https://www.inflearn.com/courses?s=%EA%B9%80%EC%98%81%ED%95%9C
프록시
JPA로 Member를 조회할 때 Team도 함께 조회해야 할까? 이것은 너무 비효율적이지 않을까? 라는 생각을 할 수 있습니다.
Member는 Team을 참조로 가지고 있지만 DB에는 Member와 Team 테이블이 따로따로 있어서 두개의 테이블을 모두 참조해야 합니다.
하지만 Member객체를 가져온 후 Member의 name, age 등등 의 필드만 사용하고, 조회한 Member의 Team에 대한 정보는 일절 필요하지 않은데, Team에 대한 정보까지 DB에서 가져오는 것은 굉장히 비효율적으로 보입니다.
Member를 조회할 때 Team도 함께 조회해야 할까?
회원과 팀을 함께 출력
회원만 출력
이를 위해 프록시가 있습니다.
1. em.find() vs em.getReference()
em.find() 먼저 살펴보면 아래와 같이 em.flush(), em.clear()를 한 상황에서 find에 의해 select문이 실행됩니다.
em.getReference()는 다릅니다.
아래에서 보듯이 select쿼리가 나가지 않습니다.
하지만 아래와 같이 team에 대한 정보도 조회한다면 select 쿼리가 나가는 것을 볼 수 있습니다.
( ! 주의 ! 여기서 Member의 id만을 조회한다면 select쿼리가 나가지 않습니다.
왜냐하면 em.getReference(Member.class, member.getId());를 했을 때 member의 id를 넘겨줬기에 이미 JPA가 DB로 가지 않아도 Member의 id를 알고 있습니다.)
이제서야 select쿼리가 나간 것을 볼 수 있습니다.
즉 JPA는 getReference를 사용시 실제로 해당 필드가 사용될 때만 가져오는 것을 알 수 있습니다.
이것을 가능하게 하는 것이 프록시입니다.
한 번 getReference로 가져온 객체의 class를 조회해보겠습니다.
조회해보니 아래와 같이 순수 Member객체가 아닌 Proxy와 관련된 객체임을 알 수 있습니다.
즉
em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
프록시는 아래와 같이 생겼습니다.
안이 텅텅 비어있고, target으로 실제 Member객체의 래퍼런스(참조값)을 가지고 있습니다.
(위의 그림에서는 실제 Member의 메소드를 실행하지 않고. getReference만 했으므로 target은 아직 null입니다.)
프록시의 특징은 다음과 같습니다.
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
동작과정은 아래와 같습니다.
Member member = em.getReference(Member.class, "id1");
member.getName();
위와 같은 코드를 실행한다면
영속성 컨텍스트에 초기화를 요청을 한다는 것이 중요합니다.
프록시의 특징 심화
1. 프록시 객체는 처음 사용할 때 한번만 초기화
2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능:
즉 아래와 같은 코드를 실행했을 때
첫 번째 출력문, 세 번째 출력문 모두 같은 프록시 객체가 출력됩니다. getName()을 하여 영속성 컨텍스트에 초기화를 요청했다고 해서 프록시 객체가 Member객체가 되는 것은 아니라는 것입니다. Member객체는 따로 존재합니다.
이와 같은 특징은 아래의 3번 특징과 연결됩니다.
3. 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
즉 프록시 Member 객체와 프록시가 아닌 Member 객체를 비교할 때 주의해야 합니다.
위와 같을 때 결과는 당연히 false입니다.
하여 프록시객체(m2)는 Member 객체(m1)을 상속하므로 instance of 을 사용하면 true가 나올 것 입니다.
4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
즉 getReference를 날렸는데, 이미 영속성 컨텍스트에 해당 객체가 있다면 굳이 프록시를 만들고, DB에서 가져올 필요없이 영속성 컨텍스트에서 가져온 엔티티를 반환하는 것입니다.
getReference로 조회했지만 프록시Member가 아닌 Member객체임을 알 수 있습니다.
em.find()를 한 과정에서 이미 영속성 컨텍스트의 1차 캐시에 member1이 등록된 것입니다.
❗️ JPA는 항상 같은 트랜잭션 안에서 같은 객체이거나 같은 PK값인 대상을 조회한다면 무조건 같은 객체임을 보장해줘야 합니다. ❗️
아래와 같이 프록시 객체도 마찬가지 입니다.
⁉️ 심지어 아래와 같이 프록시 객체를 먼저 조회한 상황에서도 마찬가지 입니다. ⁉️
em.find()를 한 결과마저 프록시 객체가 반환되었습니다.
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트org.hibernate.LazyInitializationException 예외를 터트림)
즉 아래처럼 코드로 테스트해보겠습니다.
프록시 객체인 m1을 영속성 컨텍스트에서 제거(detach)한 후 getName()을 하고 있습니다.
위에서 프록시의 동작원리를 서술한 부분에서 name을 가져오기 위해 영속성 컨텍스트를 통해 초기화한다고 한 것을 기억하실 겁니다.
하지만 이미 m1은 em.detach(m1); 에 의해 영속성 컨텍스트에서 관리되지 않는 상태이죠.
히여 다음과 같은 에러가 발생합니다.
영속성 컨텍스트를 close해도 마찬가지입니다.
두 상황 모두 LazyInitializationException이 발생하는 것을 알 수 있습니다.(em.clear()를 해도 마찬가지 입니다.)
프록시 확인
프록시의 상태를 알아볼 수 있는 방법을 알아보겠습니다.
1. 프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)
아래처럼 getName()후에는 초기화 되어 true가 나옵니다.
2. 프록시 클래스
확인 방법 entity.getClass().getName() 출력(..javasist.. or HibernateProxy...)
3. 프록시 강제 초기화 org.hibernate.Hibernate.initialize(entity);
참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
참고: 김영한님 인프런 강의, 강의 PDF
'JPA > JPA원리' 카테고리의 다른 글
20. 영속성 전이 : CASCADE, 고아객체 (0) | 2021.07.16 |
---|---|
19. 지연로딩 (0) | 2021.07.16 |
17. @MappedSuperclass (0) | 2021.07.14 |
16 상속관계 매핑 (0) | 2021.07.14 |
15 더 복잡한 연관관계를 매핑하기 (0) | 2021.07.13 |