JPA 2 - Lazy/Eager Loading

JPA 공부 시작!

Ssafy study 자료 정리 + JPA 공부한 내용을 정리하기 위해 만든 tag.


Lazy Loading

자 저번 포스팅에서는 JPA가 등장한 배경, 그리고 과거에 비슷한 기술 중에 뭐가 있었는지 알아봤고, 각각이 뭔지 알아봤다. JPA의 는 LAZY fetching을 지원한다. 따라서 우리가 객체를 DB에서 꺼내올 때, ‘필요하지 않는 연관된 객체는 굳이 미리 안가져올 수 있다’. 과연 이러한 기술은 어떻게 구현되는 것일까? 오늘은 이러한 Lazy loading을 구현할 수 있게 해주는 proxy라는 기술에 대해 포스팅을 해 볼 것이다.


Proxy 를 왜 쓸까?

모든 공부는 ‘왜 쓸까’라는 것에서 시작된다. 그래서 우리는 proxy를 써야할 상황에 대해 먼저 생각해보겠다.

jpa2_1

사진 출처 : 인프런 김영한 강사님 - JPA 기본편1

@Getter @Setter
@Entity
class Member {
  @Id @Column(name = "member_id")
  Long id;
  String username;
  
  @OneToOne
  @JoinColumn(name = "team_id")
  Team team;
}

@Getter @Setter
@Entity
class Team {
  @Id @Column(name = "team_id")
  Long id;
  String name;
}

위 그림을 JPA를 사용해서 나타낸다면 위와 같이 설계될 수 있을 것이다. 그리고 ‘조회’ 하는 상황을 생각해보자.

상황 : member_id = 100 인 정보를 조회하려고 한다. 이 때에 team 에 대한 정보는 부득이하게도 필요가 없는 상황이다.

라는 상황을 가정해보자. team의 정보가 필요 없기 때문에 우리는 ‘굳이’ JOIN을 하여 team에 대한 정보를 가져올 필요가 없다. 왜?

조회하려는 Entity와 연관된 Entity들이 항상 사용되는 것은 아니기에

따라서 member에 대한 정보만 가져오는게 이득이라고 보인다. 이러한 상황을 JPA에서는 이러한 상황에서 DB의 조회를 지연시킬 수 있는데 그 기술을 Lazy loading이라고 한다. 그리고 Lazy loading기능을 사용하기 위해 실제 Entity 객체 대신에 DB조회를 지연할 수 있는 가짜 객체가 필요한데 이를 Proxy객체라고 부른다.

JPA는 LAZY Loading과 proxy 로 이러한 문제를 해결. Lazy loading이전에 Proxy에 대해 공부해야 하는 이유이다.


Proxy 기초

Proxy가 어떻게 구성되어 있는지 구조부터 살펴보자.

jpa2_3

jpa2_4

사진 출처 : 인프런 김영한 강사님 - JPA 기본편1


JPA에서는 EntityManager.find() method말고도 getReference()라는 method를 제공한다.

가장 이해하기 쉽게 풀어서 설명하자면, 연관된 정보를 가져오기는 싫은데 null값이 있으면 안되니깐… null대신 가짜(proxy)를 넣어놓자!! 라고 생각하면 될 것 같다.


여기서 들 수 있는 의문 1가지.

Proxy객체는 어떻게 만들어지는 걸까…?

생각해보면 Hibernate가 알아서 Proxy객체가 Entity class 상속받게 하고…한다는데 이게 가능한가..? field명 정도야 미리 만들어두면 그만이지만 run time에 동적으로 상속관계를 만들어 줄 수 있는가? 에 대한 의문이 들었다. 그리고 다음과 같은 기술을 알게 됐다.

Java Reflection : compile-time이 아닌, run-time에 동적으로 특정 클래스의 정보를 객체화를 통해 분석 및 추출해낼 수 있는 프로그래밍 기법

따라서 reflection이라는 기술을 사용하여 동적으로 자바코드를 만들 수 있는 것이다. Spring Data JPA도 해당 기술을 사용하기 때문에 우리는 그저 ‘interface를 알맞게 만들어서 구현체처럼 사용이 가능’하게 되는 것이다.


