본문 바로가기
Spring

Spring JPA / 즉시 로딩과 지연 로딩 (with N+1 문제, 프록시 객체)

by yang sing 2023. 7. 31.

안녕하세요. 이번 포스팅에서는 JPA의 다양한 연관관계에서 데이터를 가져오는 두가지 방법인 지연 로딩과 즉시 로딩에 대한 개념을 작성을 하도록 하겠습니다. 

프로젝트를 진행을 하다보면 두 테이블 또는 여러 테이블들을 조인하는 구조가 발생을 하게 되며 JPA로 구현을 할 때 앞서 포스팅한 여러 연관관계를 사용하여 데이터를 가져와야하는 경우가 많이 생기게 됩니다. 이렇게 연관관계에 있는 엔티티들을 조회할 때 두 테이블을 조인해서 한번에 데이터를 모두 가져오는 방법과 연관관계에 있는 엔티티에 접근을 할 때 조회를 하는 방법 두가지 옵션이 있습니다.

 

예를 들어 A라는 부모 개념인 엔티티와 B라는 자식 개념의 엔티티가 존재한다고 했을 때 A 엔티티의 특정 시퀀스로 데이터를 조회할 때 연관관계에 있는 B 엔티티의 데이터까지 한번에 조회를 하냐와 A 엔티티를 조회하고 A 엔티티에 연관관계에 있는 B 엔티티에 접근을 할 때 B 테이블을 조회해서 데이터를 가져오는 방식이 있습니다. 

이렇게 얘기를 하니 살짝 복잡해 보여서 아래에서 좀 더 자세히 알아보도록 하겠습니다.

 

즉시 로딩

- FetchType.EAGER 옵션을 사용
- 연관관계에 있는 두 엔티티를 비즈니스 로직에서 자주 같이 사용을 할 때 사용하는 옵션
- 연관관계에 있는 두 테이블이 join 쿼리를 이용해 데이터를 한번에 출력한다.
- 연관관계에 있는 필드가 프록시 객체를 사용하지 않고 실제 엔티티 클래스 객체로 가져와진다.
- @ManyToOne, @OneToOne 설정은 즉시 로딩이 기본값이다.

 

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long seq;
    private String name;
    
    @OneToMany(mappedBy="team")
    private List<Member> memberList = new ArrayList<Member>();
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long seq;
    private String name;
    private int age;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 옵션
    @JoinColumn(name="team_seq")// member 테이블의 team_seq 컬럼 명시
    private Team team;	// 부모 테이블 매핑 객체인 Team 엔티티 참조
}

위에 코드에서 Member 엔티티 클래스에 연관관계 매핑이 되어있는 team 필드에 FetchType.EAGER 옵션을 적용시켰습니다. 

위와 같이 설정을 한 뒤에 Member 엔티티에서 특정 시퀀스를 가지고 조회 쿼리를 진행을 하게 되면 Team 엔티티에 있는 테이블까지 join 쿼리를 사용해서 두개의 테이블에 대한 데이터가 한번에 출력이 됩니다.

join으로 두개의 테이블을 한번에 가져오기 때문에 Member 엔티티에 있는 Team team의 필드는 프록시 객체가 아닌 실제 Team 엔티티 객체로 데이터를 가져와지게 된다.

 

지연 로딩

- FetchType.LAZY 옵션을 사용
- 연관관계에 있는 두 엔티티를 비즈니스 로직에서 자주 같이 사용을 하지 않을 때 사용하는 옵션
- 해당 엔티티에 해당하는 테이블만 조회하는 쿼리만 실행 후 연관관계에 있는 필드에 접근할 때 해당 엔티티에 해당하는 테이블 조회 쿼리가 별도로 실행 
- 연관관계에 있는 필드는 프록시 객체로 가져와진다.
- @OneToMany, @ManyToMany 설정은 지연 로딩이 기본값이다.
@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long seq;
    private String name;
    
    @OneToMany(mappedBy="team")
    private List<Member> memberList = new ArrayList<Member>();
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long seq;
    private String name;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 옵션
    @JoinColumn(name="team_seq")// member 테이블의 team_seq 컬럼 명시
    private Team team;	// 부모 테이블 매핑 객체인 Team 엔티티 참조
}

위에 코드에서 Member 엔티티 클래스에 연관관계 매핑이 되어있는 team 필드에 FetchType.LAZY 옵션을 적용시켰습니다. 

