영원히 남는 기록, 재밌게 쓰자

프록시 본문

springboot/JPA

프록시

youngjae-kim 2024. 2. 27. 09:02
728x90
반응형

프록시를 왜 사용해야할까?

예제를 통해 알아보자

멤버 하나를 조회할 때 팀의 정보도 데이터 베이스로 부터 한번에 조회를 해야할까? 그럴 경우가 있을 수 있다. 또는 반대로 멤버를 조회할 때에 멤버만 조회하고 싶은 경우가 있다.

프록시 초기화는 처음에는 아무것도 없는 빈 껍데기 프록시 객체만 가지고 있다. 타겟이라는 변수를 초기화 시킬 때 영속성 컨텍스트에 진짜 엔티티 객체를 요청하는 동작을 거친다.

꼭 필요한 데이터를 적절한 때에 가져오기 위해서 하이버네이트에서는 프록시 객체를 사용하는 기능을 제공한다.

 

em.find()와 em.getReference() (em: 엔티티 매니저)

em.find() → 실제 엔티티 조회

em.getReference() → 프록시 가자 객체를 조회 (DB를 거치지 않아 빈껍데기만 있다.)

이런 프록시들은 특징이 있다.

  • 실제 클래스(엔티티)를 상속 받아서 만들어진다.
  • 실제 클래스와 겉모양이 같다.
  • 그래서 사용하는 입장에서 이론상으로는 실제 객체인지 프록시 객체인지 구분하지 않고 사용할 수 있다. (실제로는 구분해야 한다)

프록시 객체는 target이라는 참조 값이 실제 객체의 참조 값을 보관한다.

 

그래서 실제 객체를 호출하는 것 처럼 프록시 객체의 메서드를 호출하게 되면 그 때 실제 객체의 참조된 값을 통해 실제 객체의 메서드를 호출한다.

 

프록시 객체 초기화 동작 원리

  1. 가정: 회원 엔티티가 프록시 객체로 트랜잭션에 올라와있음. 이 회원 객체에서 회원 이름을 조회하는 getName()을 호출
  2. 회원 객체는 프록시 객체이기 때문에 빈 껍데기이다.
  3. 하지만 실제 엔티티 객체를 상속한 객체이기 때문에 메서드를 호출하면 그제서야 영속성 컨텍스트를 뒤지고 없으면 DB를 통해 찾아온다.
  4. 그리고 영속성 컨텍스트에 실제 엔티티 클래스를 불러오고 프록시 객체의 target은 실제 엔티티의 getName()의 반환 값을 가져와서 반환을 한다.

영속성 컨텍스트를 통해서 초기화를 요청한 뒤에는 초기화 된 프록시 객체는 실제 엔티티 값이 걸려 있다. 타겟에 값이 걸린 뒤부터는 getName()등을 요청해도 이미 초기화가 되어 있으므로 바로 프록시 객체에서 부른다.

그리고 프록시 객체의 매커니즘은 위 동작 원리가 JPA의 표준은 아니다.
JPA를 구현한 라이브러리가 구현하기 나름이다는 것을 알아두자.

프록시 객체의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
    • 여러번 호출해도 한번 불러온 객체를 사용한다.
  • 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기에 프록시 객체를 통해서 실제 엔티티로 접근한다.
    • 한번 프록시 객체로 호출되면 그 프록시 객체는 실제 엔티티로 교체되지는 않는다.
  • 이렇듯 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크 시 주의해야 한다. (==비교 대신 instance of 사용하기) 중요.
    • 프록시를 쓸지 안쓸지 모르기 때문
    • 웬만하면 == 비교 지양하자
  • 영속성 컨텍스트에 찾는 엔티티가 이미 1차 캐시에 있으면 getReference()를 호출 하여도 실제 엔티티를 반환한다.
    • 하나의 트랜잭션 안에서 1차캐시로 가져오는것을 생각해보자?
  • 영속성 컨텍스트 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. (실무에서 진짜 많이 만날 수 있다고 한다.)
    • 하이버네이트는 LazyInitializationException 예외를 터트린다.

 

1차 캐시에 엔티티나 프록시 객체가 이미 있을 때 호출 및 동일 객체 비교

Member member = new Member();
member.setName("hello");
em.persist(member);

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

// find로 먼저 찾음 -> 실제 엔티티 반환
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());

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

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

 

실제 엔티티를 먼저 조회했을 때 결과

  • findMember : find()를 통해 조회한 객체 (실제 객체)
  • refMember : getReference()로 조회

하지만 한 트랜잭션에서 영속성 컨텍스트의 1차 캐시의 라이프 사이클 안에서 동작하기 때문에 flush(), clear() 이후 처음 조회한 실제 엔티티 객체가 1차 캐시에 올라가 있어 “== 비교 시 실제 객체로 같다”

반대 상황 역시 getReference() 로 먼저 조회한 프록시 객체가 1차 캐시로 올라가기 때문에 find()로 조회하여도 둘 다 프록시 객체로 찍히는 것을 확인할 수 있었다.

프록시 객체를 먼저 호출 했을 때의 결과

프록시 객체라 처음엔 빈 값이라 getClass() 호출 시 System 출력이 먼저 찍히는 것을 확인할 수 있다.

 

프록시 확인

프록시 객체를 확인하고 조회를 도와주는 메서드들이 있다.

  • 프록시 인스턴스 초기화 여부 확인
    • 엔티티 매니저 팩토리로 부터 PersistenceUtil.isLoaded(엔티티)
  • 그냥 getClass()등으로 강제로 찍어보면서 확인

 

프록시와 즉시 로딩 사용시 주의할 점

  • 가급적 지연 로딩만 사용하자
    • 모든 연관된 객체의 테이블이 엮여서 조인 쿼리가 나가게 된다. 만약 10개가 맞물려 있다면 조인이 10개가 된 조원 쿼리가 날아간다.
  • 즉시 로딩은 JPQL 에서 N + 1 문제를 일으킨다.
    • fetch 조인을 활용하자
  • @ManyToOne, @OneToOne은 기본이 즉시로딩임
    • 로딩 전략을 LAZY로 설정하자!

참고

 

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

회사땜에 매일 바쁜 와중에 학원이라도 다닐까 생각했는데 마침 JPA 강의가 생겨서 꿀 타이밍이네요. 저는 이 전에 JPA 책을 보고 공부 했었는데요 궁금했던 점, 업무에 적용하며 고민하고 해결하

www.inflearn.com

 

728x90
반응형