Proxy 특징


지금까지 Proxy에 대해 조금 알아봤다. Lazy Loading은 JPA에서 Entity에 대한 정보를 DB에서 가져올 때 연관된 Entity에 대하여 한번에 가져오는 것이 아닌, ‘필요할 때 해당 Entity를 가져오는 것’이다. Lazy loading에 대해서 이해하기 위해서는 proxy에 대해 알고 있어야 했기 때문에 공부를 한 것이다.


Eager vs Lazy Loading

Lazy Loading은 Proxy Design Pattern으로 구현이 된다. JPA에서는 실제 객체가 필요할 때까지 객체의 생성을 미룰 수 있게 해준다. 따라서 조회하려는 Entity와 연관된 Entity정보가 필요없을 때 Lazy loading을, 연관관계 Entity의 정보가 바로 필요하다면 Eager loading을 하면 된다.

자 말로만 하지 말고 실제로 한번 코드를 만들어서 실행시켜 보자.

@Entity // JPA 가 관리하는 class 임을 알려주는 역할
public class Member {
	@Id
	@Column(name = "member_id")
	private Long idx;

	@Column
	private String username;

	@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
	@JoinColumn(name = "team_id")
	private Team team;
}
@Entity
public class Team {
	@Id
	@Column(name = "team_id")
	private Long idx;

	@Column
	private String teamName;

	@OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>();
}

Memeber, Team 2개의 Entity에게 연관관계를 맺도록 했다. 1 : n의 관계를 갖고 있기에 FK는 n쪽인 Member가 가지게 되는 모습을 볼 수 있다. Member에서 team에 관한 field에 연관관계를 매핑하였고, fetchType으로는 LAZY로 주었다. 따라서 Member Entity를 조회해서 가져오더라도, team에 대한 정보를 한번에 가져오는게 아니게 된다. 실제로 확인해보자. 아래 코드는 여기서 확인할 수 있다.

import hellojpa.domain.Member;
import hellojpa.domain.Team;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

/**
 * Lazy Loading 으로 연관객체를 가져오면 Proxy 객체임을 알았음.
 * 따라서 이번엔 해당 Proxy 객체의 실제값에 접근할 때 query 가 발생하는지 확인할 것.
 */
public class LazyLoadingQueryCheck {
	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		EntityTransaction transaction = em.getTransaction(); // db connection 하나 얻기!
		transaction.begin();

		/**
		 * [이 코드를 실행시켰을 당시 DB table 에 저장된 데이터]
		 * Member
		 * ID | user| team_id
		 * 1L | KJJ | 1L
		 *
		 * Team
		 * ID | Team_name
		 * 1L | teamA
		 */
		try {
			Member findMember = em.find(Member.class, 1L); // Member 에 대한 select query 발생
			System.out.println("=== query check 1 ===");
			Team findMemberTeam = findMember.getTeam(); // query 안날라감. 실제 값에 접근 안했기 때문
			System.out.println("=== query check 2 ===");

			// 연관객체 Team 의 실제 값 team name 에 접근하려고 했기에 Proxy 객체는 초기화 과정이 일어남.
			// 따라서 이 때 team 에 대한 조회 query 가 발생
			System.out.println("findMemberTeam.getTeamName() = " + findMemberTeam.getTeamName());

			System.out.println("=== query check 3 ===");


/*		[출력 결과]
			Hibernate:
				select
					member0_.member_id as member_i1_0_0_,
					member0_.team_id as team_id3_0_0_,
					member0_.username as username2_0_0_
				from
					Member member0_
				where
					member0_.member_id=?
			=== query check 1 ===
			=== query check 2 ===
			Hibernate:
				select
					team0_.team_id as team_id1_1_0_,
					team0_.teamName as teamname2_1_0_
				from
					Team team0_
				where
					team0_.team_id=?
			findMemberTeam.getTeamName() = teamA
			=== query check 3 ===
*/
			transaction.commit();
		} catch (Exception e) {
			e.printStackTrace(); // 오류는 이 코드로 확인이 가능
			transaction.rollback();
		} finally {
			em.close();
		}

