본문 바로가기

[Kotlin&Spring] 5기 내일배움캠프

[Kotlin&Spring] 5기 JPA 연관 관계 매핑과 N+1 문제

 

연관관계 매핑이란,

데이터베이스에서 엔티티 간 관계를 정의하고, 이를 ORM을 통해 객체지향적으로 표현하는 것을 말한다

자바 객체와 관계형 데이터베이스는 객체는 상속이 가능한 반면에 데이터베이스는 테이블 구조를 가지는 등의 다른 구조를 가진다

JPA같은 ORM 기술을 도입하면 객체 간의 관계를 기반으로 데이터베이스 테이블 간의 관계를 매핑할 수 있다

또한 JPA를 통해 연관관계를 매핑하면 SQL을 직접 작성하지 않고도 객체 간의 관계를 활용하여 쉽게 데이터를 조회하고 조작할 수 있다

 

단방향 매핑(Unidirectional Mapping)은

한 엔티티에서만 다른 엔티티를 참조하는 방식으로, 관계를 설정한 한쪽에서만 접근이 가능하며 반대쪽에서는 접근할 수 없게 된다

상대 엔티티에서 관계를 알지 못해 객체 그래프 탐색이 어렵다

*객체 그래프: 객체 지향 프로그램에서 객체 그룹이 네트워크를 형성하는 것을 그래프 이론의 개념으로 표현한 것이다

그래프는 객체 간의 관계를 표현하는 자료구조이다

따라서 관계가 단순한 경우에, 또 불필요한 관계를 줄이고 싶을 때 사용한다

유지보수면에서 간단하며 성능 최적화가 쉽게 이루어진다

 

양방향 매핑(Bidirectional Mapping)은

양쪽 엔티티에서 서로를 참조하는 방식으로, 양쪽에서 상대 엔티티를 조회할 수 있어 양쪽에서 데이터를 쉽게 조회해야 할 때 사용한다

mappedBy 속성을 사용해서 관계의 주인을 설정해야한다

객체 그래프 탐색이 쉬워 객체 그래프 탐색이 필요할 떄 사용한다

상속등의 구조를 가진 직관적인 연관관계가 중요할 때 사용한다

설계가 상대적으로 복잡하며 코드량이 증가하고 유지보수에서 관리가 필요하다

 

예시를 들자면 다음과 같다

게시글과 댓글 엔티티로 게시글이 관계의 주인이다

@NoArgsConstructor
@Entity
@Table(name = "posts")
public class Post {
    @Id @GeneratedValue
    private Long id;
    
    private String title;
    private String content;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();
}

 

@NoArgsConstructor
@Entity
@Table(name = "comments")
public class Comment {
    @Id @GeneratedValue
    private Long id;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
}

 

N+1 문제는 주로 양방향 매핑에서 발생하는 문제로, 하나의 쿼리로 N개의 객체를 로딩한 후, 각 객체에 연관된 데이터를 추가로 조회하는 개별 쿼리가 N번 실행되면서 총 N+1번의 쿼리가 발생하는 문제이다

또한 fetch 타입이 FetchType.LAZY(지연 로딩)로 설정된 경우에는 연관 관계로 데이터를 가져오면서도 N+1 문제가 생길 수 있다

 

N+1는 단방향의 @ManyToOne 매핑에서는 거의 발생하지 않고 이 경우에는 JOIN을 통해 한번의 조회 쿼리 실행으로 가능하다

단방향 @OneToMany 경우에는 Many의 데이터를 다량 가져오기 때문에 N+1 문제가 발생할 수 있고 FETCH JOIN 또는 배치 사이즈 설정으로 해결할 수 있다

이때 @BatchSize(size = 10) 어노테이션을 사용하여 한 번에 로드할 연관 엔티티의 수를 조정할 수 있다

size는 본인이 설정 가능하다

양방향 매핑의 @ManyToOne 또는 @OneToMany 의 경우 FetchType.Lazy 로 설정 후 JPQL 에 FETCH JOIN을 사용해 해결할 수 있다

FETCH JOIN을 사용하면 한 번의 쿼리로 연관된 엔티티들을 함께 로드할 수 있다

FetchType.EAGER(즉시 로딩)로 설정된 것과 유사한 효과를 내지만, 쿼리를 명시적으로 제어할 수 있다는 장점이 있다

 

위의 게시글과 댓글 예시를 들어 N+1 문제를 FETCH JOIN을 해결하는 PostRepository 코드는 다음과 같다

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :postId")
    Optional<Post> findPostWithComments(@Param("postId") Long postId);
}

 

데이터를 직접적으로 다루게 되고 JPQL등의 쿼리도 작성하면서 SQL 부분도 공부해야함을 느낀다

내일은 SQL JOIN 부분을 더 공부해야겠다

게을러지는 나 자신에게 너그러워지지말자

화이팅 ~ !