즉시 로딩과 지연 로딩 비교

땡글이·2023년 3월 8일
0

JPA

목록 보기
3/9

JPA를 이용해 처음 개발하다 보면, 엔티티와 연관된 엔티티가 많아지게 되고 이것이 성능 저하를 야기할 수 있지 않을까라는 고민에 빠지게 된다. 나 또한 그랬었다.

하지만, JPA에서는 엔티티를 조회할 때 연관된 엔티티를 모두 조회하는 것이 아니다. 프록시 객체를 활용해서 조회된다.

프록시

Spring 에서는 프록시라는 개념이 아주 많이 쓰인다. DI 를 위해서 CGLIB 혹은 JDBC 동적 프록시 기술을 이용해 프록시 객체를 만들기도 하고, AOP를 위해 프록시 개념을 이용한다. 또한 JPA에서도 이 프록시 개념이 사용된다. 언제? 바로 지연 로딩을 구현할 때 쓰인다.

아래의 코드 예제를 보자. 회원 엔티티와 팀 엔티티가 있고 회원(다)-팀(일) 로 회원 기준으로 다대일 관계를 가진다.

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
    
    @ManyToOne
    private Team team;
}
@Entity
public class Team extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

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

Member 엔티티와 Team 엔티티가 있을 때, 비즈니스 로직에서 Member의 이름이나 id만 조회해도 된다면, Team 엔티티에 접근할 필요는 없어진다.

JPA에서는 이런 문제를 해결하기 위해서 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법인 지연 로딩 방식을 제공한다.

쉽게 이야기해서, team.getName()처럼 팀 엔티티의 값을 실제 사용하는 시점에 데이터베이스에서 Team 엔티티에 대해 조회하는 것이고, 이전에는 조회하지 않는 것이다.

이런 지연 로딩 방식을 활용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이를 프록시 객체라고한다.

프록시 객체 초기화

프록시 객체는 실제 객체(target)에 대한 참조를 보관한다. 그리고 프록시 객체의 메서드를 호출하면, 프록시 객체는 실제 객체의 메서드를 호출한다.
프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다.
참고로, 프록시 객체는 식별자 값을 가지고 있어서, team.getId()를 호출해도 프록시를 초기화하지는 않는다. 단, 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우에만 초기화하지 않는다.

프록시 객체가 초기화됐는지 확인하는 방법은 PersistenceUtil.isLoaded(Object entity) 메서드를 사용하면 된다. 만약, 초기화되지 않았다면 false를, 초기화됐다면 true를 리턴한다.

그리고 Hibernate에서는 initialize() 메서드를 사용해서 프록시 객체를 초기화할 수 있다. JPA 표준은 단지 초기화 여부만 확인할 수 있다.

즉시 로딩 (Eager Loading)

프록시 객체는 주로 연관된 엔티티를 지연로딩할 때 사용한다. 지연로딩은 프록시 객체를 통해 대충 감이 잡힌다. 그렇다면 즉시 로딩은 뭘까?

즉시 로딩 방식은 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. 예시로, em.find(Member.class, "member1")를 호출할 때 회원 엔티티와 연관된 Team 엔티티도 같이 조회되는 것이다.

즉시 로딩을 사용하려면 연관관계의 주인에서 (@ManyToOne(fetch = FetchType.EAGER)) 로 설정해주면 된다.

@ManyToOne, @OneToOne 은 기본적으로 즉시 로딩으로 설정되어 있고, @ManyToMany, @OneToMany 는 기본적으로 지연 로딩으로 설정되어 있다.

그렇기에 실무에선 즉시 로딩으로 개발하게 되면 의도치 않은 성능 저하를 불러올 수 있기에 즉시 로딩 방식 보다는 지연 로딩 방식을 채택해 개발해야 한다!

대부분의 JPA 구현체에서는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다. 네트워크를 타고 여러 번의 SQL 실행은 성능저하를 야기할 수 있다.

JPA 구현체는 기본적으로 조인 쿼리를 만들 때 Outer join 방식으로 SQL 을 작성한다.
하지만, Outer join은 매칭되지 않는 데이터까지 비교하며 추출하므로 데이터베이스 시스템은 보다 많은 양의 데이터를 처리해야 하기 때문에, 매칭되는 데이터만을 비교하면 추출하는 Inner join에 비해 더 많은 시간과 리소스를 소비하게 된다.
JPA에서 Inner join을 사용하려면, (optional = false) 설정을 주면 되고 Outer join(기본값)을 사용하려면 (optional = true) 사용하면 된다.
만약, FK가 null일 수도 있는 환경에선 Outer join을 사용해줘야한다.

즉시 로딩은 항상 안좋을까?

만약 Member 엔티티를 조회할 때 연관된 엔티티인 Team 엔티티를 항상 조회해야 하는 경우에는 지연 로딩 방식보다는 즉시 로딩 방식을 채택해줘야 한다.

위의 상황에서도 지연 로딩 방식을 채택했을 때, 항상 연관된 엔티티를 조회하는 SQL을 따로 보내게 됨으로써 2개 이상의 SQL문이 네트워크를 타서 DB에 전달되고, DB에서는 2번 이상의 SQL문을 실행하게 된다.

하지만 즉시 로딩 방식으로 구현한다면 항상 조인 SQL문을 작성해 한 번의 쿼리만으로 연관된 엔티티의 정보들까지 가져오므로 의도치 않은 성능저하를 막을 수 있다.

하지만, 중요한 점은 즉시 로딩은 JPQL 작성 시 N+1문제(의도한 SQL은 1개인데, 의도치 않은 SQL문 N개가 실행되는 문제) 를 발생시킬 수 있다. 그래서 실무에선 지연 로딩을 사용하는 것이 좋다. 지연 로딩이라고 N+1문제가 발생하지 않는 것은 아니다. fetch join 이나 EntityGraph 기능을 이용해 N+1문제를 해결해야 한다.

추후 N+1문제와 그 해결방법에 대해 더 자세히 다루는 글 작성하도록 하겠습니다 :)

지연 로딩 (Lazy Loading)

지연 로딩은 연관된 엔티티를 실제 사용할 때 조회한다. 예시로, member.getTeam().getName()을 실행하게 되면, 이 때 SQL문이 실행되는 것이다. JPA가 SQL을 호출해서 팀 엔티티를 조회한다. 사용되기 전까지는 연관된 엔티티는 앞에서 봤던 프록시 객체로 조회되는 것이다.

  • 지연로딩 방식을 사용하려면, @ManyToOne(fetch = FetchType.LAZY) 를 작성하면 된다.

조회 대상이 영속성 컨텍스트에 있다면, 지연 로딩 방식으로 채택되어 있어도 프록시 객체를 참조하는 것이 아니라 실제 객체를 참조하게 된다.

Reference

자바 ORM 표준 JPA 프로그래밍
https://schatz37.tistory.com/2

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글