과정을 즐기자

JPA 지연 로딩 전략 사용시 Json 반환이 안될 때, N+1 문제 해결 본문

Spring Data

JPA 지연 로딩 전략 사용시 Json 반환이 안될 때, N+1 문제 해결

320Hwany 2023. 1. 29. 20:20

문제 상황

웹툰 서비스 프로젝트에서 코인을 지불하면 미리 보기 기능을 이용할 수 있도록 구현하고 있었다.  

이때 Internal Server Error 500, ByteBuddyInterceptor Type defnition error가 발생하였다.  

DB에는 회원의 코인 지불이 반영되었는데 만화정보를 json으로 반환하지 못하였다.  

 

문제의 원인

우선 엔티티의 구조부터 살펴보자 

 

Content

@Getter
@NoArgsConstructor
@Entity
public class Content extends BaseTimeEntity {

    ...

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "cartoon_id")
    private Cartoon cartoon;
    
    ...
}

Cartoon

@Getter
@NoArgsConstructor
@Entity
public class Cartoon extends BaseTimeEntity {

    ...
    
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "author_id")
    private Author author;
    
    ...
}

모두 지연 로딩 전략을 사용하였다.

따라서 Content 엔티티를 조회하면 Cartoon은 실제 객체가 아닌 프록시 객체이다.  

Cartoon을 실제 조회하는 시점에 직접 DB에서 조회를 한다. 

그렇기 때문에 Content에 있는 Cartoon을 Json으로 반환하지 못해 에러가 발생하였다.  

 

해결 방법

크게 2가지의 해결방법이 있다. 

 

1. Dto를 사용한다 

 

프로젝트에서는 이미 Dto를 사용하여 반환하고 있었다.  

Before

@Getter
public class ContentResponse {

    private Cartoon cartoon;

    private String subTitle;

    private Integer episode;
    
    private float rating;

    private LocalDate registrationDate;
    
    ...
}

After

@Getter
public class ContentResponse {

    private CartoonResponse cartoonResponse;

    private String subTitle;

    private Integer episode;
    
    private float rating;

    private LocalDate registrationDate;
    
    ...
}

하지만 ContentResponse 안에 Cartoon 엔티티가 있었다...ㅎ 

이것도 당연히 Dto여야 한다.

 

2. fetch join으로 해결한다. 

 

fetch join도 사용했지만 Cartoon 엔티티 안에 Author 엔티티도 ManyToOne 관계인 것을 생각했어야 했다.

즉 Content를 조회할 때 Cartoon, Author까지 3개의 테이블을 조인해서 한꺼번에 가져와야 한다.

Before

@Override
public Optional<Content> findByCartoonIdAndEpisode(Long cartoonId, Integer episode) {
    return Optional.ofNullable(jpaQueryFactory.selectFrom(content)
            .leftJoin(content.cartoon, cartoon)
            .fetchJoin()
            .where(content.cartoon.id.eq(cartoonId))
            .where(content.episode.eq(episode))
            .fetchOne());
}

After

@Override
public Optional<Content> findByCartoonIdAndEpisode(Long cartoonId, int episode) {
    return Optional.ofNullable(
            jpaQueryFactory.selectFrom(content)
                    .leftJoin(content.cartoon, cartoon)
                    .fetchJoin()
                    .leftJoin(cartoon.author, author)
                    .fetchJoin()
                    .where(content.cartoon.id.eq(cartoonId),
                            content.episode.eq(episode),
                            cartoon.author.id.eq(author.id))
                    .fetchOne());
}

따라서 Content를 조회할 때 한방 쿼리로 Cartoon, Author도 같이 조회해 N+1 문제 또한 해결하여

쿼리 성능을 개선했다. 

 

Content를 조회할 때 나가는 쿼리를 확인해보면 다음과 같다.