N + 1 문제

2024. 11. 18. 20:27Spring

N + 1은 의도하지 않은 쿼리가 여러개가 나가는 현상이다.

정확히는 특정 데이터 1개를 조회했는데 원치않는 N개의 데이터가 추가로 조회되는 현상이다.

이렇게 글로만 설명하면 무슨말인지 감이 잘 잡히지 않으니 예시로 봐보자

 

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Post> posts;

}

 

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

}

 

이렇게 Post, User 두개의 엔티티가 일대다 관계를 갖는다 해보자 이 상황에서 특정 게시글의 제목 내용을 조회 하고싶어져서 게시글 하나를 조회 했다 생각해보자 내가 원하는 정보는 게시글의 제목, 내용이다. 하지만 실제로 조회하면 이 엔티티의 게시글 + 유저 정보까지 조회 될것이다. 아래의 쿼리문을 보자

Hibernate: 
    select
        p1_0.id,
        p1_0.content,
        p1_0.title,
        p1_0.user_id,
        u1_0.id,
        u1_0.name 
    from
        post p1_0 
    join
        user u1_0 
            on u1_0.id=p1_0.user_id 
    where
        p1_0.id=?

나는 분명 게시글의 정보만 필요하지만 User의 id, name등의 정보를 조인해서 가져오는 모습을 볼 수 있다. 그러면 원하는 정보만 가져오고 싶은데 추가적인 쿼리가 발생했으니 성능적으로도 문제가 생겼다. 어떻게 하면 이 문제를 해결 할 수 있을까? 방법은 여러가지가 있지만

그 전에 이 상황에 원인을 알아보자 위의 포스트 엔티티를 보면 @ManyToOne으로 되어있다. 갑자기 이게 무슨 소리냐 할 수 있지만 fetchType 이란게 존재하는데 이 fetchType은 연관된 엔티티를 어떻게 가져올지에 대한 전략을 설정하는 것이다. fetchType에는 두가지 종류가 있는데 EagerLoading(즉시 로딩),  LazyLoading(지연 로딩) 두가지 전략이 있다. 

 

- EagerLoading(즉시 로딩)

즉시 로딩은 말 그대로 연관된 엔티티를 즉시 로딩 한다는 것이다. XXXToOne의 경우는 모두 FetchType이 즉시로딩이 default 값으로 되어 있다. 

package jakarta.persistence;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ManyToOne {
    Class targetEntity() default void.class;

    CascadeType[] cascade() default {};

	//default 값이 즉시 로딩이다.
    FetchType fetch() default FetchType.EAGER;

    boolean optional() default true;
}

그래서 위의 예시 처럼 Post엔티티를 조회할때 연관된 유저의 정보까지 한번에 조회 한것이다.

 

- LazyLoading(지연 로딩)

지연 로딩은 해당 엔티티의 정보가 필요 할때까지 지연해 두었다가 필요해지는 시점에 로딩 하는 것이다.

XXXToMany의 경우는 모두 default 값이 지연로딩이다.

 

위의 예시에서는 즉시로딩이기 때문에 엔티티를 필요할때까지 로딩하지 않았다. 하지만 fetchType을 Lazy로 바꾸고 게시글을 조회해 보겠다.

Hibernate: 
    select
        p1_0.id,
        p1_0.content,
        p1_0.title,
        p1_0.user_id 
    from
        post p1_0 
    where
        p1_0.id=?

지연 로딩으로 설정해두니 유저의 정보가 필요하지 않은 경우에 지금 처럼 user 테이블을 조인하지 않고 정보를 가져오지 않았다.

그런데 여기서 필요한 시점이 무엇인지 감이 안잡히는 사람도 있을것이다. 연관된 정보의 필요한 시점은 말 그대로 코드에서 해당 정보가 필요한 시점이다.

@SpringBootApplication
@RequiredArgsConstructor
public class BlogExApplication implements CommandLineRunner {
	private final UserService userService;
	private final PostService postService;
	private final PostRepository postRepository;

	public static void main(String[] args) {
		SpringApplication.run(BlogExApplication.class, args);
	}

	@Override
	public void run(String... args) {
		Post post = postService.findById(1L);


	}
}

public void run 부분을 보면 post 정보를 찾기만 했다. 여기서 유저의 정보를 필요로 하는 부분은 존재하지 않는다.

@SpringBootApplication
@RequiredArgsConstructor
public class BlogExApplication implements CommandLineRunner {
	private final UserService userService;
	private final PostService postService;
	private final PostRepository postRepository;

	public static void main(String[] args) {
		SpringApplication.run(BlogExApplication.class, args);
	}

	@Override
	public void run(String... args) {
		Post post = postService.findById(1L);
        //유저의 정보가 필요해지는 시점 이때 로딩함
		System.out.println(post.getUser().getName());

	}
}

예시를 위해 저렇게 적어두긴 했지만 실제로 저 코드를 실행하면 이미 영속성 컨텍스트가 닫힌 상태이기에 LazyInitializationException 오류가 발생할것이다. 이해를 위해 저렇게 적어둔것이니 간단히 이해하고 넘어가길 바란다.

지연로딩은 이렇게 필요시점까지 로딩을 지연 시키는 방식이다. 그러면 여기서 의문이 생기는데 진짜로 로딩을 안해두는 걸까? 정답은 아니다. 지연 로딩은 프록시 객체라는 가짜 껍데기를 가져와 대기 시켜두고 진짜 필요한 시점에 DB를 조회해 실제 데이터를 가져오는 방식이다.

글을 쓰다보니 길어져서 N + 1 해결하는 방법은 다음 글로 미루겠다.

'Spring' 카테고리의 다른 글

N + 1 문제 해결법  (0) 2024.11.19
Spring Vs SpringBoot  (0) 2024.06.15