JPA

프록시와 연관관계 매핑 - 2

방구석 대학생 2020. 10. 15. 23:13

"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."

www.inflearn.com/course/ORM-JPA-Basic#

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그

www.inflearn.com

 

프록시의 좀 더 자세한 특징을 알아보자.

- 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.

- 프록시 객체를 초기화 할 대 객체가 실제 Entity 로 바뀌는 것이 아니다, 초기화 되면 프록시 객체를 통해서 실제 Entity 에 접근이 가능해지는 것이다.(프록시는 유지가 되고, 내부의 target 상태만 바뀌는 것이다.)

- 프록시 객체는 원본 Entity 를 상속받는다, 따라서 타입 체크 시 주의해야 한다.(== 비교는 실패한다 - 실제 값을 가진것이 아니기 때문, 대신 instance of 를 사용한다.)

 

아래의 코드를 통해 타입 체크에 대해 좀 더 자세히 알아보자.

-JpaMain.java

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getId());
Member m1 = em.find(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));

 

위와 같은 코드를 통해 find 메소드를 통해 찾아온 객체의 클래스 정보를 == 비교를 해보면 아래와 같은 결과를 얻을 수 있다.(member1, member2 모두 프록시 객체에 한 번 연결된 상태가 아닌 순수한 초기 상태임을 가정한다.)

m1 == m2 : true

 

그런데 여기서 m2 객체에 find 메소드가 아닌 getReference 메소드를 통해 데이터를 가져오면서 프록시 객체로 만들어주면 == 비교를 했을 때 어떤 결과가 발생할까?

- JpaMain.java

Member m1 = em.find(Member.class, member1.getId());
// Member m1 = em.find(Member.class, member2.getId());
Member m1 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));

- 출력

m1 == m2 : false

 

find 메소드로 찾아온 실제 객체 Entity와 getReference 메소드로 찾아온 프록시 객체 Entity 를 == 비교한 결과 false 를 반환하는 것을 확인할 수 있다.

(실제의 경우 메소드를 통해 객체 데이터를 비교하게 되면, 메소드만을 보았을 땐 실제 Entity 객체인지, 아니면 프록시 객체인지 모르는 상태로 비교를 하는 경우가 발생할 수 있다, 그렇기 때문에 타입 비교를 할 땐 절대로 == 비교를 사용해서는 안된다.)

- JpaMain.java

System.out.println("m1 == m2 : " + (m1 instanceof Member));
System.out.println("m1 == m2 : " + (m2 instanceof Member));

 위와 같이 코드를 작성하면 아래와 같은 결과를 얻을 수 있다.

(m2 의 경우 프록시 객체로 생성 되었더라도 타입 비교의 경우 애초에 실제 존재하는 Entity 의 클래스 타입으로 프록시 객체가 생성되기 때문에 true 가 나오는 것이 정확하다.)

m1 == m2 : true
m1 == m2 : true 

 

 

- 영속성 컨텍스트에 찾는 Entity 가 이미 있으면 getReference 메소드를 호출해도 실제 Entity 를 반환한다.

JPA 에서는 같은 인스턴스의 == 비교에 대해서 같은 영속성 컨텍스트, 즉 같은 트랜잭션 레벨 안에서 조회하면 항상 "같다." 라는 결과가 나와야 한다.

- JpaMain.java

// 영속성 컨텍스트에 member1 객체의 데이터를 올려놓는다.
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

System.out.println("m1 == reference : " + (m1 == reference));

 

위와 같은 코드를 작성하여 애플리케이션을 실행하면 아래와 같은 결과를 출력하는 것을 볼 수 있다.

m1 = class hellojpa.Member
reference = class hellojpa.Member
m1 == reference : true

 

왜 reference 로 조회했는데 프록시 객체가 아니라 find 메소드로 찾아온 것과 같은 결과가 나온걸까?

