01. 프록시 개요
Member를 조회할 때 Team도 함께 조회해야 할까?
Member와 Team은 다음과 같은 관계를 맺고 있다.
회원과 팀을 함께 출력하는 경우, 코드는 다음과 같다.
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소속팀: " + team.getName());
}
회원만 출력하는 경우, 코드는 다음과 같다.
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}
위 내용을 참고하여 프록시에 대한 간단한 예제를 들자.
try 문 안에 다음과 같이 작성하면 쿼리가 나간다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
다음과 같이 코드를 고치고 실행 결과를 보자.
Member findMember = em.find(Member.class, member.getId()); // 이 부분을
Member findMember = em.getReference(Member.class, member.getId()); // 이렇게
먼저, sout 두 줄을 주석처리 하고 실행하면 select 쿼리가 안나간다. 하지만 sout의 주석을 풀고 실행하면 select 쿼리가 나가고 sout의 결과도 출력된다. 조금 더 자세히 보면, id를 찾을 때는 select 쿼리가 안나간다. 왜냐하면 id를 찾을 때 파라미터로 member.getId()
를 직접 넣었기 때문에 DB에 접근하지 않아도 알 수 있다. 하지만 username을 출력할 때는 findMember.getUsername()
에 접근하는 순간 DB에 쿼리를 보낸다.
그렇다면 findMember의 정체는 무엇인지 findMember.getClass()
로 출력해보자.
Hibernate가 강제로 만든 가짜 클래스인 프록시인 것이다.
02. 프록시 기초와 특징
프록시 기초 : em.find() vs em.getReference()
em.find() 는 데이터베이스를 통해 실제 엔티티 객체를 조회한다.
em.getReference() 는 데이터베이스 조회를 미루는 가짜인 프록시 엔티티 객체를 조회한다.
프록시 특징
프록시는 실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같다.
이론상, 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
프록시 객체는 실제 객체의 참조인 target
을 보관한다. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
03. 프록시 객체의 초기화
프록시 객체의 초기화 과정
다음과 같은 코드를 실행하면
Member member = em.getReference(Member.class, “id1”);
member.getName();
내부적으로 다음과 같은 과정이 일어난다.
프록시 객체인 member를 가져와 getName을 호출하면 target에 값이 없음을 확인하고 JPA가 영속성 컨텍스트에 요청하여 영속성 컨텍스트가 DB를 조회하고 실제 Entity 객체를 생성하여 target에 진짜 객체와 연결해준다.
프록시 객체의 초기화 특징
1. 프록시 객체는 처음 사용할 때 한 번만 초기화하면 된다. 따라서 두 번째 요청부터는 프록시가 진짜 객체에 연결되어 있으므로 다시 DB에 접근하지 않아도 된다. 다음은 관련 try문 코드이다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.class = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
실행 결과를 확인해보면 다음과 같다. id는 직접 값을 넣어줬으므로 DB에 접근하지 않는 것을 확인할 수 있고, 처음 username을 출력할 때는 DB에 접근하여 select 쿼리문을 날리는 것을 볼 수 있다. 하지만 두번째에 username을 출력할 때는 이미 프록시가 진짜 객체를 가지고 있기 때문에 추가적인 DB 접근을 하지 않는 것을 볼 수 있다.
2. 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라, target
에 값이 채워지는 것이라고 생각하면 된다. 초기화가되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있는 것이다. 다음은 관련 try문 코드이다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("before | findMember.class = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("after | findMember.class = " + findMember.getClass());
tx.commit();
before와 after로 클래스를 찍어본 결과, 프록시가 초기화 되기 전, 후 클래스는 동일함을 볼 수 있다.
3. 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다. == 비교보다 instance of 사용해야 한다.
4. 영속성 컨텍스트에 찾는 엔티티가 이미 존재하면 em.getReference()
를 호출하여도 실제 엔티티를 반환한다. (JPA는 한 트랜잭션 안에서 같은 것은 보장을 해준다 a == a
를 보장한다고 생각하면 이해가 쉽다.) 다음은 관련 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
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("a == a : " + (m1 == reference));
tx.commit();
더 심화로, 둘 다 reference로 가져오면 같은 proxy를 반환한다. a==a와 같이 true여야 하기 때문이다. 다음은 관련 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member m1 = em.getReference(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("a == a : " + (m1 == reference));
tx.commit();
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. (하이버네이트는 org.hibernate.LazyInitializationException 이라는 초기화를 할 수 없다는 예외를 터트린다.) 다음은 관련 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy 객체
em.detach(refMember); // 영속성 컨텍스트에서 끄집어낸다면? - 준영속 상태
refMember.getUsername(); // 실제 DB에 쿼리 나가면서 Proxy 객체 초기화
tx.commit();
04. 프록시 확인
프록시와 관련된 정보를 확인하는 방법이 있다.
초기화 여부 확인과 관련된 내용이다. 다음은 프록시 객체를 초기화 하지 않은 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy 객체
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
tx.commit();
실행 결과는 당연히 false이다.
다음은 프록시 객체를 초기화 한 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy 객체
refMember.getUsername(); // Proxy 초기화
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
실행 결과로 true가 나오는 것을 볼 수 있다.
다음은 강제 초기화와 관련된 내용이다. 강제 초기화를 하는 코드이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy 객체
Hibernate.initialize(refMember); // 강제 초기화
쿼리가 나가는 것을 확인할 수 있다.
참고로, JPA 표준에는 강제 초기화가 없고 Hibernate에서 지원해 주는 것이다.
'JPA' 카테고리의 다른 글
영속성 전이(CASCADE)와 고아 객체 (0) | 2024.01.14 |
---|---|
즉시 로딩과 지연 로딩 (2) | 2024.01.12 |
@MappedSuperclass - 매핑 정보 상속 (0) | 2024.01.11 |
상속관계 매핑 (0) | 2024.01.08 |
다양한 연관관계 매핑 (0) | 2024.01.07 |