Tiny Star

📊ORM

[ORM] 즉시로딩(EAGER)과 지연로딩(LAZY)

청크 2024. 4. 18. 10:45

게시물-댓글 로직 작성하다가 마주하게 된 N+1

 

문제를 파고 파다가 즉시로딩과 지연로딩까지 거슬러 올라오게 되었다.

 

JPA 환경에서 즉시로딩, 지연로딩에 대한 설명은 있는데 근본적으로 얘네가 무엇이고 어떤건지

설명해놓은 분들은 거의 없는 것 같아서 적어본다.

 

대체 이게 뭔지는 알아야 적용을 할 것 아니냐며...(?)

 

우선 로직을 구현하다보면 데이터 조회를 통해 기능을 만들어야 할 때가 많이 생긴다.

 

특히 대부분의 기능들은 각자의 테이블이 존재하고 서로 연관관계를 맺어 참조하는 형태로 코드가 짜여지게 되는데

이 때 연관관계가 맺어진 데이터를 조회할 때 연관된 엔티티들을 언제 어떻게 로딩할지, 조회 시점을 결정할 수 있도록 제공하는 방법이다.

즉시로딩 (Eager Loading)과 지연로딩(Lazy Loading)이다.

 

즉시로딩과 지연로딩은 데이터베이스에서 데이터를 로드하는 시점을 어떻게 관리할지에 대한 중요한 전략으로,

이 두 전략의 차이점을 이해하는 것은 데이터의 로딩 시점과 관련된 성능 이슈를 최적화하는 데 필수적이다.


즉시로딩 (EAGER Loading)

즉시로딩은 데이터 조회 시 주 엔티티를 조회할 때 연관된 모든 엔티티도 전부 같이 조회가 되면서 로드되는 전략으로,

이 과정은 단일 쿼리 또는 조인을 통해 발생한다.

 

예를 들어 게시물 조회 시 해당 게시물에 달린 댓글을 같이 조회해야하는 케이스에 즉시로딩을 적용한다면

게시물을 조회하는 쿼리가 실행 될 때 게시물과 연결된 댓글도 함께 조회가 이루어지는 것이다.

 

해당 예를 쿼리문으로 본다면 이렇게 던져진다.

SELECT * FROM boards
JOIN comments ON boards.id = comments.board_id
WHERE boards.id = {id};

 

즉시로딩의 장점

1. 단순하다.

데이터를 처음 로드할 때 모든 필요한 정보를 가져오기 때문에, 개발자는 연관된 데이터가 이미 로드되었는지 걱정할 필요가 없고

그렇기 때문에 즉시로딩은 프로그래밍 모델을 단순하게 만들어준다.

 

2. 성능 최적화

여러번의 쿼리 대신 한번의 쿼리로 필요한 모든 데이터를 가지고 오기 때문에

연관데이터가 항상 필요한 경우에는 즉시로딩이 효과적인 전략이 될 수 있다.

 

즉시로딩의 단점

1. 과도한 데이터 로드

엔티티를 설계하다보면 항상 모든 연관 데이터를 로드 할 필요가 없다.

예를 들자면 유저를 조회한다고해서 그 유저가 쓴 게시물과 댓글들까지 모조리 필요하지 않다는 이야기다.

이런 상황에서 즉시로딩은 항상 연관된 모든 데이터를 로드하기 때문에 굳이 필요하지 않은 데이터까지 가지고 오고,

만약 연관관계가 많고 그 안의 데이터가 방대한 경우에는 오히려 성능 저하로 이어질 수 있다.

 

2. 리소스 사용 증가

1번 단점에 이어 불필요하게 많은 데이터를 로드하면 메모리의 사용량이 증가하고,

응답 시간이 길어질 수 있다.


지연로딩 (LAZY Loading)

지연로딩은 연관된 엔티티가 실제 사용될 때까지 로드하지 않는 방법으로,

즉시로딩과 다르게 주 엔티티 로드 시에는 주 엔티티만 SELECT하는 쿼리를 날린 뒤

이후 연관된 엔티티에 실제로 접근이 시도될 때 그제서야 연관된 엔티티만을 조회한다.

 

즉, 처음 한번 주 엔티티에 대한 쿼리가 발생하고 이후

연관된 데이터 로드는 해당 엔티티에 실제로 접근할 때 추가로 쿼리가 발생되는 것이다.

 

지연로딩이 실행되는 과정을 쿼리로 만들어서 이해해보면 이렇다.

먼저 게시물의 ID를 게시물 테이블에서 찾아온 뒤,

SELECT * FROM boards WHERE boards.id = {id};

 

게시물에 연결된 댓글들을 불러올 수 있도록 댓글 데이터에 접근을 시도(뭐 대충 boards.comments 이렇게?)하면

SELECT * FROM comments WHERE comments.board_id = {id};

추가로 쿼리를 발생시켜 댓글 테이블에서 게시물 아이디로 찾아오게 된다.

 

그럼 여기서 또 의문이 든가.

그렇다면 데이터 접근을 시도받는 연관 엔티티는 그렇다치고, 주 엔티티는 매번 새롭게 쿼리가 발생하는가?

 

그건 아니다.

지연 로딩의 과정을 더 자세히 파보면

1. 초기 로드 : 주 엔티티의 데이터만 로드가 된다. (초기 1회 쿼리 실행으로 연관 엔티티는 로드되지 않는다.)

2. 연관 엔티티 접근 시 : 사용자나 프로그램이 연관 엔티티에 접근하려고 할 때

