N+1 문제 해결 과정

애이용·2021년 12월 25일
0

JPA

목록 보기
2/2
post-custom-banner

나는 N+1 문제를 실제로 겪어보진 못했고, 블로그 읽기만 주구장창했었다
드디어 N+1 문제를 겪어보았다! ㅎㅎ

[길잡] 프로젝트에서 전체 퀘스트 목록들을 조회하는 api를 개발했다.
다음은 N+1 문제가 존재하는 코드를 보기 전 퀘스트 관련 로직을 처리하는 서비스 클래스이다.

@Transactional(readOnly = true)
    fun getQuestList(questId: Long?, position: Position, size: Long): List<QuestResponseDto> {
        val questList =
            questRepository.findByIdLessThanAndOrderByIdDesc(questId = questId, position = position, size = size)

        return questList.map {
            questMapper.toDto(it, userMapper.toDto(it.quest.user, it.point))
        }
    }

Dto로 변환할 때, it.quest.user 에서 생길 때 추가적으로 user를 조회하는 쿼리가 생긴다.
다음은 queryDsl로 작성한 쿼리 메서드이다.


private fun selectQuestList(builder: BooleanBuilder, size: Long) = query.select(
        Projections.constructor(
            QuestSupportVo::class.java,
            quest,
            ability.point,
            JPAExpressions.select(questParticipation.count()).from(questParticipation)
                .where(questParticipation.quest.eq(quest))
        )
    ).distinct()
        .from(quest)
        .where(builder)
        .leftJoin(ability).on(ability.position.eq(quest.user.position).and(ability.user.id.eq(quest.user.id)))
        .orderBy(quest.id.desc())
        .limit(size)
        .fetch()

실제 조회하는 쿼리를 보자

select
        distinct quest0_.quest_id as col_0_0_,
        ability1_.point as col_1_0_,
        (select
            count(questparti3_.participant_id) 
        from
            quest_participation questparti3_ 
        where
            questparti3_.quest_id=quest0_.quest_id) as col_2_0_,
        quest0_.quest_id as quest_id1_1_,
        quest0_.created_at as created_2_1_,
        quest0_.modified_at as modified3_1_,
        quest0_.detail as detail4_1_,
        quest0_.difficulty as difficul5_1_,
        quest0_.is_real_quest as is_real_6_1_,
        quest0_.name as name7_1_,
        quest0_.position as position8_1_,
        quest0_.thumbnail as thumbnai9_1_,
        quest0_.writer_id as writer_10_1_ 
    from
        quest quest0_ 
    left outer join
        user user2_ 
            on quest0_.writer_id=user2_.user_id 
    left outer join
        ability ability1_ 
            on (
                ability1_.position=user2_.position 
                and ability1_.user_id=quest0_.writer_id
            ) 
    order by
        quest0_.quest_id desc limit ?
        
-- N+1 문제
Hibernate: 
    select
        user0_.user_id as user_id1_10_0_,
        user0_.created_at as created_2_10_0_,
        user0_.modified_at as modified3_10_0_,
        user0_.authority as authorit4_10_0_,
        user0_.intro as intro5_10_0_,
        user0_.nickname as nickname6_10_0_,
        user0_.position as position7_10_0_,
        user0_.social_id as social_i8_10_0_ 
    from
        user user0_ 
    where
        user0_.user_id=?
Hibernate: 
    select
        user0_.user_id as user_id1_10_0_,
        user0_.created_at as created_2_10_0_,
        user0_.modified_at as modified3_10_0_,
        user0_.authority as authorit4_10_0_,
        user0_.intro as intro5_10_0_,
        user0_.nickname as nickname6_10_0_,
        user0_.position as position7_10_0_,
        user0_.social_id as social_i8_10_0_ 
    from
        user user0_ 
    where
        user0_.user_id=?
Hibernate: 
    select
        user0_.user_id as user_id1_10_0_,
        user0_.created_at as created_2_10_0_,
        user0_.modified_at as modified3_10_0_,
        user0_.authority as authorit4_10_0_,
        user0_.intro as intro5_10_0_,
        user0_.nickname as nickname6_10_0_,
        user0_.position as position7_10_0_,
        user0_.social_id as social_i8_10_0_ 
    from
        user user0_ 
    where
        user0_.user_id=?
Hibernate: 
    select
        user0_.user_id as user_id1_10_0_,
        user0_.created_at as created_2_10_0_,
        user0_.modified_at as modified3_10_0_,
        user0_.authority as authorit4_10_0_,
        user0_.intro as intro5_10_0_,
        user0_.nickname as nickname6_10_0_,
        user0_.position as position7_10_0_,
        user0_.social_id as social_i8_10_0_ 
    from
        user user0_ 
    where
        user0_.user_id=?

N+1 문제는 ORM을 사용하면 가장 쉽게 접할 수 있는 문제 중에 하나이다.
1번의 쿼리를 날렸지만, 추가로 N번 더 쿼리문을 날려야 하는 상황이다.

나는 FetchJoin으로 N+1 문제를 해결했다!
FetchJoin으로 user 정보도 한꺼번에 가져와보도록 해보겠다.

