Skip to content
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

[Vacgom-119] 초대코드 조회 실패 시 레디스 롤백 예외 처리 #63

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kr.co.vacgom.api.invitation.domain.InvitationCode
import kr.co.vacgom.api.invitation.exception.InvitationError
import kr.co.vacgom.api.invitation.presentation.dto.InvitationDto
import kr.co.vacgom.api.invitation.repository.InvitationRepository
import org.slf4j.Logger
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*
Expand All @@ -18,7 +19,9 @@ class InvitationService(
private val invitationRepository: InvitationRepository,
private val babyManagerService: BabyManagerService,
private val babyQueryService: BabyQueryService,
private val logger: Logger,
) {
@Transactional
fun createInvitationCodeByBabyId(userId: UUID, babyId: UUID): InvitationDto.Response.Create {
val key = UuidCreator.create().toString()

Expand All @@ -42,19 +45,29 @@ class InvitationService(
return InvitationDto.Response.Create(invitationCode = key)
}

@Transactional
@Transactional(readOnly = true)
fun getBabiesByInvitationCode(code: String): List<BabyDto.Response.Detail> {
val invitationCode = invitationRepository.getAndDeleteInvitationCode(code)
?: throw BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND)

return babyQueryService.getBabiesById(invitationCode.babyIds).map {
BabyDto.Response.Detail(
id = it.id,
name = it.name,
profileImg = it.profileImg,
gender = it.gender,
birthday = it.birthday,
)
}
return runCatching {
val invitationCode = invitationRepository.getInvitationCodeAndUpdateExpired(code)

babyQueryService.getBabiesById(invitationCode.babyIds).map {
BabyDto.Response.Detail(
id = it.id,
name = it.name,
profileImg = it.profileImg,
gender = it.gender,
birthday = it.birthday,
)
}
}.onFailure {
if (it is BusinessException) {
if (it.errorCode == InvitationError.INVITATION_CODE_NOT_FOUND) {
throw it
}
}

invitationRepository.rollBackInvitationCodeExpired(code)
logger.warn("Expired Invitation Code roll back to redis")
}.getOrThrow()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ data class InvitationCode(
val key: String,
val userId: UUID,
val babyIds: List<UUID>,
var isExpired: Boolean = false,
val timeToLive: Long = 86400L,
val createdAt: LocalDateTime = LocalDateTime.now()
val createdAt: LocalDateTime = LocalDateTime.now(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ class InvitationRedisRepository(
.also { value -> redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS)}
}

fun getAndDeleteInvitationCode(code: String): InvitationCode? {
val queryKey = INVITATION_CODE_KEY_PREFIX + code
val value = redisTemplate.opsForValue().getAndDelete(queryKey)
fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode? {
val key = INVITATION_CODE_KEY_PREFIX + code
return redisTemplate.execute { getInvitationCodeAndUpdateExpired(key, true) }
}

fun rollBackExpiredInvitationCode(code: String) {
val key = INVITATION_CODE_KEY_PREFIX + code
redisTemplate.execute { getInvitationCodeAndUpdateExpired(key, false) }
}

private fun getInvitationCodeAndUpdateExpired(key: String, expireStatus: Boolean): InvitationCode? {
val value = redisTemplate.opsForValue().get(key) ?: return null

return objectMapper.readValue(value.toString(), InvitationCode::class.java)
.apply { isExpired = expireStatus }
.also { redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(it)) }
}

companion object{
companion object {
const val INVITATION_CODE_KEY_PREFIX = "invitation_code:"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import kr.co.vacgom.api.invitation.domain.InvitationCode

interface InvitationRepository {
fun save(invitationCode: InvitationCode, ttl: Long)
fun getAndDeleteInvitationCode(code: String): InvitationCode?
fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode
fun rollBackInvitationCodeExpired(code: String)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package kr.co.vacgom.api.invitation.repository

import kr.co.vacgom.api.global.exception.error.BusinessException
import kr.co.vacgom.api.invitation.domain.InvitationCode
import kr.co.vacgom.api.invitation.exception.InvitationError
import org.springframework.stereotype.Repository

@Repository
Expand All @@ -11,7 +13,12 @@ class InvitationRepositoryAdapter(
invitationRedisRepository.save(invitationCode, ttl)
}

override fun getAndDeleteInvitationCode(code: String): InvitationCode? {
return invitationRedisRepository.getAndDeleteInvitationCode(code)
override fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode {
return invitationRedisRepository.getInvitationCodeAndUpdateExpired(code) ?:
throw BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND)
}

override fun rollBackInvitationCodeExpired(code: String) {
invitationRedisRepository.rollBackExpiredInvitationCode(code)
}
}
9 changes: 1 addition & 8 deletions src/main/kotlin/kr/co/vacgom/api/redis/RedissonConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,7 @@ class RedissonConfig(
connectionFactory = redisConnectionFactory
keySerializer = StringRedisSerializer()
valueSerializer = serializer
setEnableTransactionSupport(true)
}
}

// @Bean
// fun objectMapper(): ObjectMapper {
// return ObjectMapper().apply {
// registerModule(KotlinModule.Builder().build())
// registerModule(JavaTimeModule())
// }
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ import kr.co.vacgom.api.invitation.exception.InvitationError
import kr.co.vacgom.api.invitation.repository.InvitationRepository
import kr.co.vacgom.api.user.domain.User
import kr.co.vacgom.api.user.domain.enums.UserRole
import org.slf4j.Logger
import java.time.LocalDate
import java.util.*

class InvitationServiceTest : DescribeSpec({
val invitationRepositoryMock: InvitationRepository = mockk(relaxed = true)
val babyManagerServiceMock: BabyManagerService = mockk(relaxed = true)
val babyQueryServiceMock: BabyQueryService = mockk(relaxed = true)
val loggerMock: Logger = mockk(relaxed = true)

val sut = InvitationService(
invitationRepositoryMock,
babyManagerServiceMock,
babyQueryServiceMock,
logger = loggerMock
)

describe("초대 코드 생성 테스트") {
Expand Down Expand Up @@ -122,7 +125,7 @@ class InvitationServiceTest : DescribeSpec({

context("초대 코드를 정상적으로 조회한다면") {
it("해당하는 아기 상세 정보를 반환한다.") {
every { invitationRepositoryMock.getAndDeleteInvitationCode(invitationCode.key) } returns invitationCode
every { invitationRepositoryMock.getInvitationCodeAndUpdateExpired(invitationCode.key) } returns invitationCode
every { babyQueryServiceMock.getBabiesById(babies.map { it.id }) } returns babies

val result = sut.getBabiesByInvitationCode(invitationCode.key)
Expand All @@ -140,7 +143,7 @@ class InvitationServiceTest : DescribeSpec({
context("초대 코드가 존재하지 않는다면") {
it("${InvitationError.INVITATION_CODE_NOT_FOUND} 예외가 발생한다.") {
every {
invitationRepositoryMock.getAndDeleteInvitationCode(invitationCode.key)
invitationRepositoryMock.getInvitationCodeAndUpdateExpired(invitationCode.key)
} throws BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND)

val result = shouldThrow<BusinessException> { sut.getBabiesByInvitationCode(invitationCode.key) }
Expand Down