본문 바로가기
Back-end/JPA 개념

14. 프록시(Proxy)

by devraphy 2022. 4. 8.

0. 개요

- 이번 포스팅에서는 JPA Proxy에 대해서 알아보자.

- proxy는 JPA 개념 중에서도 어려운 개념에 속한다. 차근차근 배워보자. 

 

1. Proxy를 사용하는 이유 

a) 언제, 왜 사용할까?

- 예를 들어, Player와 Team 객체를 모두 조회하는 비즈니스 로직이 있다고 해보자.

- 경우에 따라 Player와 Team 객체가 모두가 필요할 때도, 두 객체 중 하나만 필요할 때도 있을 것이다.

 

- 그러나 이미 두 객체를 모두 조회하도록 비즈니스 로직을 선언하였기에, 매번 Player와 Team 객체를 모두 조회한다.

- 이처럼 필요하지 않은 객체에 대한 조회는 자원 낭비를 발생시킨다. 

 

- JPA는 이러한 상황을 해결하기 위해 지연 로딩을 사용하는데, 이때 Proxy라는 개념이 사용된다.

- 즉, 지연 로딩을 사용하려면 Proxy가 필요하다.

 

b) 지연 로딩(= lazy loading)

- 지연 로딩이란, 어떤 객체 A를 조회할 때 객체 A와 연관된 다른 객체를 필요할 때 조회(= 로딩)하는 것을 말한다.

- 무슨 말인지 이해가 잘 가지 않는다면 다음 예시를 살펴보자.

 

- 예를 들어, 인스타그램 댓글을 생각해보자. 

- 어떤 댓글에 대댓글이 달리면, "댓글 더보기"라는 버튼이 있다.

- "댓글 더보기" 버튼을 누르면 그때서야 추가적인 대댓글을 확인할 수 있다. 

 

- 즉, 댓글 객체를 조회할 때 해당 댓글 객체와 연관된 대댓글 객체를 함께 조회(=로딩)하는 것이 아니라

  필요한 시점에서 조회(= 로딩)하는 기능이 지연 로딩이다.

 

c) 즉시 로딩(= eager loading)

- 즉시 로딩은 지연 로딩과 반대되는 개념이다.

- 어떤 객체 A를 조회할 때, 이와 연관된 객체 B를 함께 조회(= 로딩)하는 것을 말한다.

- 다음 예시를 살펴보자.

 

- 동일하게 인스타그램 댓글을 생각해보자.

- 댓글의 대댓글을 보기 위해 "댓글 더보기" 버튼을 눌러 추가적인 대댓글을 조회하는 기능이 지연 로딩이라고 했다.

- 반면에 즉시 로딩의 경우, 애초에 "댓글 더보기" 버튼이 존재하지 않는다. 이미 모든 대댓글이 댓글과 함께 조회되기 때문이다. 

 

- 즉, 댓글 객체를 조회할 때 해당 댓글 객체와 연관된 대댓글 객체를 함께 조회(= 로딩)한다.

 

- 즉시 로딩의 경우, 함께 조회되는 연관 객체가 많을수록 더 많은 자원을 소모하게 된다. 

- 그러므로 조회의 목적에 따라 즉시 로딩 또는 지연 로딩을 적절하게 사용해야 한다.

 

2. EntityManager.getReference()

a) getReference()

- getReference() 메서드는 대표적인 Proxy 메서드 중 하나다.

- getReference()로 객체를 조회하는 경우, SELECT 쿼리 없이 객체가 조회된다.

- 다음 예시를 살펴보자.

 

- em.persist()를 통해 선수를 저장한 뒤, flush()와 clear()로 영속성 콘텍스트와 1차 캐시를 모두 비웠다.

- 그 후에 getReference() 메서드를 호출하여 player 객체를 조회하였다.

 

- 위의 코드를 실행하면 콘솔에서 다음과 같은 내용을 확인할 수 있다.

 

- 위의 INSERT 쿼리는 em.persist()에서 발생된 쿼리다.

- 그러나 SELECT 쿼리는 보이지 않고, 바로 reference 객체의 값이 출력된 것을 확인할 수 있다. 

 

- 이게 어떻게 가능할까?

- 위의 콘솔에 출력된 reference 객체의 값을 확인해보자.

 

