본문 바로가기

Backend/JPA

[JPA] 프록시와 연관관계 관리(즉시로딩, 지연로딩)

728x90

개요

객체는 객체 그래프로 필드 객체들을 탐색할 수 있다. 하지만 DB에 저장되면 연관된 객체를 마음껏 탐색하기 어렵다.

JPA에서는 이를 해결하기 위해 프록시라는 기술을 사용한다.

 

프록시를 사용하면 처음부터 연관 객체를 조회하는 것이 아니라 실제 사용하는 시점에 조회할 수 있다. 하지만 자주 함께 사용되는 객체들은 처음부터 함께 조회해놓는 것이 효과적일 수 있다. 따라서 JPA는 위 두 방법 모두 지원한다.

 

위에서 말한 객체와 연관 객체의 관계를 JPA가 어떻게 풀었는지 살펴보자.

프록시

프록시는 "대리인"이라는 뜻을 가진다. 따라서 프록시 기술은 특정 작업을 대리인을 통해 실행하도록 하는 기술을 말한다.

JPA에서 프록시는 어떤 작업을 대신 실행해 줄까? Member와 Team 예시를 통해 대리인이 필요한 이유를 살펴보자.

Member.java

 

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

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

    // getter, setter
}

Team.java

@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

	// getter, setter
}

위 경우에 하나의 Member와 Team은 다대일의 연관 관계를 가진다.

Member를 조회할 때 Team의 정보까지 필요하지 않을 때를 생각해 보자. 로그인 기능 등 Team과는 무관한 Member 조회에 항상 Team까지 같이 조회된다면 비효율적일 것이다.

 

그렇다면 프록시 기술에서 특정 작업 중 하나가 객체를 로딩하는 일이 될 수 있다. 

프록시 기술을 이용해 불필요한 객체 로딩을 미루고, 필요한 시점에 조회할 수 있도록 하는 것이다.

 

프록시 기술을 이용하기 위해서 getReference() 메서드를 이용해 보자.

public static void proxy(EntityManager entityManager) {
        Member member = new Member("choi");
        entityManager.persist(member);

        entityManager.flush();
        entityManager.clear();

        Member findMember = entityManager.getReference(Member.class, member.getId());
        System.out.println(findMember.getClass());
 }

조회된 Member 객체의 클래스와 쿼리문을 보자.

persist() 메서드로 INSERT 쿼리가 날아갔고, SELECT 쿼리는 없다.

그리고 Member 객체가 아닌 Hibernate의 Proxy 객체인 것을 볼 수 있다.

 

실제 Member 객체의 값을 조회해 보자.

public static void proxy(EntityManager entityManager) {
        Member member = new Member("choi");
        entityManager.persist(member);

        entityManager.flush();
        entityManager.clear();

        Member findMember = entityManager.getReference(Member.class, member.getId());
        System.out.println(findMember.getClass());
        System.out.println(findMember.getName());
    }

위처럼 Member를 프록시 객체로 가지고 있다가 값을 조회할 때(실제 사용 시점에) SELECT 쿼리가 날아가는 것을 볼 수 있다.

실제로 프록시 객체는 해당 엔티티 객체를 상속한 객체다.

프록시 객체는 엔티티 객체의 참조를 보관해 getName() 등 실제로 엔티티가 사용될 때 DB를 조회해 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다. 

 

프록시 클래스의 코드는 아래와 같을 것이다.

class MemberProxy extends Member {
	
    Member target = null;
    
    public String getName() {
    	if(target == null) {
        	// 초기화 요청, DB 조회, 실제 엔티티 생성 및 참조 보관
            this.target = 조회한 엔티티;
        }
    }
    
    return target.getName();
}

위 코드 상황을 도식화해보자.

프록시 특징

프록시의 특징은 아래와 같다.

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화하면 실제 엔티티로 바뀌는 것이 아닌 프록시 객체를 통해 초기화 없이 실제 엔티티에 접근 가능해진다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체다.
    • 타입 체크 시에 주의해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.

즉시 로딩, 지연 로딩

도입부에 말했던 특정 객체의 연관 객체 접근 방식에는 두 가지가 있다.

  1. 연관 객체를 즉시 조회하는 방식
  2. 연관 객체가 필요한 시점에 조회하는 방식

1번 방식이 즉시 로딩(EAGER), 2번 방식이 지연 로딩(LAZY)이다. 구체적으로 두 로딩 방식에 대해 알아보자.

지연 로딩 방식

위 Member와 Team 관계에 지연 로딩을 적용해 보자.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
	
    // getter, setter
}

실제로 Member 조회가 아닌 member의 team을 사용하는 시점에 team이 조회되는지 살펴보자.

public static void proxy(EntityManager entityManager) {
        Member member = new Member("choi");
        entityManager.persist(member);

        Team team = new Team("team1");
        entityManager.persist(team);

        member.setTeam(team);
        entityManager.flush();
        entityManager.clear();

        Member findMember = entityManager.find(Member.class, member.getId());
        System.out.println(member.getName());
        System.out.println(findMember.getTeam().getName());
    }

결과가 의도한 대로 나온 것을 볼 수 있다. entity manager를 clear 하기 전 Member와 Team을 생성하고 두 객체 연결해 줬다.

그리고 결과를 나열해 보면 아래와 같다.

  1. Member 조회
  2. Member의 name 조회
  3. Team의 클래스 조회
    • Lazy 로딩을 사용했으므로 Team은 엔티티 객체가 아닌 프록시 객체
  4. Team의 name 조회
    • 실제 Team 사용 시점에 DB에서 Team 조회

즉시 로딩

다대일 연관관계에서 위처럼 fetchType을 따로 지정하지 않으면 즉시 로딩 방식으로 동작한다.

즉시 로딩 방식에선 위 과정이 어떻게 이뤄지는지 살펴보자.

Member.java

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    private Team team;
    
    // getter, setter
}

즉시 로딩으로 Member를 조회하게 되면, Member 조회 시점에 Team을 Join 해 엔티티 객체를 생성함으로써 Team 객체가 프록시가 아닌 실제 엔티티 객체가 되는 것을 볼 수 있다.

 

상황에 따라 Member만을 사용하는 로직이 많다면 지연 로딩을 사용하고, 대부분의 경우 Member와 Team을 같이 사용한다면 즉시 로딩을 사용하면 될 것 같다.

fetch type

@ManyToOne에서 따로 fetch type을 지정하지 않으면 즉시 로딩 방식으로 동작했다. fetch 속성의 기본값은 아래와 같다.

  • @ManyToOne, @OneToOne : 즉시 로딩
  • @OneToMany, @ManyToMany : 지연 로딩

마무리

모호하게 알고 있었던 JPA의 프록시와 로딩 방식이 조금 명확해 진 것 같다.

이전엔 로딩 방식을 특정 객체의 연관 객체에 접근하는 방식으로 이해하지 못하고, 암기하고 있어서 모호하다고 생각했던 것 같다.

 

잘못된 부분에 대한 지적이나 피드백은 환영입니다. 감사합니다!

참고

 

자바 ORM 표준 JPA 프로그래밍 - 예스24

자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고

www.yes24.com

 

728x90