[데이터베이스 이론] 지연 로딩(Lazy Loading) vs 즉시 로딩(Eager Loading): 성능 최적화의 핵심 전략!
지연 로딩(Lazy Loading) vs 즉시 로딩(Eager Loading): 성능 최적화의 핵심 전략!
안녕하세요! 데이터베이스와 객체를 매핑해주는 ORM(Object-Relational Mapping) 프레임워크(예: JPA, Hibernate)를 사용하면서, N+1 문제, 성능 저하 같은 고민을 해보신 적 있나요? 이런 문제들의 핵심에는 바로 연관 관계(Association) 데이터를 언제 로딩할 것인가에 대한 전략, 즉 ‘지연 로딩(Lazy Loading)’과 ‘즉시 로딩(Eager Loading)’이 숨어 있습니다.
오늘은 이 두 가지 로딩 전략이 무엇인지, 어떤 상황에서 사용해야 하는지, 그리고 잘못 사용했을 때 발생할 수 있는 문제점과 해결책에 대해 자세히 알아보겠습니다!
연관 관계 로딩: 언제 데이터를 가져올까?
데이터베이스에서 테이블들은 서로 관계를 맺고 있습니다. 예를 들어, ‘회원’은 여러 개의 ‘주문’을 가질 수 있고, ‘주문’은 하나의 ‘회원’에게 속합니다. ORM에서는 이러한 관계를 객체 간의 연관 관계(일대다, 다대일, 일대일, 다대다)로 매핑하죠.
이때, 하나의 객체를 조회할 때 그와 연관된 다른 객체(들)의 데이터를 언제 데이터베이스에서 가져올 것인가에 대한 두 가지 주요 전략이 있습니다.
즉시 로딩 (Eager Loading):
- 설명: 연관된 엔티티를 메인 엔티티를 조회하는 시점(즉시)에 함께 데이터베이스에서 가져오는 방식입니다. 마치
SELECT쿼리 한 번으로 모든 정보를 한꺼번에 가져오는 것과 같습니다. - 장점: 연관된 데이터를 미리 가져오므로, 이후에 해당 데이터에 접근할 때 추가적인 쿼리가 발생하지 않아 편리합니다.
- 단점: 필요하지 않은 연관 데이터까지 미리 가져올 수 있어 불필요한 데이터 로딩으로 성능 저하를 일으킬 수 있습니다. 특히,
다대일(ManyToOne)이나일대일(OneToOne)관계에서는JOIN쿼리가 발생하여 복잡해질 수 있습니다.일대다(OneToMany)나다대다(ManyToMany)관계에서는 특히N+1 문제를 유발할 가능성이 높습니다. - JPA 기본 전략:
@ManyToOne,@OneToOne은 기본적으로 즉시 로딩(FetchType.EAGER)입니다.
- 설명: 연관된 엔티티를 메인 엔티티를 조회하는 시점(즉시)에 함께 데이터베이스에서 가져오는 방식입니다. 마치
지연 로딩 (Lazy Loading):
- 설명: 연관된 엔티티를 실제로 사용하는 시점(지연해서)에 데이터베이스에서 가져오는 방식입니다. 필요한 순간에 ‘게으르게’ 데이터를 가져온다고 해서 ‘지연 로딩’이라고 부릅니다.
- 장점: 필요한 데이터만 그때그때 가져오므로, 불필요한 데이터 로딩을 막아 성능을 최적화할 수 있습니다. 메모리 사용량도 줄일 수 있습니다.
- 단점: 연관된 데이터를 사용할 때마다 추가적인 쿼리가 발생할 수 있습니다. 특히, 특정 연관 데이터를 반복적으로 접근하는 루프 안에서 지연 로딩을 사용하면
N+1 문제가 발생하여 오히려 성능이 심각하게 저하될 수 있습니다. (자세한 내용은 아래에서 다룹니다!) - JPA 기본 전략:
@OneToMany,@ManyToMany는 기본적으로 지연 로딩(FetchType.LAZY)입니다.
지연 로딩의 양날의 검: N+1 문제
지연 로딩은 분명 효율적이지만, 잘못 사용하면 N+1이라는 치명적인 성능 문제를 야기할 수 있습니다.
N+1 문제란?
예를 들어, 100명의 회원이 있고, 각 회원이 여러 개의 주문을 가질 수 있는 상황을 생각해 봅시다.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();(1번 쿼리: 100명의 회원 조회)이제 각 회원의 주문 목록에 접근하려고 합니다.
Java
1 2 3
for (Member member : members) { System.out.println(member.getOrders().size()); // 🚨 문제 발생 지점! }만약
member.getOrders()가 지연 로딩으로 설정되어 있다면,getOrders()가 호출될 때마다 각 회원의 주문을 가져오기 위한 추가 쿼리(N번의 쿼리)가 발생합니다. 즉, 전체적으로1 (회원 조회) + N (각 회원의 주문 조회)번의 쿼리가 실행되어 총101번의 쿼리가 발생하게 됩니다. 100명의 회원이라면 101번의 쿼리, 1000명이라면 1001번의 쿼리가 발생하여 성능에 심각한 악영향을 미칩니다.
N+1 문제 해결 전략 (지연 로딩을 효율적으로 사용하기)
N+1 문제는 대부분의 경우 지연 로딩을 사용하면서 발생하는 문제입니다. 이를 해결하고 지연 로딩의 장점을 살리려면 다음과 같은 전략을 사용합니다.
페치 조인 (Fetch Join):
- 가장 일반적이고 효과적인 방법입니다.
JOIN FETCH구문을 사용하여 처음 쿼리할 때 연관된 데이터까지 한 번에 가져옵니다. SELECT m FROM Member m JOIN FETCH m.orders- 이렇게 하면 100명의 회원과 그들의 모든 주문을 단 한 번의 쿼리로 가져올 수 있습니다.
- 가장 일반적이고 효과적인 방법입니다.
엔티티 그래프 (Entity Graph):
- JPA 2.1부터 도입된 기능으로, 조회 시점에 특정 연관 관계를 즉시 로딩할지 결정할 수 있게 해줍니다. XML이나 어노테이션으로 미리 정의해둘 수도 있습니다.
@NamedEntityGraph(name = "Member.withOrders", attributeNodes = @NamedAttributeNode("orders"))- 쿼리 시점에
em.find(Member.class, id, getEntityGraph("Member.withOrders"))처럼 사용합니다.
배치 사이즈 (Batch Size) 설정:
- Hibernate에서 제공하는 기능으로, 지연 로딩 시
N개의 쿼리가 발생하는 대신,N개의 아이디를 묶어IN절로 한 번에 가져오도록 최적화합니다. @BatchSize(size = 100)어노테이션을 연관 관계 필드 위에 붙여주거나, 글로벌 설정으로hibernate.default_batch_fetch_size를 설정할 수 있습니다. 예를 들어 100개씩 묶어서 가져오면 100개의 회원을 조회하고, 그 회원의 주문들을 100개 단위로 묶어 몇 번의 쿼리만으로 가져올 수 있습니다. (100번의 쿼리가 1-2번의 쿼리로 줄어듦)
- Hibernate에서 제공하는 기능으로, 지연 로딩 시
어떤 로딩 전략을 선택해야 할까?
일반적으로는 다음과 같은 가이드라인을 따르는 것이 좋습니다.
기본은 지연 로딩(Lazy Loading):
- 대부분의 연관 관계는 지연 로딩으로 설정하는 것이 좋습니다. 필요 없는 데이터를 미리 가져와서 메모리를 낭비하거나 성능을 저하시키는 것을 방지할 수 있습니다.
- 특히
일대다(@OneToMany)나다대다(@ManyToMany)관계는 기본이 지연 로딩인데, 이를 즉시 로딩으로 변경하는 것은N+1문제 발생 가능성이 매우 높으므로 신중해야 합니다.
즉시 로딩(Eager Loading)은 신중하게:
ManyToOne이나OneToOne관계에서, 연관된 엔티티가 항상, 그리고 거의 모든 상황에서 함께 사용된다면 즉시 로딩을 고려할 수 있습니다.- 하지만 조금이라도 불필요한 상황이 예상된다면 지연 로딩으로 설정하고, 필요할 때
페치 조인이나엔티티 그래프를 사용하여 최적화하는 것이 더 안전한 방법입니다.
항상 비즈니스 로직과 함께 고민:
- 어떤 로딩 전략을 선택할지는 결국 해당 데이터를 어떤 비즈니스 로직에서, 얼마나 자주, 어떻게 사용할 것인가에 따라 달라집니다. 무조건적인 규칙보다는 상황에 맞는 유연한 판단이 중요합니다.
마무리하며
지연 로딩과 즉시 로딩은 ORM을 사용하는 개발자라면 반드시 이해하고 적절하게 활용해야 할 중요한 개념입니다. 특히 지연 로딩의 장점은 살리되, N+1 문제와 같은 잠재적인 성능 이슈를 해결하는 방법을 아는 것이 중요합니다.
이 두 가지 로딩 전략을 마스터하여, 여러분의 애플리케이션 성능을 한 단계 끌어올리고 더욱 효율적인 데이터베이스 연동 코드를 작성하시길 바랍니다!