From a22c0a5c808f88af549f6debe92d555f50f7036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=9E=AC=EC=9B=85=20=28JaeWoong=20Shin=29?= Date: Wed, 14 Jan 2026 00:52:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[build]:=20caffeine=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4=20(#1?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coupon-api/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) 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") From 1120bc376f464f0dac5fa1fe28dc3f025f71132d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EC=9E=AC=EC=9B=85=20=28JaeWoong=20Shin=29?= Date: Wed, 14 Jan 2026 00:53:25 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]:=20local=20cache=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EC=97=AC=20=EC=9E=AC=EA=B3=A0=20?= =?UTF-8?q?=EC=86=8C=EC=A7=84=20=EC=8B=9C=20=EB=A9=94=EB=AA=A8=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=99=95=EC=9D=B8=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=94=8C=EB=9E=98=EA=B7=B8=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=EB=91=90=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=EB=8B=A4=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/AsyncLuaCouponIssueService.kt | 24 +++++++++++++------ .../infra/cache/LocalCouponCacheManager.kt | 23 ++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/cache/LocalCouponCacheManager.kt 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