feat(recommend): 추천 팔로우 성공 로그를 커밋 후 기록한다

This commit is contained in:
2026-06-01 17:56:50 +09:00
parent da387f43a0
commit bb96f07872
2 changed files with 113 additions and 28 deletions

View File

@@ -5,16 +5,27 @@ import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
@Service
class RecommendedCreatorFollowService(
private val memberRepository: MemberRepository,
private val creatorFollowingRepository: CreatorFollowingRepository
) {
private val log = LoggerFactory.getLogger(javaClass)
@Transactional
fun followCreators(member: Member, creatorIds: List<Long>) {
val startedAt = System.currentTimeMillis()
var savedCount = 0
var reactivatedCount = 0
var skippedCount = 0
runCatching {
val distinctCreatorIds = creatorIds.distinct()
val creatorById = distinctCreatorIds
.filter { it != member.id }
@@ -25,6 +36,7 @@ class RecommendedCreatorFollowService(
distinctCreatorIds.forEach { creatorId ->
if (creatorId == member.id) {
skippedCount += 1
return@forEach
}
@@ -36,6 +48,9 @@ class RecommendedCreatorFollowService(
if (!existingFollowing.isActive) {
existingFollowing.isNotify = true
existingFollowing.isActive = true
reactivatedCount += 1
} else {
skippedCount += 1
}
return@forEach
}
@@ -46,6 +61,47 @@ class RecommendedCreatorFollowService(
creator = creatorById.getValue(creatorId)
}
)
savedCount += 1
}
distinctCreatorIds.size
}.onSuccess { distinctCount ->
afterCommit {
log.info(
"event=recommended_creator_follow_success " +
"memberId={} requestedCount={} distinctCount={} savedCount={} " +
"reactivatedCount={} skippedCount={} elapsedMs={}",
member.id,
creatorIds.size,
distinctCount,
savedCount,
reactivatedCount,
skippedCount,
System.currentTimeMillis() - startedAt
)
}
}.onFailure { ex ->
log.warn(
"event=recommended_creator_follow_failure memberId={} requestedCount={} distinctCount={} elapsedMs={} error={}",
member.id,
creatorIds.size,
creatorIds.distinct().size,
System.currentTimeMillis() - startedAt,
ex.message,
ex
)
throw ex
}
}
private fun afterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
return
}
TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() = action()
}
)
}
}

View File

@@ -14,16 +14,21 @@ import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.system.CapturedOutput
import org.springframework.boot.test.system.OutputCaptureExtension
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.persistence.EntityManager
@SpringBootTest
@Transactional
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@ExtendWith(OutputCaptureExtension::class)
class RecommendedCreatorFollowServiceTest @Autowired constructor(
private val service: RecommendedCreatorFollowService,
private val memberRepository: MemberRepository,
@@ -32,7 +37,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
) {
@Test
@DisplayName("신규 크리에이터만 팔로우 저장하고 이미 팔로우/본인 id는 서버 내부에서 제외한다")
fun shouldFollowOnlyNewCreatorsAndSkipExistingAndSelf() {
fun shouldFollowOnlyNewCreatorsAndSkipExistingAndSelf(output: CapturedOutput) {
val member = saveMember("viewer", MemberRole.USER)
val newCreator = saveMember("new-creator", MemberRole.CREATOR)
val followedCreator = saveMember("followed-creator", MemberRole.CREATOR)
@@ -40,16 +45,39 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
entityManager.flush()
entityManager.clear()
val beforeCount = TransactionSynchronizationManager.getSynchronizations().size
service.followCreators(
member = member,
creatorIds = listOf(newCreator.id!!, followedCreator.id!!, member.id!!)
)
entityManager.flush()
entityManager.clear()
TransactionSynchronizationManager.getSynchronizations().drop(beforeCount).forEach { it.afterCommit() }
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(newCreator.id!!, member.id!!))
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(followedCreator.id!!, member.id!!))
assertEquals(2, creatorFollowingRepository.findAll().size)
assertTrue(output.out.contains("event=recommended_creator_follow_success"))
assertTrue(output.out.contains("requestedCount=3"))
assertTrue(output.out.contains("savedCount=1"))
}
@Test
@DisplayName("추천 크리에이터 동시 팔로우 성공 로그는 트랜잭션 커밋 후 기록한다")
fun shouldLogFollowSuccessAfterTransactionCommit(output: CapturedOutput) {
val member = saveMember("after-commit-viewer", MemberRole.USER)
val creator = saveMember("after-commit-creator", MemberRole.CREATOR)
entityManager.flush()
entityManager.clear()
val beforeCount = TransactionSynchronizationManager.getSynchronizations().size
service.followCreators(member = member, creatorIds = listOf(creator.id!!))
assertEquals(false, output.out.contains("event=recommended_creator_follow_success"))
TransactionSynchronizationManager.getSynchronizations().drop(beforeCount).forEach { it.afterCommit() }
assertTrue(output.out.contains("event=recommended_creator_follow_success"))
}
@Test
@@ -119,7 +147,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
@Test
@DisplayName("존재하지 않는 id가 하나라도 포함되면 전체 실패하고 신규 저장하지 않는다")
fun shouldFailAllAndSaveNothingWhenAnyCreatorIdDoesNotExist() {
fun shouldFailAllAndSaveNothingWhenAnyCreatorIdDoesNotExist(output: CapturedOutput) {
val member = saveMember("viewer", MemberRole.USER)
val validCreator = saveMember("valid-creator", MemberRole.CREATOR)
entityManager.flush()
@@ -131,6 +159,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
assertEquals("member.validation.creator_not_found", exception.messageKey)
assertEquals(0, creatorFollowingRepository.findAll().size)
assertTrue(output.out.contains("event=recommended_creator_follow_failure"))
}
@Test