[JPA] 프록시(Proxy) 기본

3Beom's 개발 블로그·2022년 12월 1일
1

SpringJPA

목록 보기
9/21
post-custom-banner

출처

본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 수강하며 기록한 필기 내용을 정리한 글입니다.

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


  • 본 글은 지연 로딩과 즉시 로딩 개념을 이해하기 위한 프록시의 기본 개념을 다루고 있다.

0. 예시 설정

  • 엔티티 두 개를 설정한다.
    • Member
    • Team
  • 두 엔티티는 다음과 같이 다대일 연관관계를 갖는다.
    • Member : Team = N : 1
  • 각 엔티티는 다음과 같이 구현되어 있다.

(GETTER, SETTER 생략)

< Member >

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "MEMBER_NAME")
    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

< Team >

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @Column(name = "TEAM_NAME")
    private String name;
}

1. 프록시의 필요성

  • 예시로 설정된 엔티티에서 다음과 같이 Member 객체를 조회할 경우, Team 객체도 함께 조회된다.
Team team = new Team();
team.setName("team1");
entityManager.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
entityManager.persist(member);

entityManager.flush();
entityManager.clear();
            
Member findMember = entityManager.find(Member.class, member.getId());

  • 즉, Member 객체만 조회해도 해당 Member 객체가 참조하고 있는 Team 객체까지 한번에 딸려오는 것이다.
  • Member와 Team 데이터를 함께 조회하는 경우가 많은 서비스의 경우에는 괜찮지만, 그렇지 않은 경우에는 매번 불필요한 쿼리가 전달되며, MEMBER 테이블 대상 조회 과정인데 TEAM 테이블에도 쿼리가 전달되면 개발 과정에서 의도치 않게 쿼리가 나갈 수 있다.
  • 따라서 다음과 같은 기능이 필요할 것이다.
    • Member 객체를 조회할 때는 MEMBER 테이블에게만 쿼리를 보낸다.
      -> MEMBER 테이블의 데이터만 조회하는 것이다.
    • 만약 개발 중 해당 Member 객체의 Team 객체 정보가 필요하면 그 때 TEAM 테이블로 쿼리를 보내서 받아온다.
  • 해당 기능을 프록시 객체로 구현할 수 있다.

2. 프록시 객체

  • 프록시 객체는 기본적으로 빈 껍데기 객체이며, EntityManager.getReference() 메소드를 통해 직접 생성할 수 있다.
Member findMember = entityManager.find(Memeber.class, member.getId()); // 실제 엔티티를 반환한다.
Member findMember = entityManager.getReference(Member.class, member.getId()); // 프록시 객체를 반환한다.
  • 위 코드와 같이 EntityManager.getReference() 메소드로 Member 클래스의 프록시 객체를 생성할 수 있다.
  • 프록시 객체를 활용하면 실제로 활용되기 전까지 DB 조회를 미룰 수 있다.
Member findMember = entityManager.getReference(Member.class, member.getId());
System.out.println("Class : " + findMember.getClass());
System.out.println("id : " + findMember.getId());
System.out.println("name : " + findMember.getName());

  • 결과 사진의 첫번째 줄 Class : class velogbasic.Member$HibernateProxy$rtp2fw8A을 통해 프록시 객체로 설정된 것을 확인할 수 있다.
  • 결과 사진을 통해 동작 과정을 확인할 수 있다.
    (1) Class와 id 값이 출력된다.
    (2) DB로 조회 쿼리가 나간다.
    (3) name 값이 출력된다.
  • 즉, EntityManger.getReference() 메소드로 생성된 Member 프록시 객체는 빈 껍데기로만 이루어져 있다가 실제로 name 이라는 필드 값이 활용될 때 DB를 조회한다.
    -> 해당 과정을 프록시 초기화라고 한다.

  • 프록시 객체는 위 사진과 같이 원본 엔티티를 상속받은 클래스의 객체이다.
    • 따라서 원본과 동일한 구조의 빈 껍데기 형태를 가질 수 있게 된다.
  • 프록시 객체 내에는 원본 엔티티를 참조하는 target이 존재한다.
  • 프록시 초기화 과정에서 target이 원본 엔티티를 참조하게 된다.
  • 위에서 name 필드 값 활용으로 인한 프록시 초기화는 다음과 같이 이루어진다.

  1. getName()이 호출된다.
  2. 프록시가 아직 초기화 되지 않은 상태이므로, 영속성 컨텍스트에 프록시 초기화 요청을 전달한다.
  3. 영속성 컨텍스트가 DB에 조회쿼리를 전달하여 데이터를 가져온다.
  4. 가져온 데이터로 실제 Member 엔티티를 생성한다.
  5. Member 프록시 객체의 target에 실제 Member 엔티티가 설정되고, 참조를 통해 Member 엔티티의 getName()이 호출된다.
    -> target.getName()
  • 그런데 여기서 의문점이 하나 생겼다.
    -> getClass()로 Class 정보를 가져오는건 그렇다 치고, getId()로 id 값을 가져올 때는 왜 프록시 초기화가 안되는가?
  • 본 의문점은 다음 글을 통해 알 수 있었다.
    -> JPA Hibernate 프록시 제대로 알고 쓰기

