-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#146: 포즈 조회수 #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/#146: 포즈 조회수 #149
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.neki.pose.application.port | ||
|
|
||
| interface PoseViewCachePort { | ||
|
|
||
| fun addViewer(poseId: Long, userId: Long): Boolean | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.neki.pose.infra.cache.fake | ||
|
|
||
| import com.neki.pose.application.port.PoseViewCachePort | ||
| import org.springframework.context.annotation.Profile | ||
| import org.springframework.stereotype.Component | ||
| import java.util.concurrent.ConcurrentHashMap | ||
|
|
||
| @Component | ||
| @Profile("test") | ||
| class FakePoseViewCacheAdapter : PoseViewCachePort { | ||
|
|
||
| private val viewers = ConcurrentHashMap<Long, MutableSet<Long>>() | ||
|
|
||
| override fun addViewer(poseId: Long, userId: Long): Boolean { | ||
| val userSet = viewers.computeIfAbsent(poseId) { ConcurrentHashMap.newKeySet() } | ||
| return userSet.add(userId) | ||
| } | ||
|
|
||
| fun clearAll() { | ||
| viewers.clear() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.neki.pose.infra.cache.redis | ||
|
|
||
| internal object PoseViewRedisCacheKey { | ||
| private const val VIEW_PREFIX = "pose:view:" | ||
|
|
||
| fun viewKey(poseId: Long): String = "$VIEW_PREFIX$poseId" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package com.neki.pose.infra.cache.redis | ||
|
|
||
| import com.neki.pose.application.port.PoseViewCachePort | ||
| import org.slf4j.LoggerFactory | ||
| import org.springframework.context.annotation.Primary | ||
| import org.springframework.context.annotation.Profile | ||
| import org.springframework.data.redis.core.RedisTemplate | ||
| import org.springframework.stereotype.Component | ||
| import java.time.Instant | ||
| import java.time.LocalDate | ||
| import java.time.ZoneId | ||
|
|
||
| @Component | ||
| @Primary | ||
| @Profile("!test") | ||
| class RedisPoseViewCacheAdapter(private val redisTemplate: RedisTemplate<String, Any>) : PoseViewCachePort { | ||
|
|
||
| private val log = LoggerFactory.getLogger(javaClass) | ||
|
|
||
| override fun addViewer(poseId: Long, userId: Long): Boolean { | ||
| val key = PoseViewRedisCacheKey.viewKey(poseId) | ||
| return try { | ||
| val added = redisTemplate.opsForSet().add(key, userId) ?: 0 | ||
| if (added > 0) { | ||
| redisTemplate.expireAt(key, getMidnight()) | ||
| true | ||
| } else { | ||
| false | ||
| } | ||
| } catch (e: Exception) { | ||
| log.warn("[PoseViewCache] Failed to check viewer for pose: $poseId, user: $userId", e) | ||
| true // fail-open: Redis 장애 시 신규 조회로 처리 | ||
|
Comment on lines
+31
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fail-open strategy ( |
||
| } | ||
|
Comment on lines
21
to
33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of
val key = "${PoseViewRedisCacheKey.viewKey(poseId)}:$userId"
return try {
redisTemplate.opsForValue().setIfAbsent(key, true, VIEW_TTL) ?: false
} catch (e: Exception) {
log.warn("[PoseViewCache] Failed to check viewer for pose: $poseId, user: $userId", e)
true // fail-open: Redis 장애 시 신규 조회로 처리
} |
||
| } | ||
|
|
||
| private fun getMidnight(): Instant { | ||
| val tomorrow = LocalDate.now(KOREA_ZONE).plusDays(1) | ||
| return tomorrow.atStartOfDay(KOREA_ZONE).toInstant() | ||
| } | ||
|
|
||
| companion object { | ||
| private val KOREA_ZONE = ZoneId.of("Asia/Seoul") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE TB_POSE ADD COLUMN view_count BIGINT NOT NULL DEFAULT 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
transactionRunner.runblock is used here to increment the view count. While this ensures the database update is transactional, it's important to consider ifincrementViewCountitself needs to be part of a larger business transaction or if it can be an independent operation. Given thataddVieweris called outside this transaction, there's a potential for a view to be recorded in Redis but not in the database if the transaction fails later, or vice-versa ifaddViewerfails after the transaction commits. However, the current implementation's 'fail-open' for Redis suggests a preference for eventual consistency in view counts, so this might be acceptable. If strict consistency is required, theaddViewercall should also be part of the transaction or handled with compensating actions.