1. 서비스 요구사항


https://github.com/Gseungmin/playlist/pull/5

플레이리스트에 노래를 추가해야 한다.


<aside>

플레이리스트에는 노래가 들어가야 한다.

하지만 플레이리스트에 노래를 직접 넣으면 같은 노래라도 플레이리스트마다 관리하 필요하다.

따라서 플레이리스트와 노래는 N : M 관계를 가지기에 이를 해소시켜줄 플레이리스트 아이템이라는 객체가 필요하다

따라서 플레이리스트 아이템을 생성해서 플레이 리스트와 음악과 연관관계를 맺워줘야 하는데 총 5번의 쿼리가 나간다.

1️⃣ 음악 조회 쿼리


public Music getMusic(
        Long musicId
) {
    Optional<Music> optional =
            musicRepository.findById(musicId);

    if (optional.isEmpty()) {
        throw new PlayListException(
                MUSIC_NOT_EXIST.getCode(),
                MUSIC_NOT_EXIST.getErrorMessage()
        );
    }

    return optional.get();
}

사용자가 보낸 음악아이디를 통해 해당 음악이 존재하는지 판단해야 한다.

만약 음악이 없을 경우 예외를 터뜨린다.

2️⃣ 플레이리스트 조회 쿼리


사용자가 저장하려고 하는 플레이리스트가 실제 존재하는지 확인해야 한다.

만약 플레이리스트가 없을 경우 예외를 터뜨린다.

또한 플레이리스트와 이를 수정하려는 회원 정보가 동일해야 한다.

@Query("""
		    select pl
		    from   PlayList pl
		    where  pl.id        = :playListId
		      and  pl.member.id = :memberId
""")
Optional<PlayList> findPlayListByPlayListId(Long playListId, Long memberId);

3️⃣ 플레이리스트 카운트 쿼리


플레이리스트의 최대 카운트 수는 1000이다.

즉, 플레이리스트 속 음악의 수가 1000일 경우 더이상 추가하면 안된다.

따라서 count 쿼리를 통해 조회한다.

@Query("""
		    select count(pli)
		    from   PlayListItem pli
		    where  pli.playList.id = :playListId
		      and  pli.playList.member.id = :memberId
""")
Long countItems(Long playListId, Long memberId);

4️⃣ 플레이리스트 마지막 순서 조회


플레이리스트의 최대 순서 값을 가지고 와야 한다.

플레이리스트의 각 음악은 순서 정보를 가지고 있고, 이때 마지막 순서의 순서 값이 필요하다.

이 순서값을 기반으로 이후 순서를 지정할 것이기 때문이다.

플레이리스트의 순서는 Gap Based Ordering 방식을 사용하므로 이에 대한 내용은 이 글에서 참고한다.

@Query("""
		    select coalesce(max(pli.position), 0)
		    from   PlayListItem pli
		    where  pli.playList.id = :playListId
		      and  pli.playList.member.id = :memberId
""")
Long findMaxPosition(Long playListId, Long memberId);

5️⃣ 플레이리스트 아이템 저장


이제 이 정보들을 토대로 만들어진 플레이리스트 아이템을 플레이리스트로 저장한다.

PlayListItem playListItem = new PlayListItem(
        lastOrder + GAP,
        playListStat.getPlayList(),
        music
);

// 5️⃣ 플레이리스트 아이템 저장
playListItemRepository.save(playListItem);

</aside>

쿼리를 줄여보자!


<aside>

2️⃣ 3️⃣ 4️⃣ 쿼리를 하나로 만들어보자

즉, 플레이리스트를 조회하면서 플레이리스트의 아이템의 수를 가지고 오고, 또한 플레이리스트 카운트도 한번에 가지고 오는 것이다.

여기서 핵심은 쿼리 자체가 조금 무거워지더라도 성능에서 효과적으로 가지고 올 수 있는지 체크하는 것이다.

@Query("""
        select  pl                            as playList,
                count(pi)                     as count,
                coalesce(max(pi.position), 0) as lastOrder
        from    PlayList pl
        left join PlayListItem pi on pi.playList = pl
        where   pl.id         = :playListId
          and   pl.member.id  = :memberId
        group by pl
""")
Optional<PlayListStatProjection> findPlayListWithStat(
        Long playListId,
        Long memberId
);

1️⃣ SELECT를 통해 원하는 정보 명시


우리가 원하는 것을 쿼리에 명시한다.

  1. 플레이리스트
  2. 플레이리스트 아이템의 수
  3. 플레이리스트 아이템의 가장 큰 MAX, 단 이때 만약 아무 행이 없다면 0을 반환한다.

2️⃣ Group By를 통해 하나의 플레이리스트만 가지고 온다.


기본적으로 지금같이 쿼리를 날리면 플레이리스트 아이템의 수만큼 플레이리스트 로우가 생성된다.

따라서 우리가 원하는 건 플레이리스트 한개 이므로, Group 화 시킨다.

</aside>

2. 성능테스트


프로세스


<aside>

플레이리스트가 10000개가 있고 각 플레이리스트 별 총 1000개의 음악을 가지고 있다고 가정하자

즉, 천만개의 데이터 셋에서 효과를 체크한다.

사용자는 5분간 5000명으로 점차 증가시키며 계속 요청을 보낸다. 또한 추가적으로 5분동안 5000명은 유지한다. 각 요청당 0.5ms 의 요청 제한을 둔다.

</aside>

1️⃣ 쿼리가 3개로 나뉘는 경우


<aside>

항목
총 요청 수 2,536,315 건
총 테스트 시간 10분 (601.1 초)
평균 처리량 (Throughput) 4,219.51 req/s
평균 HTTP 요청 응답 시간 388.03 ms
요청 실패율 0% (실패 없음)
</aside>

2️⃣ 쿼리를 1개로 통일한 경우