< 식별자를 조회할 때 프록시 초기화가 일어나지 않는 이유 >

  • 프록시 초기화는 AbstractLazyInitializer 클래스의 초기화 로직에 의해 이루어 진다.
@Override
public final Serializable getIdentifier() {
    if (isUninitialized() && isInitializeProxyWhenAccessingIdentifier() ) {
        initialize();
    }
    return id;
}
  • 초기화 로직은 다음과 같다.
    • isUninitialized() : 초기화 되었는지 확인
    • isInitializeProxyWhenAccessingIdentifier() : 식별자 접근 시, 프록시를 초기화 할 것인지에 대한 옵션 설정 값
      • hibernate.jpa.compliance.proxy 설정 값을 true로 해야함.
  • 위 로직에서 일반적으로는 if 문에 해당되지 않아 그냥 id 값이 반환된다.
  • 그렇다면 id 값은 언제 채워지는지 구현 코드를 찾아 좀 더 살펴보았다.
	/**
	 * Main constructor.
	 *
	 * @param entityName The name of the entity being proxied.
	 * @param id The identifier of the entity being proxied.
	 * @param session The session owning the proxy.
	 */
	protected AbstractLazyInitializer(String entityName, Object id, SharedSessionContractImplementor session) {
		this.entityName = entityName;
		this.id = id;
		// initialize other fields depending on session state
		if ( session == null ) {
			unsetSession();
		}
		else {
			setSession( session );
		}
	}
  • 위 코드를 통해 AbstractLazyInitializer 생성자에서 id 값을 저장해 두는 것을 확인할 수 있다.
  • 프록시가 빈 껍데기이지만, id 값은 간직하고 있는 듯 하다.

3. 프록시 특징

3-1. 기본 특징

  • 프록시 객체는 딱 한번만 초기화 된다.
    • 두번 세번 초기화 안된다.
    • 한번 초기화 되면 그 값을 두고두고 계속 쓴다.
  • 프록시 객체가 초기화 될 때, 프록시 객체가 실제 엔티티로 교체되는 것이 아니다.
    • 프록시 객체가 실제 엔티티로 바뀌는 것이 아닌, 참조(target)를 통해 실제 엔티티에 접근이 가능해 지는 것이다.
  • 준영속 상태일 때 프록시를 초기화하면 문제가 발생한다.
    • org.hibernate.LazyInitializationException 예외가 발생한다.

3-2. 타입 관련 특징

  • 프록시 객체는 원본 엔티티를 상속 받는다.
  • 따라서 타입 체크 시 주의해야 한다.

< 타입 특징 1. == 비교 안된다. instance of를 사용해야 한다. >

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // true
  • 위 코드는 m1과 m2 모두 find() 메소드가 활용되었으므로 둘 다 실제 엔티티 객체이며, 당연히 == 비교가 가능하다.
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false
  • 하지만 위 코드는 m2가 프록시 객체이므로, ==으로 비교할 경우 false가 나온다.
  • 따라서 다음과 같이 instanceof를 활용해야 한다.
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 : " + (m1 instanceof Member)); // true
System.out.println("m2 : " + (m2 instanceof Member)); // true
  • 보통은 타입 비교할 때 아래와 같이 함수에서 정의하게 되는 경우가 많은데, 매개변수로 전달되는 m1과 m2가 실제 엔티티 객체인지, 프록시 객체인지 알 수 없으므로 instanceof를 써야 한다.
private static void isSame(Member m1, Member m2) {
  System.out.println("m1 : " + (m1 instanceof Member));
  System.out.println("m2 : " + (m2 instanceof Member));
}