		emf.close();
	}
}

주석의 설명으로 정리가 됐다. 따라서,

LAZY loading으로 가져오면, 연관된 객체를 Proxy로 가져오는 것이다.


Lazy, Eager… 그러면 무조건 뭐가 더 좋은거냐?

인프런의 김영한 강사님께서는 Lazy loading이라고 한다. 그 근거는 만약 Eager loading을 쓴다면

  1. 예상하지 못한 SQL이 발생할 수 있다.

    복잡한 설계에서는 table들의 연관관계 또한 복잡하게 얽혀있기 때문에 Eager loading을 쓰게 되면 각각과 연관된 Entity들을 가져오기 위해 몇개의 query가 더 발생할지 모른다.

  2. JPQL에서 N + 1문제를 일으킨다

    • EntityManager.find() : JPA가 내부적으로 최적화 하여 어떤 Entity와 연관된 객체들의 fetchType이 Eager라면, Query가 한번에 나가서 연관된 객체 정보를 가져온다. 따라서 문제가 없다. 문제는 JPQL에서 나타난다.

    • EntityManager.createQuery("select * from Entity e", Entity.class)

      위 코드에서 SQL은 그대로 번역이 된다. 따라서 Entity에 대한 정보를 가져온다. 여기서 해당 가져온 Entity와 연관된 객체의 fetchType이 Eager로 되어 있다면, 다시한번 연관된 객체들을 가져오기 위한 Query가 발생하게 된다. 각각의 객체에 대한 query가 날라가기 때문에 N + 1문제가 발생되는 것이다.


N + 1 문제란…?

연관 관계가 설정된 Entity를 조회할 경우에 조회된 데이터 개수(N) 만큼 연관관계의 조회 query가 추가로 발생하여 data를 읽어오게 되는 문제.


아 그러면 Lazy loading으로 하면 되겠다!!

가 아니다. fetchType에 상관없이 N + 1문제는 발생할 수 있다. “Query를 가져오는 시점만 다르지,” 결국 추가적인 query가 발생한다는 문제는 발생한다는 것이다. Lazy loading에 있어서는 연관된 Entity에서 참조를 하려고 할 때 ‘추가적인 query가 발생’한다는 것이 문제가 발생했다는 것이다. 위에서 봤듯이 우리가 ‘언제든 필요하면 Team Proxy 객체의 실제 값에 접근’하여 query를 날려 실제 값을 가져올 수 있다. 그러나 이 또한 “추가적인 query의 발생”이고, 따라서 N + 1문제가 발생되는 것이다.

→ 여기서 Lazy의 경우에는 뭐가 문제지..? 라는 생각이 들 수 있다. 중요한 것은 ‘무조건 데이터가 다 필요한 상황에서 불필요하게 추가적인 query가 발생’한다는 점이다. 따라서 문제가 된다는 것.


아니, EAGER, LAZY 둘 다 N + 1 문제 일으키면 어떻게 하라는거냐?

라는 질문에 2가지로 답할 수 있음을 찾았다. 간단하게 이런게 있다고만 알아두고 향후에 더 깊이 다루도록 하겠다. 여기를 보면 fetch join에 대한 한계… 이런 것들이 있다.

  1. fetch join

    SQL join 종류가 아니다…

    Fetch Join이란 JPQL로 특정 엔티티를 조회할 때 ‘연관된 엔티티 혹은 컬렉션을 즉시 로딩과 같이 한 번에 함께 조회하는 기능’.

  2. Entity graph

    Entity 조회 시점에 연관된 Entity들을 함께 조회하는 기능


이번 포스팅에서는 Lazy, Eager loading에 대해 알아봤다.


Reference

proxy에 대해 - 인프런 김영한 강사님 JPA기본편1

Proxy

Java Reflection

reflection

N + 1 문제

N + 1, fetch join

Entity graph