-> 이미 member1 데이터를 영속성 컨텍스트에 올려놨는데 그걸 프록시로 가져와봐야 아무 이점이 없다.

그냥 원본을 반환하는게 성능 최적화 이점에서 훨씬 낫다.

* 진짜 중요한 이유?

: JPA 에서는 실제 Entity 든 프록시든 상관없이, 마치 자바 컬렉션에서 가져온 걸 == 비교하듯이, == 비교가 한 영속성 컨텍스트에서 가져온 상태에서 기본 키 값이 같으면 JPA 는 항상 true 를 반환 해주어야 한다.

(JPA 가 기본적으로 제공해주는 매커니즘 중 하나이다.)

 

- JpaMain.java

// 영속성 컨텍스트에 member1 객체의 데이터를 프록시 객체로 올려놓는다.
Member refMember = em.getReference(Member.class, member1.getId()); 
System.out.println("refMember = " + refMember.getClass());

Member findMember = em.getReference(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());

System.out.println("refMember == findMember : " + (refMember == findMember));

 

위와 같은 코드를 작성한 후 애플리케이션을 실행하면 아래와 같이 같은 프록시 객체를 반환하는 것을 알 수 있다.

refMember = class hellojpa.Member$HibernateProxy$UkeXb1bE
findMember = class hellojpa.Member$HibernateProxy$UkeXb1bE
refMember == findMember : true

 

- 왜?

: 같은 영속성 컨텍스트 안에서 기본 키 값이 같은 경우 == 비교를 했을 때 true 를 반환해야 하므로, 기본 키 값이 같으면 선언된 객체가 다르더라도 참조 값은 같아진다.

 

그런데 여기서 findMember 를 getReference 메소드가 아닌 find 메소드로 찾아오면 어떻게 될까?

- JpaMain.java

// 영속성 컨텍스트에 member1 객체의 데이터를 프록시 객체로 올려놓는다.
Member refMember = em.getReference(Member.class, member1.getId()); 
System.out.println("refMember = " + refMember.getClass());

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());

System.out.println("refMember == findMember : " + (refMember == findMember));

- 실행결과

 // refMember 는 프록시 객체로 생성되었다.(getReference)
refMember = class hellojpa.Member$HibernateProxy$2ienD9wY
Hibernate:  // 이어서 find 메소드가 실행되므로 select 쿼리가 데이터베이스에 전달된다.
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createdBy as createdb2_3_0_,
        member0_.createdDate as createdd3_3_0_,
        member0_.lastModifiedBy as lastmodi4_3_0_,
        member0_.lastModifiedDate as lastmodi5_3_0_,
        member0_.TEAM_ID as team_id7_3_0_,
        member0_.USERNAME as username6_3_0_,
        team1_.TEAM_ID as team_id1_7_1_,
        team1_.createdBy as createdb2_7_1_,
        team1_.createdDate as createdd3_7_1_,
        team1_.lastModifiedBy as lastmodi4_7_1_,
        team1_.lastModifiedDate as lastmodi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember = class hellojpa.Member$HibernateProxy$2ienD9wY
 // find 메소드로 찾았음에도 불구하고 프록시 객체로 생성되었다.
refMember == findMember : true

 

어째서 find 메소드로 찾았는데도 프록시 객체로 생성 되었을까?

- 프록시로 한 번 조회가 되면 find 메소드에서도 그냥 프록시로 반환을 해버린다.

그래야 매커니즘 대로 == 비교에서 같은 기본 키 값을 가지고 있는 대상들에 대해 true 를 반환해 줄 수 있기 때문이다.

 

* 핵심은 우리가 개발 할 때 프록시든 아니든 개발에 문제가 없게끔 개발하는 것이 중요하다.

(그런데 실무에서는 이렇게 복잡할 일이 거의 없다고 한다....)

 

 

다음 글에서는 프록시의 또 다른 자세한 특징과, 프록시에서 확인할 수 있는 각종 유틸리티 메소드 들에 대해 알아보자.