- Hibernate에 의해 생성된 객체라는 것을 알 수 있다.

- 즉, JPA Proxy는 가짜 객체를 만들어 사용하므로 SELECT 쿼리 없이 조회가 가능한 것이다.

 

3. Proxy 객체의 특징 

a) Proxy 객체의 특징(1) - 가짜 객체

- em.getReference() 메서드는 DB를 조회하지 않고 가짜(Proxy) 객체를 만들어 반환한다.

- Proxy 객체는 조회 시 사용된 객체와 겉모습만 동일하고 내부는 텅 비어있는 객체다.

 

- 위에서 사용한 예시에서 reference 객체(= Proxy)는 다음과 같은 구조를 가지게 된다.

 

   → Entity target은 진짜 객체의 위치를 담고 있는 필드다.

   → player.getId()는 getReference()를 통해 조회할 때 사용된 값이다.

 

- 그렇다면 Proxy 객체는 어떻게 만들어지는 걸까?

 

b) Proxy 객체의 특징(2) - 생성

- Proxy 객체는 다음과 같이 조회되는 객체의 클래스를 상속받아 만들어진다.

 

- Proxy 객체는 진짜 객체의 클래스를 상속받기 때문에 겉모습이 진짜 객체와 동일하다.

- 이와 같은 이유로 사용자는 진짜 객체와 Proxy 객체를 구분하지 않고 사용하면 된다(이론상).

- 이것이 가능한 이유는 자바 특성상 상속받은 부모 클래스의 타입을 사용하면 되기 때문이다.

 

c) Proxy 객체의 특징(3) - 위임(delegate)

- 위에서 언급했듯이, Proxy 객체는 target이라는 참조를 보관한다.

- target은 진짜 객체를 가리키는 역할을 한다.

- 그러므로 Proxy 객체를 이용하여 getId()를 호출하면 진짜 객체(= player)의 getId() 메서드를 호출한다.

 

- 그러나 최초에 Proxy 객체의 target 값은 null이다. 진짜 객체가 DB에서 조회된 적이 없기 때문이다. 

- 이와 같은 이유로 Proxy 객체를 선언할 때에는 반드시 초기화를 진행해야 한다.

 

d) Proxy 객체의 특징(4) - 초기화

- 위에 예시에서 다음과 같은 코드를 통해 Proxy 객체를 선언하였다.

- 만약 Proxy 객체인 reference를 사용해 getId()를 호출하면 어떤 과정을 거칠까?

Player reference = em.getReference(player.getClass(), player.getId());
System.out.println("player ID =" + reference.getId());

 

- 위의 그림을 살펴보자. Proxy 객체는 다음 과정을 통해서 값을 가져온다. 

 

  → 1. client에서 Proxy 객체를 이용하여 진짜 객체의 값을 호출한다.

  → 2. Proxy 객체는 영속성 Context를 통해 target 값의 초기화를 요청한다.

  → 3. 영속성 Context는 진짜 객체의 값을 얻기 위해서 DB를 조회한다.

  → 4. DB 조회 결과를 이용하여 진짜 객체를 생성한다.

  → 5. target에 진짜 객체의 값을 매핑하고, client에서 요청한 메서드를 통해 진짜 객체의 값을 호출한다. 

 

4. Proxy 사용 시 주의사항

1. Proxy 객체는 최초에 사용할 때 한 번만 초기화된다.

 

2. Proxy 객체를 초기화할 때, 진짜 객체로 바뀌는 것이 아니라 진짜 객체에 접근하여 값을 가져온다.

 

3. Proxy 객체는 진짜 객체의 클래스를 상속받는다. 그러므로 타입 체크 시 주의해야 한다.

   → Proxy 객체를 이용한 타입 체크 시, instanceOf()를 사용한다. 

   → 비교 연산자(==)를 사용하면 객체의 메모리 주소 값을 비교하므로 올바른 비교를 할 수 없다.

   → 왜냐면 client는 사용하는 객체가 Proxy 객체인지, 진짜 객체인지 매번 확인할 수 없기 때문이다.

 

