diff --git a/coupon-api/build.gradle.kts b/coupon-api/build.gradle.kts index 8f5702b..1badb8c 100644 --- a/coupon-api/build.gradle.kts +++ b/coupon-api/build.gradle.kts @@ -21,6 +21,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.redisson:redisson-spring-boot-starter:3.25.0") + // Cache + implementation("com.github.ben-manes.caffeine:caffeine") + // Monitoring implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus") diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AsyncLuaCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AsyncLuaCouponIssueService.kt index c52f661..94ad6f2 100644 --- a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AsyncLuaCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AsyncLuaCouponIssueService.kt @@ -3,6 +3,7 @@ package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl import main.kotlin.com.woong2e.couponsystem.coupon.application.port.out.CouponIssueEventPublisher import main.kotlin.com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse import main.kotlin.com.woong2e.couponsystem.coupon.application.service.CouponIssueService +import main.kotlin.com.woong2e.couponsystem.coupon.infra.cache.LocalCouponCacheManager import main.kotlin.com.woong2e.couponsystem.coupon.infra.redis.CouponRedisRepository import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException @@ -13,19 +14,28 @@ import java.util.UUID @Service("asyncLua") class AsyncLuaCouponIssueService( private val couponRedisRepository: CouponRedisRepository, - private val couponIssueEventPublisher: CouponIssueEventPublisher + private val couponIssueEventPublisher: CouponIssueEventPublisher, + private val localCouponCacheManager: LocalCouponCacheManager, ) : CouponIssueService { override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { - val result = couponRedisRepository.issueRequest(couponId.toString(), userId) + val key = couponId.toString() - when (result) { - "DUPLICATED" -> throw CustomException(CouponErrorStatus.COUPON_ALREADY_ISSUED) - "SOLD_OUT" -> throw CustomException(CouponErrorStatus.COUPON_OUT_OF_STOCK) + if (localCouponCacheManager.isSoldOut(key)) { + throw CustomException(CouponErrorStatus.COUPON_OUT_OF_STOCK) + } + + val result = couponRedisRepository.issueRequest(key, userId) + + return when (result) { "SUCCESS" -> { couponIssueEventPublisher.publish(couponId, userId) - - return CouponIssueResponse.asyncIssued() + CouponIssueResponse.asyncIssued() + } + "DUPLICATED" -> throw CustomException(CouponErrorStatus.COUPON_ALREADY_ISSUED) + "SOLD_OUT" -> { + localCouponCacheManager.putSoldOut(key) + throw CustomException(CouponErrorStatus.COUPON_OUT_OF_STOCK) } else -> throw CustomException(ErrorStatus.SYSTEM_BUSY) } diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/cache/LocalCouponCacheManager.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/cache/LocalCouponCacheManager.kt new file mode 100644 index 0000000..90dd871 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/cache/LocalCouponCacheManager.kt @@ -0,0 +1,23 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.infra.cache + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Component +class LocalCouponCacheManager { + + private val cache: Cache = Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) // 10분 후 만료 + .maximumSize(1000) // 최대 1000개 쿠폰 키 보관 + .build() + + fun putSoldOut(couponId: String) { + cache.put(couponId, true) + } + + fun isSoldOut(couponId: String): Boolean { + return cache.getIfPresent(couponId) == true + } +} \ No newline at end of file