본문 바로가기

Spring/JPA

프록시 (Proxy)

프록시는 엔티티 객체를 흉내 낸 가짜 객체입니다. 프록시 덕분에 실제 엔티티를 조회하는 시점을 미룰 수 있습니다.

그렇다면, 실제 엔티티를 조회하는 시점을 미뤄야 상황은 언제일까요?

 

Member와 Team 엔티티를 예로 들어보겠습니다. Member와 Team 엔티티는 일대다 단방향 연관관계로 연결되어 있습니다. 이때 Member 엔티티를 조회하면 Team 엔티티도 당연히 조회되어야 할까요?

 

그렇지 않습니다. 회원과 팀 엔티티를 함께 필요로 하는 경우라면 두 엔티티를 한 번에 가져오는 게 좋겠지만, Member 엔티티를 조회할 때 항상 Team 엔티티가 조회된다면 회원 엔티티만 필요할 경우에는 불필요한 쿼리가 나가며 성능 측면에서 불리하게 작용합니다. 

 

따라서 우리는 연관관계가 걸려있는 엔티티끼라도 하나의 엔티티를 조회했을 때 해당 엔티티만 조회하도록 해야 합니다. 이때 등장하는 개념이 지연 로딩(LAZY)입니다. 지연 로딩을 이해하기 전에 지연 로딩에 사용되는 프록시를 먼저 살펴볼 예정입니다.

 

프록시 (Froxy)

프록시는 em.getReference() 함수를 통해 반환받을 수 있습니다. 

em.find()
em.getReference()

 

평범하게 find 함수를 사용하면 데이터베이스를 통해 실제 엔티티 객체를 조회하지만 getReference 함수를 사용하면 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회합니다. 따라서 처음 em.getReference() 호출하면 아래 그림처럼 실제 객체와 껍데기만 똑같고 안은 상태인 프록시 객체를 얻습니다.

 

프록시 객체를 처음 얻으면 프록시의 target 필드가 null 설정되어 있습니다. 프록시의 target 필드를 통해 진짜 엔티티의 주소 값을 저장하고 진짜 엔티티에 접근하게 됩니다. 처음 반환된 프록시의 경우 실제 엔티티를 생성하기 단계이기 때문에 target 필드가 null 자연스럽습니다. target 필드는 프록시 객체가 초기화될 값이 설정됩니다.

 

프록시는 위 그림과 같이 실제 클래스를 상속받아서 만들어집니다. 때문에 실제 클래스와 겉모양이 같습니다. 객체를 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하게 됩니다. 또한 프록시 객체는 target 필드를 통해 실제 객체의 참조를 보관합니다.

 

프록시 객체의 타입 체크

프록시 객체는 원본 엔티티를 상속받지만 프록시가 아닌 실제 엔티티와 프록시를 == 연산자로 타입 체크할 경우 항상 false 반환됩니다. JPA에서 타입을 비교할 때는 == 연산자 대신 instance of 연산자를 사용합시다. (굉장히 중요합니다.)

prviate static boolean logic (Member m1, Member m2) {
    return m1.getClass() == m2.getClass();
}

 

위의 logic 함수처럼 == 연산자로 타입을 체크를 경우 실제 Member 타입끼리는 항상 참이 나오지만, 프록시 객체나 넘어올 경우 같은 타입이라도 거짓이 나오는 경우가 발생하기 때문에 아래 코드와 같이 instance of 구문을 사용하여 타입을 체크해야 합니다.

prviate static boolean logic (Member m1, Member m2) {
    return m1 instanceof Member && m2 instanceof Member;
}

 

프록시 객체의 초기화 

 

프록시 객체의 getName 호출하면 프록시 객체는 target 있는 실제 엔티티 객체의 getName 대신 호출합니다. 프록시의 target 프록시 객체가 초기화되는 과정에서 실제 엔티티의 참조 값이 들어갑니다.

 

getReference 함수로 반환받은 member 변수에는 target이 null인 프록시 객체가 저장됩니다. member 변수의 getName 함수가 호출되며 프록시 객체가 초기화됩니다.

 

프록시 객체에 getName 함수가 호출되며 프록시 객체의 target이 null인지 확인합니다. target이 null이라면 영속성 컨텍스트에 진짜 Member 객체를 요구(초기화 요청)합니다. 그럼 영속성 컨텍스트는 DB에 실제 엔티티를 조회하여 반환합니다. 이때 반환되는 실제 Member 엔티티 객체의 주소 값을 프록시 객체의 target 필드에 저장합니다. 이후에 프록시 target을 통해 실제 Member 엔티티의 getName 함수를 호출합니다.

 

프록시 객체가 한 번 초기화되면 프록시의 target에 참조가 저장되어 다음부터는 실제 엔티티 객체를 영속성 컨텍스트에 요구하지 않습니다.

 

정리하면, 프록시 객체는 처음 사용할 때 한 번만 초기화됩니다. 또한 프록시 객체를 초기화할 때는 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 프록시 객체의 target 필드로 실제 엔티티에 접근할 수 있게 됩니다.

 

프록시 사용 주의할 지점

Member refMember = em.getReference(Member.class, member1.getId());
em.detach(refMember);
refMember.getName();	// LazyInitializationException 예외 발생

 

먼저 위 코드처럼 영속성 컨테스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 하이버네이트의 경우 org.hibernate.LazyInitializationException 예외가 발생합니다. 굉장히 자주 발생하는 예외로 프록시가 준영속 상태일 때는 초기화를 진행하면 안 됩니다.

 

번째로 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 em.getReference() 호출해도 프록시가 아닌 실제 엔티티가 반환됩니다. 이미 엔티티를 영속성 컨텍스트에 보관하고 있는 상황이라면, 프록시를 굳이 사용할 필요가 없어집니다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member1.getId());

 

따라서 위 코드의 m1과 reference는 같은 클래스 타입을 가지게 됩니다. 둘 다 실제 엔티티 클래스를 얻습니다. 

 

JPA는 한 트랜잭션 안에서 == 연산을 true로 보장해야 합니다. 따라서 같은 트랜잭션이라면 m1과 reference의 == 비교가 항상 true 이어야 합니다. 따라서 m1 == reference를 true로 보장하기 위해서 reference를 프록시가 아닌 실제 엔티티로 반환합니다. 반대로 한 트랜잭션 안에서 프록시 객체를 먼저 getReference 했다면 == 를 보장하기 위해서 find 함수의 반환을 프록시로 하게 됩니다.

 

핵심은 find 함수의 반환이 프록시이던 아니던 개발에 문제가 되지 않는 것입니다.

 

프록시 확인 방법

프록시 객체와 함께 사용할 있는 유틸리티 함수를 마지막으로 살펴봅시다.

// 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
// 프록시 클래스 확인 방법, 출력
entity.getClass().getName()
// 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
// 참고: JPA 표준은 강제 초기화 없음
강제 호출: member.getName()

 

그렇다면 getReference 함수를 사용해 프록시를 직접 반환받아서 사용할 일이 많을까요? 

프록시를 직접 사용할 일은 없습니다. 하지만 프록시 메커니즘을 이해해야 즉시 로딩과 지연 로딩을 이해할 있습니다.

'Spring > JPA' 카테고리의 다른 글

CASCADE (영속성 전이)  (2) 2024.10.21
즉시 로딩과 지연 로딩  (0) 2024.10.21
@MappedSuperclass  (0) 2024.10.17
상속관계 매핑  (2) 2024.10.17
일대일 연관관계 매핑  (1) 2024.10.17