위와 같이 설정을 한 뒤에  Member 객체를 조회를 할 때 즉시 로딩과는 다르게 Member 엔티티에 매핑되어 있는 테이블만 조회하는 쿼리가 실행이 됩니다.

member.getTeam().getClass()를 하게 되면 team 필드는 Team 엔티티 클래스가 아닌 Team 엔티티의 프록시 객체로 조회가 되는것을 확인할 수 있습니다.

또한 member.getTeam().getName()이라는 Team 엔티티 안에 있는 name 필드에 접근을 하게 되면 그 때 JPA에서는 Team 엔티티에 매핑되어 있는 테이블을 조회하여 Team 엔티티의 데이터를 가져오는 초기화 단계를 거쳐 Team 데이터에 접근을 할 수 있게 됩니다.

 

프록시 객체

- 실제 클래스를 상속 받아서 만들어짐
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출 (이때 실제 쿼리가 호출)
- 최초 접근 시 한 번만 초기화 후 그 뒤로는 실제 엔티티에 계속 접근
- 초기화 한다고 프록시 객체가 실제 엔티티 객체로 변하지는 않음

출처) 김영환님의 자바 ORM 표준 JPA 프로그래밍 강의

 

 

 

프로시와 즉시 로딩 주의점

- 위에 정의한 이론과는 반대로 실무에서는 가급적 지연 로딩만을 사용하는것을 권장한다.
- 즉시 로딩을 적용하면 예상치 못한 SQL이 발생한다. 
- 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.

김영환님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣다보면 실무에서는 위와 같이 즉시 로딩을 가급적 사용하지 말라고 말씀을 해주신다.  강의에서는 엔티티 구조가 단순하지만 실무에서는 연관된 엔티티들이 많게는 10개가 넘어갈 수 있고 그런 상황에서 즉시 로딩을 하게 된다면 연관된 모든 테이블이 join 쿼리로 실행이 될것이며 예상치 못한 성능 저하가 발생할 수 있다고 하셨다.

 

또한 JPA로 조회하기 복잡한 쿼리의 경우 JPQL 문법을 사용해서 쿼리를 직접 작성하여 사용할 수 밖에 없는데 이렇게 JPQL을 사용할 때 즉시 로딩을 하게 된다면 N+1 문제가 발생한다.

 

N+1 문제

JPQL 문법을 사용을해서 쿼리를 실행을 할 때 JPA에서 즉시 로딩 옵션을 설정을 해두었을 때 join 쿼리가 실행이 되지 않고 연관관계에 있는 테이블들이 각각 한번씩 따로 실행이 되는 문제.

** JPQL : JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공을 해주며 엔티티 객체를 대상으로 쿼리를 실행시킨다.

해당 포스팅은 JPQL에 대한 포스팅이 아니기 때문에 N+1에대한 간단한 예시를 사용해서 간략하게 설명을 하도록 하겠다.

 

// JPQL 문법으로 SQL 문법과 비슷하며  from 절 뒤에 테이블 명이 아니라 엔티티 클래스 객체를 작성
// 엔티티 클래스 뒤에는 별칭을 작성하며 select에서는 별칭을 사용해서 특정 필드를 조회할 수 있다. 
// ex) m : 엔티티의 모든 필드 조회, m.name : 엔티티의 name 필드 조회 등
select m from Member m

// 위 JPQL 문구는 select * from member 라는 DB SQL 구문으로 변역되어 실행된다.

위와 같이 Member 엔티티를 가지고 JPQL로 조회 쿼리를 만들어서 실행을 시킬 때 해당 구문은 JPA에 의해 SQL로 번역이 되며 member 데이터를 가져오게 된다. 이 때 즉시 로딩으로 설정되어 있는 Team team 필드에서 데이터를 즉시 가져와야해서 team 테이블을 조회하는 쿼리가 실행하여 발생되는 문제가 N+1 문제이다.

 

해당 문제는 해당 연관관계를 지연로딩으로 설정을 바꾸고 즉시 로딩과 같이 데이터가 join을 통해서 한번에 쿼리로 조회를 해야하는 부분에서 JPQL fetch join 구문을 사용해서 해결할 수 있다.

자세한 설명은 추후에 JPQL과 N+1 문제에 대해 포스팅할 때 작성을 하도록 하겠습니다.

 

 

참고 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한 강의