fix(live-room): 최근 종료 라이브 조회와 캐시 무효화를 최적화한다
This commit is contained in:
17
docs/20260227_최근종료라이브최적화.md
Normal file
17
docs/20260227_최근종료라이브최적화.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 최근 종료 라이브(getLatestFinishedLive) 최적화
|
||||
|
||||
- [x] `getLatestFinishedLive` 조회를 DB 단계에서 차단 관계(`left join`)로 필터링하도록 변경
|
||||
- [x] 조회 결과를 `GetLatestFinishedLiveResponse`로 QueryProjection 하여 서비스 단 추가 `map` 제거
|
||||
- [x] 회원 차단(`memberBlock`) / 차단해제(`memberUnBlock`) 시 최근 종료 라이브 캐시 무효화 적용
|
||||
- [x] 정적 진단 및 테스트/빌드 검증 수행
|
||||
- [x] 검증 기록 작성
|
||||
|
||||
## 검증 기록
|
||||
|
||||
### 1차 구현
|
||||
- 무엇을: `getLatestFinishedLive`를 서비스 후처리(`filter`/`map`)에서 제거하고, `LiveRoomRepository`에서 `leftJoin(blockMember)` + `blockMember.id.isNull` 조건으로 차단 관계를 DB 단계에서 제외하도록 변경했다. 또한 `GetLatestFinishedLiveResponse`에 `@QueryProjection` 생성자를 추가해 쿼리 결과를 응답 DTO로 바로 생성했다. 마지막으로 `memberBlock`/`memberUnBlock`에서 `getLatestFinishedLive:{memberId}` 캐시를 즉시 evict 하도록 반영했다.
|
||||
- 왜: 기존 로직은 조회 후 애플리케이션 레벨에서 차단 여부를 반복 조회하고 별도 `map`을 수행해 비용이 컸고, 차단/차단해제 직후 최근 종료 라이브 캐시가 TTL 만료 전까지 stale 상태가 될 수 있어 DB 레벨 필터링 및 이벤트성 캐시 무효화가 필요했다.
|
||||
- 어떻게:
|
||||
- `lsp_diagnostics` (대상: `GetLatestFinishedLiveResponse.kt`, `LiveRoomRepository.kt`, `LiveRoomService.kt`, `MemberService.kt`) 실행 결과: **환경상 Kotlin LSP 미구성으로 진단 불가**
|
||||
- `./gradlew test && ./gradlew build` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||
- `./gradlew tasks --all` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||
@@ -6,13 +6,6 @@ import kr.co.vividnext.sodalive.extensions.getTimeAgoString
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
data class GetLatestFinishedLiveQueryResponse @QueryProjection constructor(
|
||||
val memberId: Long,
|
||||
val nickname: String,
|
||||
val profileImageUrl: String,
|
||||
val updatedAt: LocalDateTime
|
||||
)
|
||||
|
||||
data class GetLatestFinishedLiveResponse(
|
||||
@JsonProperty("memberId") val memberId: Long,
|
||||
@JsonProperty("nickname") val nickname: String,
|
||||
@@ -20,12 +13,18 @@ data class GetLatestFinishedLiveResponse(
|
||||
@JsonProperty("timeAgo") val timeAgo: String,
|
||||
@JsonProperty("dateUtc") val dateUtc: String
|
||||
) {
|
||||
constructor(response: GetLatestFinishedLiveQueryResponse) : this(
|
||||
response.memberId,
|
||||
response.nickname,
|
||||
response.profileImageUrl,
|
||||
response.updatedAt.getTimeAgoString(),
|
||||
response.updatedAt
|
||||
@QueryProjection
|
||||
constructor(
|
||||
memberId: Long,
|
||||
nickname: String,
|
||||
profileImageUrl: String,
|
||||
updatedAt: LocalDateTime
|
||||
) : this(
|
||||
memberId,
|
||||
nickname,
|
||||
profileImageUrl,
|
||||
updatedAt.getTimeAgoString(),
|
||||
updatedAt
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toString()
|
||||
|
||||
@@ -66,7 +66,7 @@ interface LiveRoomQueryRepository {
|
||||
fun getTotalHeartCount(roomId: Long): Int?
|
||||
fun getLiveRoomCreatorId(roomId: Long): Long?
|
||||
fun getHeartList(roomId: Long): List<GetLiveRoomHeartListItem>
|
||||
fun getLatestFinishedLive(): List<GetLatestFinishedLiveQueryResponse>
|
||||
fun getLatestFinishedLive(memberId: Long?): List<GetLatestFinishedLiveResponse>
|
||||
}
|
||||
|
||||
class LiveRoomQueryRepositoryImpl(
|
||||
@@ -486,10 +486,16 @@ class LiveRoomQueryRepositoryImpl(
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getLatestFinishedLive(): List<GetLatestFinishedLiveQueryResponse> {
|
||||
return queryFactory
|
||||
override fun getLatestFinishedLive(memberId: Long?): List<GetLatestFinishedLiveResponse> {
|
||||
var where = liveRoom.isActive.isFalse
|
||||
.and(liveRoom.channelName.isNotNull)
|
||||
.and(liveRoom.updatedAt.goe(LocalDateTime.now().minusWeeks(1)))
|
||||
.and(member.isActive.isTrue)
|
||||
.and(member.role.eq(MemberRole.CREATOR))
|
||||
|
||||
var select = queryFactory
|
||||
.select(
|
||||
QGetLatestFinishedLiveQueryResponse(
|
||||
QGetLatestFinishedLiveResponse(
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage.prepend("/").prepend(cloudFrontHost),
|
||||
@@ -498,13 +504,24 @@ class LiveRoomQueryRepositoryImpl(
|
||||
)
|
||||
.from(liveRoom)
|
||||
.innerJoin(liveRoom.member, member)
|
||||
.where(
|
||||
liveRoom.isActive.isFalse
|
||||
.and(liveRoom.channelName.isNotNull)
|
||||
.and(liveRoom.updatedAt.goe(LocalDateTime.now().minusWeeks(1)))
|
||||
.and(member.isActive.isTrue)
|
||||
.and(member.role.eq(MemberRole.CREATOR))
|
||||
|
||||
if (memberId != null) {
|
||||
val blockMemberCondition = blockMember.isActive.isTrue
|
||||
.and(
|
||||
blockMember.member.id.eq(member.id)
|
||||
.and(blockMember.blockedMember.id.eq(memberId))
|
||||
.or(
|
||||
blockMember.member.id.eq(memberId)
|
||||
.and(blockMember.blockedMember.id.eq(member.id))
|
||||
)
|
||||
)
|
||||
|
||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||
where = where.and(blockMember.id.isNull)
|
||||
}
|
||||
|
||||
return select
|
||||
.where(where)
|
||||
.groupBy(member.id)
|
||||
.orderBy(liveRoom.updatedAt.max().desc())
|
||||
.limit(20)
|
||||
|
||||
@@ -1444,20 +1444,9 @@ class LiveRoomService(
|
||||
@Transactional(readOnly = true)
|
||||
@Cacheable(
|
||||
cacheNames = ["cache_ttl_10_minutes"],
|
||||
key = "'getLatestFinishedLive:' + (#member ?: 'guest')"
|
||||
key = "'getLatestFinishedLive:' + (#member?.id ?: 'guest')"
|
||||
)
|
||||
fun getLatestFinishedLive(member: Member?): List<GetLatestFinishedLiveResponse> {
|
||||
return repository.getLatestFinishedLive()
|
||||
.filter {
|
||||
if (member?.id != null) {
|
||||
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.memberId) &&
|
||||
!blockMemberRepository.isBlocked(blockedMemberId = it.memberId, memberId = member.id!!)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.map {
|
||||
GetLatestFinishedLiveResponse(response = it)
|
||||
}
|
||||
return repository.getLatestFinishedLive(memberId = member?.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ class MemberService(
|
||||
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
|
||||
|
||||
private val recommendLiveCacheKeyPrefix = "getRecommendLive:"
|
||||
private val latestFinishedLiveCacheKeyPrefix = "getLatestFinishedLive:"
|
||||
|
||||
@Transactional
|
||||
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
|
||||
@@ -564,7 +565,11 @@ class MemberService(
|
||||
}
|
||||
|
||||
evictRecommendLiveCache(memberId)
|
||||
blockTargetMemberIds.forEach { evictRecommendLiveCache(it) }
|
||||
evictLatestFinishedLiveCache(memberId)
|
||||
blockTargetMemberIds.forEach {
|
||||
evictRecommendLiveCache(it)
|
||||
evictLatestFinishedLiveCache(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -580,6 +585,8 @@ class MemberService(
|
||||
|
||||
evictRecommendLiveCache(memberId)
|
||||
evictRecommendLiveCache(request.blockMemberId)
|
||||
evictLatestFinishedLiveCache(memberId)
|
||||
evictLatestFinishedLiveCache(request.blockMemberId)
|
||||
}
|
||||
|
||||
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
||||
@@ -843,6 +850,10 @@ class MemberService(
|
||||
cacheManager.getCache("cache_ttl_3_hours")?.evict(recommendLiveCacheKeyPrefix + memberId)
|
||||
}
|
||||
|
||||
private fun evictLatestFinishedLiveCache(memberId: Long) {
|
||||
cacheManager.getCache("cache_ttl_10_minutes")?.evict(latestFinishedLiveCacheKeyPrefix + memberId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
|
||||
val member = repository.findByIdOrNull(id = memberId)
|
||||
|
||||
Reference in New Issue
Block a user