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

[Feat/#384] 구독 중복 신청 방지 위한 락 구현 #387

Merged
merged 6 commits into from
Sep 9, 2024
Merged
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 @@ -18,6 +18,21 @@ import java.time.LocalDateTime
class SubscriptionDao(
private val dslContext: DSLContext,
) {
fun getLock(memberId: Long, workbookId: Long, timeout: Int = 5): Boolean {
return dslContext.fetch(
"""
SELECT GET_LOCK(CONCAT('subscription_', $memberId, '_', $workbookId), $timeout);
"""
).into(Int::class.java).first() == 1
}

fun releaseLock(memberId: Long, workbookId: Long): Boolean {
return dslContext.fetch(
"""
SELECT RELEASE_LOCK(CONCAT('subscription_', $memberId, '_', $workbookId));
"""
).into(Int::class.java).first() == 1
}

fun insertWorkbookSubscription(command: InsertWorkbookSubscriptionCommand) {
insertWorkbookSubscriptionCommand(command)
Expand Down
4 changes: 4 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-aop")

/** jwt */
implementation("io.jsonwebtoken:jjwt-api:${DependencyVersion.JWT}")
implementation("io.jsonwebtoken:jjwt-impl:${DependencyVersion.JWT}")
implementation("io.jsonwebtoken:jjwt-jackson:${DependencyVersion.JWT}")

/** aspectj */
implementation("org.aspectj:aspectjweaver:1.9.5")

/** scrimage */
implementation("com.sksamuel.scrimage:scrimage-core:${DependencyVersion.SCRIMAGE}")
/** for convert to webp */
Expand Down
8 changes: 8 additions & 0 deletions api/src/main/kotlin/com/few/api/config/AspectConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.few.api.config

import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.EnableAspectJAutoProxy

@Configuration
@EnableAspectJAutoProxy
class AspectConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.few.api.domain.common.lock

import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookUseCaseIn
import com.few.api.repo.dao.subscription.SubscriptionDao
import io.github.oshai.kotlinlogging.KotlinLogging
import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.AfterThrowing
import org.aspectj.lang.JoinPoint
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.aspectj.lang.annotation.Pointcut
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.stereotype.Component

@Aspect
@Component
class LockAspect(
private val subscriptionDao: SubscriptionDao,
) {
private val log = KotlinLogging.logger {}

@Pointcut("@annotation(com.few.api.domain.common.lock.LockFor)")
fun lockPointcut() {}

@Before("lockPointcut()")
fun before(joinPoint: JoinPoint) {
getLockFor(joinPoint).run {
when (this.identifier) {
LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> {
val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn
getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn)
}
}
}
}

private fun getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) {
subscriptionDao.getLock(useCaseIn.memberId, useCaseIn.workbookId).run {
if (!this) {
throw IllegalStateException("Already in progress for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}")
}
log.debug { "Lock acquired for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" }
}
}

@AfterReturning("lockPointcut()")
fun afterReturning(joinPoint: JoinPoint) {
getLockFor(joinPoint).run {
when (this.identifier) {
LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> {
val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn
releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn)
}
}
}
}

@AfterThrowing("lockPointcut()")
fun afterThrowing(joinPoint: JoinPoint) {
getLockFor(joinPoint).run {
when (this.identifier) {
LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> {
val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn
releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn)
}
}
}
}

private fun getLockFor(joinPoint: JoinPoint) =
(joinPoint.signature as MethodSignature).method.getAnnotation(LockFor::class.java)

private fun releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) {
subscriptionDao.releaseLock(useCaseIn.memberId, useCaseIn.workbookId)
log.debug { "Lock released for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.few.api.domain.common.lock

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LockFor(
val identifier: LockIdentifier,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.few.api.domain.common.lock

enum class LockIdentifier {
/**
* 구독 테이블에 멤버와 워크북을 기준으로 락을 건다.
*/
SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID,
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.few.api.domain.subscription.usecase

import com.few.api.domain.common.lock.LockFor
import com.few.api.domain.common.lock.LockIdentifier
import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent
import com.few.api.repo.dao.subscription.SubscriptionDao
import com.few.api.repo.dao.subscription.command.InsertWorkbookSubscriptionCommand
Expand All @@ -21,6 +23,7 @@ class SubscribeWorkbookUseCase(
private val applicationEventPublisher: ApplicationEventPublisher,
) {

@LockFor(LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID)
@Transactional
fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) {
val subTargetWorkbookId = useCaseIn.workbookId
Expand Down
Loading