본문 바로가기
Framework and Tool/JPA

JPA - 즉시 로딩과 지연 로딩

by ocwokocw 2021. 7. 15.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 즉시 로딩과 지연 로딩

이전에 살펴본 프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.

 

Member member = em.find(Member.class, 2L);
Team team = member.getTeam();
System.out.println(team.getName());

 

위와 같이 회원이 팀에 소속되어 있다고 가정해보자. 이때 회원 엔티티 조회시 팀까지 한꺼번에 하는게 좋을까? 아니면 팀 엔티티를 실제 사용시할때까지 조회를 미루는것이 좋을까? JPA 는 이 두 가지 방식을 모두 지원한다.

  • 즉시 로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. @ManyToOne(fetch = FetchType.EAGER) 로 설정할 수 있다.
  • 지연 로딩: 연관 엔티티를 실제 사용할 때 조회한다. @ManyToOne(fetch = FetchType.LAZY) 로 설정할 수 있다.

- 즉시 로딩

위에서 살펴본것과 같이 즉시 로딩을 사용하려면 FetchType 을 EAGER 로 설정한다.

 

@Entity
public class Member {

	@Id @GeneratedValue
	@Column(name = "USER_ID")
	private Long id;
	
	private String userName;
	
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
	private Team team;

 

아래와 같이 회원을 Team 에 종속시키고 find 로 회원을 검색해보자.

 

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Team team = new Team();
	
	team.setName("Team#1");
	em.persist(team);
	
	Member member = new Member();
	
	member.setUserName("UesrName#1");
	member.setTeam(team);
	em.persist(member);
	
	tx.commit();
	em.close();
}

public static void setRelationship(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Member member = em.find(Member.class, 2L);
	Team team = member.getTeam();
	
	tx.commit();
	em.close();
}

 

즉시로딩으로 설정하면 team 까지 한꺼번에 조회 한다. 이때 각 테이블을 1 번씩 2 번 조회하는게 아니라 성능 최적화를 위해 join 을 이용하여 한꺼번에 조회한다.

 

Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_,
        team1_.team_id as team_id1_6_1_,
        team1_.name as name2_6_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.user_id=?

 

@ManyToOne 어노테이션의 속성중에는 optional 이라는 속성이 있다. 이름만 보면 헷갈릴 수 있는데 외래키에 대한 nullable 허용 여부라고 생각하면 된다.

 

@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
private Team team;

 

앞에서 위 속성을 사용하지 않았어도 잘 조회했는데 굳이 사용해줘야 하는 이유가 있을까? 이 속성을 false (외래키 null 미허용) 로 설정하고 프로그램을 실행해보자.

 

Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_,
        team1_.team_id as team_id1_6_1_,
        team1_.name as name2_6_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        member0_.user_id=?

 

SQL 문이 left outer join 에서 inner join 으로 변경되었다. 외래키가 항상 있다는 것을 보장하기 때문에 inner join 으로 조회하여도 회원이 존재하면 반드시 팀에 종속되기 때문에 검색되지 않는다면 회원이 없음을 보장할 수 있게 된다. 

 

또한 일반적으로 inner join 은 left outer join 보다 성능이 더 좋다. 물론 이말이 절대적인것은 아니지만 대부분 inner join 의 on 조건은 index 를 걸어서 사용하는점과 테이블의 행수가 아주 적을때만 left outer join 이 빠를 경우는 극히 예외적인 경우이므로 일반적으로 성능이 올라간다고 이해하면 된다.

 

save 를 할 때 Team 을 설정하지 않도 그냥 Member 만 영속시키면 위의 SQL 문은 의도한 제약사항을 벗어나게 된다. 이런 상황을 피하기 위해 JPA는 optional 을 false 로 설정한 상태에서 팀을 설정하지 않은 회원을 영속시키려고 하면 Exception 을 발생시킨다.

 

Exception in thread "main" javax.persistence.PersistenceException: org.hibernate.PropertyValueException: not-null property references a null or transient value : com.example.demo.test1.Member.team
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:726)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:706)
	at com.example.demo.JpaApplication.save(JpaApplication.java:45)
	at com.example.demo.JpaApplication.main(JpaApplication.java:23)
Caused by: org.hibernate.PropertyValueException: not-null property references a null or transient value : com.example.demo.test1.Member.team
	at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:111)
	at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:55)

- 지연 로딩

지연 로딩을 사용하려면 FetchType 을 Lazy 로 설정하면 된다.

 

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
private Team team;

 

그리고 아래와 같이 테스트 코드를 작성해보자.

 

public static void setRelationship(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Member member = em.find(Member.class, 2L);
	Team team = member.getTeam();
	System.out.println("Load team reference.");
	
	System.out.println("team.getName: " + team.getName());
	
	tx.commit();
	em.close();
}

 

실행해보면 아래와 같이 결과가 나온다.

 

Hibernate: 
    select
        member0_.user_id as user_id1_3_0_,
        member0_.team_id as team_id3_3_0_,
        member0_.user_name as user_nam2_3_0_ 
    from
        member member0_ 
    where
        member0_.user_id=?
Load team reference.
Hibernate: 
    select
        team0_.team_id as team_id1_6_0_,
        team0_.name as name2_6_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
team.getName: Team#1

 

단순히 Team 엔티티의 레퍼런스를 참조하는것만으로는 Team 을 조회하는 SQL 을 수행하지 않는다. Team 의 엔티티를 실제 사용하는 getName() 을 호출해야 SQL 이 수행된다.

 

@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
private Team team;

.........

Member member = em.find(Member.class, 2L);
Team team = member.getTeam();
System.out.println("Team class eager: " + team.getClass());

.........

Team class eager: class com.example.demo.test1.Team

 

EAGER 로 설정하고 getClass 를 print 해보면 우리가 정의한 Team class 의 이름이 나온다.

 

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
private Team team;

.........

Member member = em.find(Member.class, 2L);
Team team = member.getTeam();
System.out.println("Team class lazy: " + team.getClass());

.........

Team class lazy: class com.example.demo.test1.Team$HibernateProxy$2ztWRR6r

 

LAZY 로 설정하고 getClass 를 print 해보면 class 이름이 뭔가 달라진다. 지연 로딩을 사용하면 곧바로 실제 엔티티가 아니라 프록시 객체가 반환된다.


- 즉시 로딩과 지연 로딩

연관된 엔티티를 처음부터 모두 영속성 컨텍스트에 올리는것은 비효율적이다. 그렇다고 해서 필요할 때마다 SQL 을 실행하는 지연로딩만 사용하는것도 최적화 관점에서는 좋지 않다. 비즈니스 로직에 따라 즉시 로딩을 사용할 때의 이점이 있고 지연 로딩을 사용할 때가 더 좋을때도 있다. 어쨌든 두 로딩 전략중 하나를 선택해야 한다.

 

지연 로딩은 엔티리를 프록시로 조회하여 실제 사용할 때 데이터베이스를 조회한다. 반면 즉시 로딩은 연관된 엔티티를 즉시 조회하여 가능하면 SQL 조인을 사용해서 한 번에 조회한다.

'Framework and Tool > JPA' 카테고리의 다른 글

JPA - 영속성 전이와 고아 객체  (0) 2021.07.17
JPA - 지연 로딩  (0) 2021.07.16
JPA - 프록시  (0) 2021.07.14
JPA - 고급맵핑 - 요구사항 분석과 맵핑3  (0) 2021.07.13
JPA - 고급맵핑 - Multi 테이블 맵핑  (0) 2021.07.12

댓글