프록시와 연관관계 매핑 - 1
"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."
www.inflearn.com/course/ORM-JPA-Basic#
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그
www.inflearn.com
프록시(Proxy)란 무엇일까?
Member - Team 연관관계가 있을 때, Member 클래스의 데이터를 조회할 때 Team 클래스의 데이터도 함께 조회해야 할까?
만약 비즈니스 로직에서 Member 와 Team 데이터를 같이 출력해야 하는 경우, find 메소드를 통해 데이터베이스 에서 Member 데이터를 찾아올 때 Member 데이터와 연관관계를 가지고 있는 Team 데이터도 쿼리 하나로 한방에 가져오면 좋을 것이다.
그런데 상황이 바뀌어서 Member 데이터만 출력하면 되는 경우가 되었다면?
- 이럴 경우 다른 도메인 클래스와 연관관계를 가지고 있다고 해서 연관된 도메인 클래스의 데이터까지 가져오게 되면서 손해를 보게 된다.
-> 사용하지도 않는 데이터를 가져온다는것은 사실상 최적화가 안된다는 뜻이다.
어느 경우에는 연관관계를 가지고 있는 도메인 클래스의 데이터까지 모두 가져와야 하나, 어느 경우는 그럴필요 없이 호출하고자 하는 도메인 클래스의 데이터 자체만 가져와야 한다면?
- JPA 입장에서 둘 다 써야 하는 경우라면 괜찮겠지만, 그렇지 않은 경우는 낭비라고 볼 수 있다.
JPA 는 이런경우 프록시, 또는 지연로딩 이라는 기능을 활용하여 해결할 수 있다.
지연 로딩과 같은 기능을 활용하여 위와 같은 문제를 해결하려면 일단 프록시(Proxy) 의 개념을 명확하게 알고 있어야 한다.
JPA 에서는 find 메소드 뿐만 아니라, 참조를 가져온다는 의미인 getReference() 메소드 또한 제공한다.
getReference 메소드는 데이터베이스를 통해 실제 Entity 객체를 조회해 오는 find 메소드와는 달리, 데이터베이스 조회를 미루는 가짜(프록시) Entity 객체를 조회하는 메소드이다.
-> 쉽게 말해, DB에 쿼리를 전달하지 않은 상태에서 객체를 조회해 오는 것이다.
JPA 에서 select 할 경우 연관관계를 가지고 있는 도메인 클래스의 데이터를 검색하면 연관관계에 있는 도메인 클래스들을 한번에 조인을 통해 데이터를 모두 가져온다.
hibernate SQL 이 출력되는 것을 보면 알 수 있다.
- JpaMain.java
Member member = em.find(Member.class, 1L);
- hibernate SQL
Hibernate:
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=?
여기서 Team 클래스의 데이터를 제외한 Member 클래스의 데이터만을 가져와야 한다면 getReference 메소드를 사용할 수 있다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member1.getId()); //getReference 를 통해 프록시 객체 생성
System.out.println("findMember = " + findMember.getClass()); // 프록시 객체임을 알려주는 출력문
System.out.println("findMember.id = " + findMember.getId()); // 이미 가지고 있는 데이터 이므로 SQL 이 출력되지 않는다.
// 프록시 객체를 통해 실제 클래스의 Entity 를 참조하여 데이터를 가져온다.
System.out.println("findMember = " + findMember.getUsername());
위의 코드와 같이 getReference 메소드를 활용하게 되면 위의 hibernate SQL 과 같은 select 쿼리가 나오지 않는다.
(단순히 getReference 메소드를 통해 찾아오기만 했을 땐 쿼리가 나가지 않음)
그런데 getUsername 메소드와 같이 찾아온 데이터를 직접 사용하는 경우엔 쿼리가 나오는 것을 확인할 수 있다.
즉, 찾아오기만 할 때는 쿼리를 전달하지 않고 있다가, 찾아온 객체를 직접 사용하는 코드가 실행되면 그 시점에 쿼리가 DB에 전달되어 실제 데이터를 찾아오는 것이다.
-> 심지어 Team 클래스의 데이터를 사용하지 않았기 때문에 연관관계가 있음에도 불구하고 해당 클래스의 데이터는 select 해오지 않는다.
- hibernate SQL
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, team_TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
findMember = class hellojpa.Member$HibernateProxy$ey1RDCIC
//getClass 메소드 실행 결과 프록시 객체임을 알 수 있는 출력문이다.
findMember.id = 1
Hibernate: // Team 클래스의 데이터는 select 해오지 않았다.
select
member0_.MEMBER_ID as member_i1_4_0_,
member0_.createdBy as createdb2_4_0_,
member0_.createdDate as createdd3_4_0_,
member0_.lastModifiedBy as lastmodi4_4_0_,
member0_.lastModifiedDate as lastmodi5_4_0_,
member0_.team_TEAM_ID as team_tea7_4_0_,
member0_.USERNAME as username6_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
findMember = member1
여기서 재밌는 점은 기본 키 값을 이용하여 getReference 메소드를 통해 찾아온 데이터를 getId 와 같은 메소드로 키 값을 가져오려고 할 때는 쿼리가 나오지 않는다, 이미 getReference 메소드에서 사용된 기본 키 값을 그대로 가져오면 되기 때문이다. - 이미 가지고 있던 데이터 이기 때문에 굳이 쿼리를 하지 않는다.
그렇다면 getReference 메소드를 통해 찾아온 객체 데이터의 정체가 뭘까?
위의 코드에서 System.out.println("findMember = " + findMember.getClass()); 를 통해 생성된 출력문을 살펴보자.
findMember = class hellojpa.Member$HibernateProxy$ey1RDCIC
위의 출력문에서 $HibernateProxy$ ~~~ 로 되어있는 것을 볼 수 있는데 이는 Hibernate 가 강제로 만든 가짜 클래스 라는 뜻이다.(프록시 클래스)
그렇다면 프록시의 매커니즘은 어떻게 되어 있을까?
- getReference 메소드를 호출하면 진짜 객체 데이터를 찾아오는게 아니라, Hibernate 가 내부의 라이브러리 를 활용하여 가짜, 속칭 프록시 라고 부르는 가짜 Entity 객체를 준다.
(껍데기는 똑같은데, 내부는 텅 비어있다.)
- 또한 내부에는 Entity target 이 들어있는데 이게 진짜 Reference 를 가르킨다.(초기에는 비어있는 상태이다.)
처음 getReference 메소드를 통해 객체 데이터를 호출하면 빈 껍데기 안에 데이터의 id 값만 들어있는 가짜 데이터가 반환된다.
* 프록시의 특징
- 실제 클래스를 상속받아 만들어진다, 그래서 실제 클래스와 겉모양이 똑같다.(개발자가 직접 손대지 않아도 Hibernate 가 내부에 있는 프록시 라이브러리를 활용하여 만들어내게 된다.)
- 이론상 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target 또는 reference) 를 보관한다. 그래서 프록시 객체를 호출하면 실제 target 에 있는 데이터를 대신 호출해준다.
- 그런데 초기엔 아직 DB 를 통해 데이터를 조회하지 않았으므로 아무런 데이터도 존재하지 않는다.
* 프록시 객체의 초기화 과정
Member member = em.getReference(Member.class, "id1");
member.getName();
- 위의 코드와 같이 getReference 메소드를 이용해 프록시 객체를 가지고 온 다음, getName(또는 getUsername) 과 같은 메소드를 이용해 프록시 객체를 호출하면 처음엔 target 이 비어있는 상태이다.
- 그러면 JPA 가 영속성 컨텍스트에서 DB 를 조회하여 실제 Entity 객체를 생성한 후 영속성 컨텍스트에 해당 데이터를 넘겨준다.
- 이후 프록시 객체의 target 에 해당 실제 데이터를 연결해준다.
- 그래서 getUsername 을 호출했을 때 target 에 연결된 진짜 getUsername 메소드를 통해서 해당 클래스에 있는 데이터가 반환되는 것이다.
- 그렇게 한번 실제 클래스와 연결되어서 가져온 데이터는 이후에 다시 한번 조회했을 땐, 이미 연결되어 있는 상태이기 때문에 굳이 한번 더 DB에 데이터를 조회할 필요 없이 곧장 데이터를 사용할 수 있게 된다.
다음번 글에선 프록시의 특징을 좀 더 자세하게 알아보자.