private fun selectQuestList(builder: BooleanBuilder, size: Long) = query.select(
        Projections.constructor(
            QuestSupportVo::class.java,
            quest,
            ability.point,
            JPAExpressions.select(questParticipation.count()).from(questParticipation)
                .where(questParticipation.quest.eq(quest))
        )
    ).distinct()
        .from(quest)
        .where(builder)
        .leftJoin(ability).on(ability.position.eq(quest.user.position).and(ability.user.id.eq(quest.user.id)))
        .leftJoin(quest.user, user)
        .fetchJoin()
        .orderBy(quest.id.desc())
        .limit(size)
        .fetch()
    select
        distinct quest0_.quest_id as col_0_0_,
        ability1_.point as col_1_0_,
        (select
            count(questparti4_.participant_id) 
        from
            quest_participation questparti4_ 
        where
            questparti4_.quest_id=quest0_.quest_id) as col_2_0_,
        user3_.user_id as user_id1_10_2_,
        quest0_.quest_id as quest_id1_1_0_,
        user3_.user_id as user_id1_10_1_,
        quest0_.created_at as created_2_1_0_,
        quest0_.modified_at as modified3_1_0_,
        quest0_.detail as detail4_1_0_,
        quest0_.difficulty as difficul5_1_0_,
        quest0_.is_real_quest as is_real_6_1_0_,
        quest0_.name as name7_1_0_,
        quest0_.position as position8_1_0_,
        quest0_.thumbnail as thumbnai9_1_0_,
        quest0_.writer_id as writer_10_1_0_,
        user3_.created_at as created_2_10_1_,
        user3_.modified_at as modified3_10_1_,
        user3_.authority as authorit4_10_1_,
        user3_.intro as intro5_10_1_,
        user3_.nickname as nickname6_10_1_,
        user3_.position as position7_10_1_,
        user3_.social_id as social_i8_10_1_ 
    from
        quest quest0_ 
    left outer join
        user user2_ 
            on quest0_.writer_id=user2_.user_id 
    left outer join
        ability ability1_ 
            on (
                ability1_.position=user2_.position 
                and ability1_.user_id=quest0_.writer_id
            ) 
    left outer join
        user user3_ 
            on quest0_.writer_id=user3_.user_id 
    order by
        quest0_.quest_id desc limit ?

join문을 통해 한꺼번에 user 정보를 가져왔다.
이제 추가적인 쿼리는 하지 않는다.

하지만 여기서

    left outer join
        user user2_ 
            on quest0_.writer_id=user2_.user_id 
    left outer join
        user user3_ 
            on quest0_.writer_id=user3_.user_id 

중복된 join문이 있는 걸 알 수 있다. fetchJoin을 먼저 한다면, 다시 Join을 하지 않는다.

최종 쿼리 메서드!

    private fun selectQuestList(builder: BooleanBuilder, size: Long) = query.select(
        Projections.constructor(
            QuestSupportVo::class.java,
            quest,
            ability.point,
            JPAExpressions.select(questParticipation.count()).from(questParticipation)
                .where(questParticipation.quest.eq(quest))
        )
    ).distinct()
        .from(quest)
        .where(builder)
        .leftJoin(quest.user, user)
        .fetchJoin()
        .leftJoin(ability).on(ability.position.eq(quest.user.position).and(ability.user.id.eq(quest.user.id)))
        .orderBy(quest.id.desc())
        .limit(size)
        .fetch()

최종 쿼리문!

select
        distinct quest0_.quest_id as col_0_0_,
        ability2_.point as col_1_0_,
        (select
            count(questparti4_.participant_id) 
        from
            quest_participation questparti4_ 
        where
            questparti4_.quest_id=quest0_.quest_id) as col_2_0_,
        user1_.user_id as user_id1_10_1_,
        quest0_.quest_id as quest_id1_1_0_,
        quest0_.created_at as created_2_1_0_,
        quest0_.modified_at as modified3_1_0_,
        quest0_.detail as detail4_1_0_,
        quest0_.difficulty as difficul5_1_0_,
        quest0_.is_real_quest as is_real_6_1_0_,
        quest0_.name as name7_1_0_,
        quest0_.position as position8_1_0_,
        quest0_.thumbnail as thumbnai9_1_0_,
        quest0_.writer_id as writer_10_1_0_,
        user1_.created_at as created_2_10_1_,
        user1_.modified_at as modified3_10_1_,
        user1_.authority as authorit4_10_1_,
        user1_.intro as intro5_10_1_,
        user1_.nickname as nickname6_10_1_,
        user1_.position as position7_10_1_,
        user1_.social_id as social_i8_10_1_ 
    from
        quest quest0_ 
    left outer join
        user user1_ 
            on quest0_.writer_id=user1_.user_id 
    left outer join
        ability ability2_ 
            on (
                ability2_.position=user1_.position 
                and ability2_.user_id=quest0_.writer_id
            ) 
    order by
        quest0_.quest_id desc limit ?

fetchJoin은 N+1 쿼리 문제를 해결하는 방법 중 하나이고, EntityGraph, BatchSize 등 다른 해결 방법도 있다고 들었다. 이것들도 접하면 블로그 포스팅해야지!

profile
로그를 남기자 〰️
post-custom-banner

0개의 댓글