feat(recommend): 추천 팔로우 성공 로그를 커밋 후 기록한다
This commit is contained in:
@@ -5,16 +5,27 @@ import kr.co.vividnext.sodalive.member.Member
|
|||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class RecommendedCreatorFollowService(
|
class RecommendedCreatorFollowService(
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
private val creatorFollowingRepository: CreatorFollowingRepository
|
private val creatorFollowingRepository: CreatorFollowingRepository
|
||||||
) {
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun followCreators(member: Member, creatorIds: List<Long>) {
|
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 distinctCreatorIds = creatorIds.distinct()
|
||||||
val creatorById = distinctCreatorIds
|
val creatorById = distinctCreatorIds
|
||||||
.filter { it != member.id }
|
.filter { it != member.id }
|
||||||
@@ -25,6 +36,7 @@ class RecommendedCreatorFollowService(
|
|||||||
|
|
||||||
distinctCreatorIds.forEach { creatorId ->
|
distinctCreatorIds.forEach { creatorId ->
|
||||||
if (creatorId == member.id) {
|
if (creatorId == member.id) {
|
||||||
|
skippedCount += 1
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +48,9 @@ class RecommendedCreatorFollowService(
|
|||||||
if (!existingFollowing.isActive) {
|
if (!existingFollowing.isActive) {
|
||||||
existingFollowing.isNotify = true
|
existingFollowing.isNotify = true
|
||||||
existingFollowing.isActive = true
|
existingFollowing.isActive = true
|
||||||
|
reactivatedCount += 1
|
||||||
|
} else {
|
||||||
|
skippedCount += 1
|
||||||
}
|
}
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
@@ -46,6 +61,47 @@ class RecommendedCreatorFollowService(
|
|||||||
creator = creatorById.getValue(creatorId)
|
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()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,21 @@ import org.junit.jupiter.api.Assertions.assertThrows
|
|||||||
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.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
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.dao.DataIntegrityViolationException
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
import javax.persistence.EntityManager
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@Transactional
|
@Transactional
|
||||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||||
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
||||||
private val service: RecommendedCreatorFollowService,
|
private val service: RecommendedCreatorFollowService,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
@@ -32,7 +37,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
|||||||
) {
|
) {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("신규 크리에이터만 팔로우 저장하고 이미 팔로우/본인 id는 서버 내부에서 제외한다")
|
@DisplayName("신규 크리에이터만 팔로우 저장하고 이미 팔로우/본인 id는 서버 내부에서 제외한다")
|
||||||
fun shouldFollowOnlyNewCreatorsAndSkipExistingAndSelf() {
|
fun shouldFollowOnlyNewCreatorsAndSkipExistingAndSelf(output: CapturedOutput) {
|
||||||
val member = saveMember("viewer", MemberRole.USER)
|
val member = saveMember("viewer", MemberRole.USER)
|
||||||
val newCreator = saveMember("new-creator", MemberRole.CREATOR)
|
val newCreator = saveMember("new-creator", MemberRole.CREATOR)
|
||||||
val followedCreator = saveMember("followed-creator", MemberRole.CREATOR)
|
val followedCreator = saveMember("followed-creator", MemberRole.CREATOR)
|
||||||
@@ -40,16 +45,39 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
|||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
|
|
||||||
|
val beforeCount = TransactionSynchronizationManager.getSynchronizations().size
|
||||||
service.followCreators(
|
service.followCreators(
|
||||||
member = member,
|
member = member,
|
||||||
creatorIds = listOf(newCreator.id!!, followedCreator.id!!, member.id!!)
|
creatorIds = listOf(newCreator.id!!, followedCreator.id!!, member.id!!)
|
||||||
)
|
)
|
||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
|
TransactionSynchronizationManager.getSynchronizations().drop(beforeCount).forEach { it.afterCommit() }
|
||||||
|
|
||||||
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(newCreator.id!!, member.id!!))
|
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(newCreator.id!!, member.id!!))
|
||||||
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(followedCreator.id!!, member.id!!))
|
assertNotNull(creatorFollowingRepository.findByCreatorIdAndMemberId(followedCreator.id!!, member.id!!))
|
||||||
assertEquals(2, creatorFollowingRepository.findAll().size)
|
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
|
@Test
|
||||||
@@ -119,7 +147,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("존재하지 않는 id가 하나라도 포함되면 전체 실패하고 신규 저장하지 않는다")
|
@DisplayName("존재하지 않는 id가 하나라도 포함되면 전체 실패하고 신규 저장하지 않는다")
|
||||||
fun shouldFailAllAndSaveNothingWhenAnyCreatorIdDoesNotExist() {
|
fun shouldFailAllAndSaveNothingWhenAnyCreatorIdDoesNotExist(output: CapturedOutput) {
|
||||||
val member = saveMember("viewer", MemberRole.USER)
|
val member = saveMember("viewer", MemberRole.USER)
|
||||||
val validCreator = saveMember("valid-creator", MemberRole.CREATOR)
|
val validCreator = saveMember("valid-creator", MemberRole.CREATOR)
|
||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
@@ -131,6 +159,7 @@ class RecommendedCreatorFollowServiceTest @Autowired constructor(
|
|||||||
|
|
||||||
assertEquals("member.validation.creator_not_found", exception.messageKey)
|
assertEquals("member.validation.creator_not_found", exception.messageKey)
|
||||||
assertEquals(0, creatorFollowingRepository.findAll().size)
|
assertEquals(0, creatorFollowingRepository.findAll().size)
|
||||||
|
assertTrue(output.out.contains("event=recommended_creator_follow_failure"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user