Skip to content

Commit

Permalink
[Feat/#384] 구독 중복 신청 방지 위한 락 구현 (#387)
Browse files Browse the repository at this point in the history
* feat: AOP 관련 의존성 추가

* feat: 락 획득/해지 쿼리 구현

* feat: LockAspect 구현

* feat: SubscribeWorkbookUseCase Lock 적용

* feat: AspectConfig 추가
  • Loading branch information
belljun3395 authored Sep 9, 2024
1 parent e24b096 commit 9fdce8d
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 0 deletions.
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
77 changes: 77 additions & 0 deletions api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt
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}" }
}
}
7 changes: 7 additions & 0 deletions api/src/main/kotlin/com/few/api/domain/common/lock/LockFor.kt
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

0 comments on commit 9fdce8d

Please sign in to comment.