해당 연관 엔티티에 대한 데이터베이스 쿼리가 실행된다.

3. 쿼리 실행 : 연관 엔티티에 대한 쿼리가 실행되고 데이터가 로드되는데,

이 데이터는 보통 캐시되어 이후 같은 세션 내에서 추가 쿼리가 없이 접근이 가능하다.

 

마지막 3번에서와 같이 대부분의 많은 ORM들은 첫 쿼리 이후 결과를 캐시하여

같은 트랜잭션 또는 세션 내에서는 동일한 데이터에 대한 추가 쿼리를 방지하는 기능이 있다.

 

정리하자면 지연 로딩을 사용할 때는 연관 엔티티를 처음 접근할 때만 추가 쿼리가 발생하며, 

주 엔티티 자체가 반복적으로 로드되지는 않는 다는 것이다.

 

지연로딩의 장점

1. 리소스의 효율성

실제로 데이터가 필요한 시점에만 연관 데이터를 로드하기 때문에 초기 로드 시 메모리 사용량이 적어

데이터베이스 부하를 줄일 수 있다.

 

2. 성능 최적화

이건 즉시로딩의 장점인데 뭔말인가 싶겠지만,

상황에 따라서 필요하지 않은 데이터는 로드하지 않기 때문에 전체적인 응답 시간을 단축 시킬 수 있다.

 

지연로딩의 단점

1. 복잡성 증가

연관 데이터에 접근할 때마다 추가적인 쿼리가 발생하기 때문에 프로그램의 복잡성이 증가한다.

이로 인해 개발자는 언제 데이터가 로드되는지 생각하면서 개발을 해야하는 것이다.

 

2. N+1

잘못 구현된 지연로딩은 많은 수의 쿼리가 발생할 수 있는데, 이게 바로 N+1의 문제이다.

1번 로드될 때마다 쿼리가 반복적으로 발생하여 연관된 데이터만큼 쿼리가 도는 것이기 때문에

한 번의 많은 양을 끌어오는 즉시로딩보다 효율이 떨어진다.


즉시로딩 (Eager Loading)과 지연로딩(Lazy Loading) 그래서 뭘 사용해야하나

 

즉시로딩과 지연로딩을 사용할 때 몇가지 고려해야 할 사항들이 존재한다.

분명 각각의 장단점이 뚜렷하게 존재하기 때문에 개발을 할 때 과연 한번에 데이터가 필요한지를 먼저 생각해봐야 할 것이다.

 

그 외에도, 몇가지 더 고려를 해야하는데

1. 데이터 접근 패턴

애플리케이션에서 데이터를 어떻게 접근하는지에 따라 적절한 로딩 전략을 선택해야 한다.

데이터에 자주 접근하는 경우 즉시로딩이, 접근 빈도가 낮거나 필요할 때만 데이터를 사용하는 경우 지연로딩이 더 적합할 수 있다.

 

2. 트랜잭션 관리

지연로딩을 사용할 때는 트랜잭션이 여전히 활성 상태인지 확인이 필수적이다.

만약 데이터를 로드하는 시점에 트랜잭션이 종료되어 있다면 지연 로딩 실패로 이어진다.

 

요약해보면  즉시로딩은 데이터베이스의 부하를 줄이기 위해 필요한 모든 데이터를 한 번에 로드하려는 전략이고,

지연로딩은 필요할 때까지 데이터 로드를 연기하여 리소스 사용을 최소화하는 전략이기 때문에

각각의 전략은 사용하는 애플리케이션의 특성과 요구사항에 따라 선택되어야 한다.

 

데이터 접근 패턴을 분석하고, 각 상황에 맞는 최적의 로딩 전략을 사용하는 것이 중요하다.

 

+

추가로 현업에서는 지연 로딩이 기본값이다. 

만약에 즉시 로딩이 기본값이라면 위에서 말한 것처럼 불필요한 데이터가 한번에 받아와지는 일이

여러번 발생할 수 있기 때문에 미리 사전에 방지하는 개념으로 생각하면 된다.

 

무조건 지연로딩!!!이 아니고

혹시모를 예상 밖 상황을 대비해 일단 설정은 안전빵 지연로딩으로 해야한다는 뜻이다.

 

머리가 안굴러간다면 공식처럼 외워두자.

@ManyToOne 또는 @OneToOne처럼 ~ToOne 관계는 기본이 즉시로딩이기 때문에 반드시 지연 로딩 설정이 필요하고,

@OneToMany 또는 @ManyToMany처럼 ~ToMany 관계는 기본이 지연로딩이기 때문에 따로 지연로딩 옵션을 설정해 줄 필요가 없다.

 

+

따로 포스팅을 분리 할 예정이긴하나

즉시로딩에서도 JOIN이 사용되지 않을 경우, N+1 문제가 발생할 수 있고

지연로딩에서도 fetch join(JPA), contains_eager(SQLAlchemy)를 활용해서 join쿼리로 데이터를 한번에 불러 올 수 있다.

 

두 전략이 같은 것은 아니지만 공통적으로는 성능 최적화와 N+1 문제 해결을 위해

서로 다른 방식으로 연관 데이터를 효율적으로 로드하는 데 사용되는 것으로......따로 공부해야지!

 

 

 

 

 

 

 

 

 

 

 

 

'📊ORM' 카테고리의 다른 글

[ORM] ORM(Object-Relational Mapping)의 개념  (1) 2024.04.12