< 타입 특징 2. 같은 영속성 컨텍스트 내 동일성 보장으로 인한 특징 >

  • JPA는 다음 매커니즘이 무조건 보장되어져야 한다.

같은 영속성 컨텍스트 내에서 동일한 pk 값을 갖는 엔티티 객체는 == 비교를 했을 때 무조건 true가 반환되어야 한다.

  • 즉, 하나의 영속성 컨텍스트 내에서 같은 식별자를 갖는 엔티티 객체는 무조건 == 비교가 성립되어야 한다는 것이다.

    • 이는 곧 Class도 같아져야 함을 의미한다.
  • 다음 두 경우를 두고 비교해 보자.

    • 경우 1 : find() -> getReference()
    • 경우 2 : getReference() -> find()
  • 경우 1 : find() -> getReference()

Member findMember1 = entityManager.find(Member.class, member1.getId());
Member findMember2 = entityManager.getReference(Member.class, member1.getId());

System.out.println("findMember1 Class : " + findMember1.getClass());
System.out.println("findMember2 Class : " + findMember2.getClass());
System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2));
  • findMember1과 findMember2는 모두 같은 식별자의 데이터를 조회하고 있다.

  • findMember1에서 먼저 find() 메소드를 통해 조회가 이루어지고, 이는 실제 Member 엔티티로 findMember1에 담긴다.

    • 당연히 영속성 컨텍스트에 저장된다.
  • findMember2에서 getReference() 메소드에 의해 영속성 컨텍스트를 조회하게 되고, 실제 Member 엔티티를 그대로 받아 저장한다.

  • 따라서 두 객체 모두 실제 Member 엔티티 객체가 되며, 결과는 다음과 같다.

  • 경우 2 : getReference() -> find()

Member findMember1 = entityManager.getReference(Member.class, member1.getId());
Member findMember2 = entityManager.find(Member.class, member1.getId());

System.out.println("findMember1 Class : " + findMember1.getClass());
System.out.println("findMember2 Class : " + findMember2.getClass());
System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2));
  • 이 경우 역시 동일한 식별자의 데이터를 조회하고 있다.
  • findMember1에서 getReference() 메소드를 통해 프록시 객체가 담길 것이다.
    • Class 정보도 프록시 객체를 나타낼 것이다.
  • findMember2에서 find() 메소드를 통해 실제 엔티티가 생성될 것이다.
  • 원래 여기서 findMember2의 Class 정보는 실제 Member Class가 되어야 할 것이다.
  • 하지만 앞서 언급한 JPA 매커니즘에 따르면 findMember1과 findMember2의 Class 정보가 같아야 == 비교가 가능할 것이다.
  • 이로 인해 find() 메소드로 조회한 findMember2 역시 프록시 객체가 되어버린다.
  • 따라서 getReference() -> find() 과정은 둘 다 프록시 객체가 된다.

4. 프록시 초기화 여부 확인, 강제 초기화

4-1. 프록시 초기화 여부 확인

  • 프록시 초기화 여부를 확인할 수 있는 방법이 있다.
    -> EntityManagerFactor.getPersistenceUnitUtil().isLoaded()
Member findMember1 = entityManager.getReference(Member.class, member1.getId());

// false
System.out.println("is Loaded : " + entityManagerFactory.getPersistenceUnitUtil()
                    .isLoaded(findMember1));

findMember1.getName();

// true
System.out.println("is Loaded : " + entityManagerFactory.getPersistenceUnitUtil()
                    .isLoaded(findMember1));

4-2. 프록시 강제 초기화

  • 앞서 다루었듯, 실제로 활용하면 초기화 시킬 수 있다.
  • 하지만 Hibernate에서 제공하는 초기화 메소드도 있다.
    -> org.hibernate.Hibernate.initialize()
    (JPA 표준에서는 강제 초기화가 없다. Hibernate에서 제공해 주는 것!)
Member findMember1 = entityManager.getReference(Member.class, member1.getId());

// false
System.out.println("is Loaded : " + entityManagerFactory.getPersistenceUnitUtil()
                    .isLoaded(findMember1));

Hibernate.initialize(findMember1);

// true
System.out.println("is Loaded : " + entityManagerFactory.getPersistenceUnitUtil()
                    .isLoaded(findMember1));

즉시 로딩과 지연 로딩이 본 프록시 기능을 활용하여 구현된다.

profile
경험과 기록으로 성장하기
post-custom-banner

0개의 댓글