feat(recommend): 조회 이력 성공 로그를 커밋 후 기록한다
This commit is contained in:
@@ -2,20 +2,36 @@ package kr.co.vividnext.sodalive.v2.recommend.application
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryPort
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryRecord
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryRecord
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Propagation
|
import org.springframework.transaction.annotation.Propagation
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class CreatorContentViewHistoryService(
|
class CreatorContentViewHistoryService(
|
||||||
private val port: CreatorContentViewHistoryPort
|
private val port: CreatorContentViewHistoryPort
|
||||||
) {
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
fun recordView(memberId: Long?, contentId: Long, viewedAt: LocalDateTime = LocalDateTime.now()) {
|
fun recordView(memberId: Long?, contentId: Long, viewedAt: LocalDateTime = LocalDateTime.now()) {
|
||||||
if (memberId == null) return
|
if (memberId == null) {
|
||||||
|
log.info("event=creator_content_view_history_record_skipped reason=anonymous contentId={}", contentId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val genreId = port.findGenreIdByContentId(contentId) ?: return
|
val genreId = port.findGenreIdByContentId(contentId)
|
||||||
|
if (genreId == null) {
|
||||||
|
log.info(
|
||||||
|
"event=creator_content_view_history_record_skipped reason=genre_not_found memberId={} contentId={}",
|
||||||
|
memberId,
|
||||||
|
contentId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
port.save(
|
port.save(
|
||||||
CreatorContentViewHistoryRecord(
|
CreatorContentViewHistoryRecord(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
@@ -24,5 +40,25 @@ class CreatorContentViewHistoryService(
|
|||||||
viewedAt = viewedAt
|
viewedAt = viewedAt
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
afterCommit {
|
||||||
|
log.info(
|
||||||
|
"event=creator_content_view_history_record_success memberId={} contentId={} genreId={}",
|
||||||
|
memberId,
|
||||||
|
contentId,
|
||||||
|
genreId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun afterCommit(action: () -> Unit) {
|
||||||
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
action()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(
|
||||||
|
object : TransactionSynchronization {
|
||||||
|
override fun afterCommit() = action()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.boot.test.system.CapturedOutput
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class CreatorContentViewHistoryServiceTest {
|
class CreatorContentViewHistoryServiceTest {
|
||||||
private val port = FakeCreatorContentViewHistoryPort()
|
private val port = FakeCreatorContentViewHistoryPort()
|
||||||
private val service = CreatorContentViewHistoryService(port)
|
private val service = CreatorContentViewHistoryService(port)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("인증 회원의 콘텐츠 상세 진입 시 memberId/contentId/genreId/viewedAt을 저장한다")
|
@DisplayName("인증 회원의 콘텐츠 상세 진입 시 memberId/contentId/genreId/viewedAt을 저장한다")
|
||||||
fun shouldRecordAuthenticatedMemberContentViewWithGenreAndViewedAt() {
|
fun shouldRecordAuthenticatedMemberContentViewWithGenreAndViewedAt(output: CapturedOutput) {
|
||||||
val viewedAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
val viewedAt = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||||
port.genreIdByContentId[20L] = 30L
|
port.genreIdByContentId[20L] = 30L
|
||||||
|
|
||||||
@@ -31,22 +36,44 @@ class CreatorContentViewHistoryServiceTest {
|
|||||||
),
|
),
|
||||||
port.savedRecords
|
port.savedRecords
|
||||||
)
|
)
|
||||||
|
assertTrue(output.out.contains("event=creator_content_view_history_record_success"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("조회 이력 저장 성공 로그는 트랜잭션 커밋 후 기록한다")
|
||||||
|
fun shouldLogRecordSuccessAfterTransactionCommit(output: CapturedOutput) {
|
||||||
|
port.genreIdByContentId[20L] = 30L
|
||||||
|
TransactionSynchronizationManager.initSynchronization()
|
||||||
|
try {
|
||||||
|
service.recordView(memberId = 10L, contentId = 20L)
|
||||||
|
|
||||||
|
assertEquals(false, output.out.contains("event=creator_content_view_history_record_success"))
|
||||||
|
TransactionSynchronizationManager.getSynchronizations().forEach { it.afterCommit() }
|
||||||
|
} finally {
|
||||||
|
TransactionSynchronizationManager.clearSynchronization()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("event=creator_content_view_history_record_success"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("비회원 콘텐츠 상세 진입은 조회 이력을 저장하지 않는다")
|
@DisplayName("비회원 콘텐츠 상세 진입은 조회 이력을 저장하지 않는다")
|
||||||
fun shouldNotRecordAnonymousContentView() {
|
fun shouldNotRecordAnonymousContentView(output: CapturedOutput) {
|
||||||
service.recordView(memberId = null, contentId = 20L)
|
service.recordView(memberId = null, contentId = 20L)
|
||||||
|
|
||||||
assertTrue(port.savedRecords.isEmpty())
|
assertTrue(port.savedRecords.isEmpty())
|
||||||
|
assertTrue(output.out.contains("event=creator_content_view_history_record_skipped"))
|
||||||
|
assertTrue(output.out.contains("reason=anonymous"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("콘텐츠의 활성 장르를 찾지 못하면 조회 이력을 저장하지 않는다")
|
@DisplayName("콘텐츠의 활성 장르를 찾지 못하면 조회 이력을 저장하지 않는다")
|
||||||
fun shouldNotRecordWhenContentGenreDoesNotExist() {
|
fun shouldNotRecordWhenContentGenreDoesNotExist(output: CapturedOutput) {
|
||||||
service.recordView(memberId = 10L, contentId = 20L)
|
service.recordView(memberId = 10L, contentId = 20L)
|
||||||
|
|
||||||
assertTrue(port.savedRecords.isEmpty())
|
assertTrue(port.savedRecords.isEmpty())
|
||||||
|
assertTrue(output.out.contains("event=creator_content_view_history_record_skipped"))
|
||||||
|
assertTrue(output.out.contains("reason=genre_not_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeCreatorContentViewHistoryPort : CreatorContentViewHistoryPort {
|
private class FakeCreatorContentViewHistoryPort : CreatorContentViewHistoryPort {
|
||||||
|
|||||||
Reference in New Issue
Block a user