4. 조회하는 객체가 영속성 Context에 존재하는 경우, em.getReference()는 진짜 객체를 반환한다.

   → 이미 1차 캐시에 존재하는 객체를 굳이 Proxy 객체로 사용할 이유가 없다.

   → 그러나 진짜 이유는 JPA의 영속성 Context는 트랜잭션 단위로 동작하기 때문이다.

   → JPA는 하나의 트랜잭션에서 같은 타입의 객체를 여러 개 호출하더라도 동일한 객체로 판단한다. 

 

5. em.find()로 객체를 조회하더라도 Proxy 객체가 반환될 수 있다.(4번의 반대 상황) 

 

- JPA는 하나의 트랜잭션 내부에서 조회되는 같은 타입의 객체를 동일하게 취급한다.

- 위의 코드에서는 Proxy 객체가 영속성 Context 내부에 존재하는 상태다.

- 위의 코드에서 Proxy 객체(= reference)는 Player 클래스를 상속받는다.

- reference 객체의 상위 타입은 Player이므로, Proxy 객체 조회 후 진짜 Player 객체를 조회하더라도 

  JPA는 동일한 타입의 객체로 인식하여 위의 콘솔 출력문처럼 em.find() 메서드에서 Proxy 객체를 반환한다. 

 

- 여기서 핵심은 Client가 사용하는 메서드를 통해 객체의 구분이 불가하다는 것이다.

- 그러므로 위에서 언급한 바와 같이, Proxy 객체와 진짜 객체의 구분 없이 사용해야 한다.

 

6. 준영속 상태일 때, Proxy 객체를 초기화하면 에러가 발생한다.

 

- 위의 예시 코드를 실행하면 다음과 같은 결과를 확인할 수 있다.

 

- Proxy 객체를 최초에 초기화하는 경우, 영속성 Context를 거쳐 DB를 조회한다고 위에서 언급하였다.

- 그러나 위의 예시 코드에서는 Proxy 객체를 할당받은 reference가 detach() 메서드를 통해 준영속 상태가 되었다.

 

- 즉, 더 이상 영속성 Context의 관리를 받지 않는 Proxy 객체를 이용하여 DB를 접근하려는 것이다.

- 이와 같은 경우, LazyInitializationException이 발생하며 could not initialize proxy라는 메시지를 확인할 수 있다.

 

- 해당 예시의 핵심은 Proxy 객체의 초기화 시 영속성 Context를 반드시 거친다는 것을 알 수 있다. 

 

5. 다양한 Proxy 메서드 

- Proxy 객체의 상태를 확인하는 다양한 메서드를 알아보자.

a) Proxy 객체의 초기화 여부 확인

EntityManagerFactory.getPersistenceUnitUtil().isLoaded(Object entity); // boolean 반환

// ex) emf.getPersistenceUnitUnil().isLoaded(reference);

 

b) Proxy 객체의 클래스 확인

entity.getClass();
entity.getClass().getName();

// ex) reference.getClass();
// ex) reference.getClass().getName();

 

c) Proxy 강제 초기화

org.hibernate.Hibernate.initialize(entity);

// ex) Hibernate.initialize(reference);

- 위에서 사용한 initialize() 메서드는 Hibernate에서 지원하는 강제 초기화 메서드다.

- JPA 표준에는 강제 초기화 메서드는 존재하지 않는다.

- 그러므로 JPA 표준에서는 다음과 같이 Proxy 객체를 이용한 조회를 통해 강제 초기화를 진행한다.

reference.getId(); // 이런식으로 조회를 통해 강제 초기화를 한다.

 

6. em.getReference()를 자주 사용할까?

- 사실 em.getReference()를 호출하여 Proxy 객체를 구현하는 방법은 잘 사용되지 않는다.

- 다만, Proxy에 대한 기본적인 이해가 바탕에 있어야 다음에 설명할 지연 로딩에 대해 이해할 수 있다.

- 다음 포스팅에서는 Proxy를 이용한 지연 로딩에 대해서 알아보자. 

'Back-end > JPA 개념' 카테고리의 다른 글

16. 영속성 전이(Cascade)  (0) 2022.04.12
15. 지연 로딩 & 즉시 로딩(N + 1 문제)  (0) 2022.04.11
13. @MappedSuperClass  (0) 2022.04.07
12. 상속 관계 매핑  (0) 2022.04.06
11. 연관 관계의 종류(일대일, 다대다)  (0) 2022.04.05

댓글