From fe093a942c562a7e49c6fdadaa46e2bb2ca1f67d Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 19 Mar 2026 16:45:36 +0900 Subject: [PATCH] =?UTF-8?q?perf(explorer:creator-profile):=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=B0=A9=20=EB=AA=A9=EB=A1=9D=20N+1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=98=88=EC=95=BD/=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=97=AC=EB=B6=80=20=EC=9D=BC=EA=B4=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - member 연관 로딩에 fetch join 적용으로 N+1 제거 - reservations 컬렉션 접근 제거 → QLiveReservation 기반 방 ID 일괄 조회로 isReservation 계산 - useCan per-room 조회 제거 → 방 ID 집합 일괄 조회(Set)로 isPaid 계산 - 기존 비즈니스 로직(날짜 포맷, 성인/성별 필터, PRIVATE 플래그 등) 유지 --- .../explorer/ExplorerQueryRepository.kt | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index 8182b926..4a906d7b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -23,6 +23,7 @@ import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource +import kr.co.vividnext.sodalive.live.reservation.QLiveReservation import kr.co.vividnext.sodalive.live.room.GenderRestriction import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.live.room.LiveRoomType @@ -374,7 +375,7 @@ class ExplorerQueryRepository( result.addAll( queryFactory .selectFrom(liveRoom) - .innerJoin(liveRoom.member, member) + .innerJoin(liveRoom.member, member).fetchJoin() .leftJoin(liveRoom.cancel, liveRoomCancel) .where(where) .orderBy(liveRoom.beginDateTime.asc()) @@ -388,13 +389,43 @@ class ExplorerQueryRepository( val dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimePattern) .withLocale(langContext.lang.locale) + // N+1 방지: 한 번에 필요한 정보 일괄 조회 + val roomIds = result.mapNotNull { it.id }.toSet() + if (roomIds.isEmpty()) { + return emptyList() + } + + // 사용자 예약 여부를 방 ID 기준으로 일괄 조회 + val reservationRoomIdSet: Set = run { + // Q 클래스는 의존 파일들에서 사용되는 패턴을 맞춰 import 없이 정규 참조 + val resIds = queryFactory + .select(QLiveReservation.liveReservation.room.id) + .from(QLiveReservation.liveReservation) + .where( + QLiveReservation.liveReservation.room.id.`in`(roomIds) + .and(QLiveReservation.liveReservation.member.id.eq(userMember.id)) + .and(QLiveReservation.liveReservation.isActive.isTrue) + ) + .fetch() + resIds.filterNotNull().toSet() + } + + // 결제 여부를 방 ID 기준으로 일괄 조회 (CanUsage.LIVE) + val paidRoomIdSet: Set = run { + val ids = queryFactory + .select(useCan.room.id) + .from(useCan) + .where( + useCan.room.id.`in`(roomIds) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + ) + .groupBy(useCan.room.id) + .fetch() + ids.filterNotNull().toSet() + } + return result .map { - val reservations = it.reservations - .filter { reservation -> - reservation.member!!.id!! == userMember.id!! && reservation.isActive - } - val beginDateTime = it.beginDateTime .atZone(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of(timezone)) @@ -403,22 +434,7 @@ class ExplorerQueryRepository( val beginDateTimeUtc = it.beginDateTime .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - val isPaid = if (it.channelName != null) { - val useCan = queryFactory - .selectFrom(useCan) - .innerJoin(useCan.member, member) - .where( - useCan.member.id.eq(member.id) - .and(useCan.room.id.eq(it.id!!)) - .and(useCan.canUsage.eq(CanUsage.LIVE)) - ) - .orderBy(useCan.id.desc()) - .fetchFirst() - - useCan != null - } else { - false - } + val isPaid = it.channelName != null && paidRoomIdSet.contains(it.id!!) LiveRoomResponse( roomId = it.id!!, @@ -441,7 +457,7 @@ class ExplorerQueryRepository( else -> "$cloudFrontHost/$profileImage" } }, - isReservation = reservations.isNotEmpty(), + isReservation = reservationRoomIdSet.contains(it.id!!), isActive = it.isActive, isPrivateRoom = it.type == LiveRoomType.PRIVATE )