perf(explorer:creator-profile): 라이브방 목록 N+1 제거 및 예약/결제 여부 일괄 조회

- member 연관 로딩에 fetch join 적용으로 N+1 제거
- reservations 컬렉션 접근 제거 → QLiveReservation 기반 방 ID 일괄 조회로 isReservation 계산
- useCan per-room 조회 제거 → 방 ID 집합 일괄 조회(Set)로 isPaid 계산
- 기존 비즈니스 로직(날짜 포맷, 성인/성별 필터, PRIVATE 플래그 등) 유지
This commit is contained in:
2026-03-19 16:45:36 +09:00
parent 2e0f0c5044
commit fe093a942c

View File

@@ -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)
return result
.map {
val reservations = it.reservations
.filter { reservation ->
reservation.member!!.id!! == userMember.id!! && reservation.isActive
// N+1 방지: 한 번에 필요한 정보 일괄 조회
val roomIds = result.mapNotNull { it.id }.toSet()
if (roomIds.isEmpty()) {
return emptyList()
}
// 사용자 예약 여부를 방 ID 기준으로 일괄 조회
val reservationRoomIdSet: Set<Long> = 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<Long> = 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 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
)