-
9. 프록시와 연관관계웹개발/Hibernate(JPA) 2020. 4. 25. 00:55
안녕하세요 현우입니다. 이번 포스팅은 [ JPA에서 사용되는 프록시 개념 ] 입니다.
JPA 학습에 도움을 주신 김영한 개발자님과 김성인 개발자님에게 항상 감사드립니다 :)
참고서 http://acornpub.co.kr/book/jpa-programmig
자바 ORM 표준 JPA 프로그래밍
JPA 기초 이론과 핵심 원리, 그리고 실무에 필요한 성능 최적화 방법까지 JPA에 대한 모든 것
www.acornpub.co.kr
1. 프록시
Member를 조회할 때 Team도 함께 조회해야 할까?
-
printMemberAndTeam(Member member)
member를 가져온뒤 해당 member에서 다시 team을 가져온다. 이때 select 쿼리문이 두번나가게 되는데 member한번으로 team까지 같이 가져올 수는 없을까?
-
printMember(Member member)
반대로 member의 name값만 출력해주면 된다. 이경우에는 member을 조회할 시에 team까지 조회하게 되면 오히려 손해를 본다.
위의 두가지 경우의 수를 프록시를 사용하여 해결한다.
2. em.find() vs em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티값을 조회
select 문 사용 - em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시)엔티티 객체 조회 (DB에 쿼리가 안나가는데 객체가 조회된다)
Member findMember = em.getReference(Member.class, membmer.getId());
select 쿼리가 나가지 않았다 다음코드를 보자. findMember.getid()는 쿼리문이 나가지 않았는데 findMember.getUsername()는 쿼리문에 나갔다 왜일까?
??? JPA는 위의 em.getReference로 member.getId()를 이미 찾았으므로 getUsername만 따로 select 쿼리문으로 가져온다.(똑똑한 JPA)
그렇다면 findMember의 정체는 뭘까?
findMember.getClass() 결과
Hibernate가 만든가짜 클래스 em.getReference() 적용시에 Proxy라는 가짜 클래스가 생성되고 해당 클래스는 id값만을 담고있따 엔티티 타겟 = 실제 클래스를 담고있지만 아직 실행 전이기 때문에 null로 표현된다.
프록시는 아래사진과 같이 HIbernate가 내부적으로 실제 클래스를 상속받아서 만들어진다. 실제클래스와 겉모양이 똑같으며 사용자 입장에서는 진짜인지 까자인지 구분하지 않고 사용하면 된다.
프록시 객체를 호출하면 target에있는 실제 클래스의 엔티티값을 대신 호출한다. 당연히 디비에서 조회한적이 없기때문에 타겟에 값이 없을 텐데 어떻게 조회가 될까?
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
매커니즘 1. 프록시에 getName이 없는상태. 이때 프록시는 영속성 컨텍스트에 초기화을 요청한다.
2. 영속성 컨텍스트는 디비를 조회해서 실제 엔티티 를 생성한다.
3. 그 후 target을 사용하여 Member을 가리키고 getName를 요청한다.
초기화 한 이후에는 프록시에 getName이 있으니 더이상 select 쿼리문이 날라가지 않는다.
3. 프록시 특징 정리 매우중요**
1. 프록시 객체는 처음 사용할때 한 번만 초기화 되고 프록시에 저장된 값을 계속 쓰게된다.
2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다!
초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능할 뿐이다. 즉 값이 교체되는 것이 아니라 proxy는 그대로고 내부의 값이 바뀌는 것인데 여기서 주의할 점은 프록시 객체는 타입의 "==" 비교가 되지 않는다는 것이다. 개발도 중는 언제 어디서 proxy에서 값을 가져다 썻는지 알지 못하는 경우가 많은데 그러한 이유로 JPA에서는 instance of 를 비교연산자 대신 사용하는걸 권장한다.
당연히 True가 나온다 타입 비교 false 물론 위의 코드만 본다면 당연히 프록시객체랑 실제 엔티티객체랑 구분할 수 있지 않을까? 라고 생각할 수 있지만...
실제 서비스 로직에서는 함수의 매개변수로 넘어오기 때문에 저게 프록시인지, 엔티티 클래스에서 넘어온 값인지 정확히 알지를 못한다...
instance of 사용 3. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
??? m1 과 reference의 클래스가 같다 왜일까? 위의 얘기대로 보면 당연히 프록시의 클래스와 member 클래스로 둘의 결과는 달라야한다.
첫번째 이유는 영속성컨텍스트에 이미 member의 값이 있는데 굳이 프록시를 써야하나? 라고 jpa가 판단하여 member클래스로 반환해주는 것이다.
두번째 이유는 JPA가 기본적으로 제공해주는 메커니즘 중 하나인 같은 영속성 컨텍스트 안에서 "==" 비교시 무조건 true를 반환 해주는 것 때문이다. jpa는 하나의 트랜잭션안에서 "=="을 보장해준다. 즉 트랜잭션안에서 무조건 true를 만들어주기 위해서 jpa는 영속성 컨텍스트에서 member의 값을 빼내는 것이다!
즉 이말을 달리하여 같은 프록시에 반환된 값을 비교하면 ?? 당연히 같은이름의 프록시객체가 반환 될것이다.
같은 이름의 프록시 객체 자 그러면 다음의 경우는 어떻게 될까? 위의 코드는 reference가 적용되고 아래의 코드는 find가 적용되었다. 당연히 위는 proxy가 출력되고 아래코드는 member이 출력될까?
위는 proxy, 아래는 member refmember은 프록시가 반환되고 당연히? find는 select 쿼리문이 나간다 하지만
결과값은 둘다 프록시객체가 반환된다..
즉 em.find로 해도 프록시로 튀어나올 가능성이 있다는 것이다. 결과적으로 jpa에서는 트랜잭션안에서는 어떻게든 비교값을 맞추게된다.
4 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일때, 프록시를 초기화하면 생기는 문제
getUsername를 호출할 경우 영속성컨텍스트에 힘을 빌려 초기화를 해야하는데 detach를 사용해 컨텍스트에서 빼냇으므로 프록시로 값을 불러오지 못하게 된다. refmember가 영속성 컨텍스트가 더이상 관리가 되지 않는 것이다.
마찬가지로 em.close(), em.clear() 도 마찬가지로 프록시로 초기화 하지 못하여 위와같은 에러가 난다.
보통 트랜잭션이 시작하고 끝날때 영속성 컨텍스트도 같이 맞춰서 끝나는데 영속성 컨텍스트가 끝날때 프록시 값을 불르면 no session 에러가 생겨버린다
프록시 확인
1. 프록시 인스턴스 초기화 확인 방법
emf.getPersistenceUnitUtil().isLoaded(refMember)
2. 프록시 클래스 출력
entity.getClass()
3. 프록시 강제초기화
org.hibernate.Hibernate.initialize(refMember);
JPA에는 강제초기화가 없기때문에 refMember.getName() 식으로 강제호출 해야한다.
'웹개발 > Hibernate(JPA)' 카테고리의 다른 글
11. 영속성 전이(CASCADE)와 고아 객체 (0) 2020.04.28 10. 즉시 로딩과 지연 로딩 (0) 2020.04.28 7. 고급매핑 (0) 2020.04.20 6. [JPA] 다양한 연관 관계매핑 방법 (0) 2020.04.10 5. [JPA] 양방향 연관관계와 연관관계 주인 (0) 2020.04.01 -