feat(recommend): 조회 이력 성공 로그를 커밋 후 기록한다

This commit is contained in:
2026-06-01 17:56:20 +09:00
parent 7ad514dcc0
commit da387f43a0
2 changed files with 68 additions and 5 deletions

View File

@@ -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()
}
)
} }
} }

View File

@@ -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 {