diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 7b85bd2..6f77e3e 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -40,26 +40,42 @@ jobs: - name: Build & Run Tests run: ./gradlew clean build - - name: Upload Build Artifact + # [API] JAR 파일 업로드 + - name: Upload API Artifact uses: actions/upload-artifact@v4 with: - name: build-artifacts - path: build/libs/*.jar + name: api-jar + path: coupon-api/build/libs/*.jar + + # [Consumer] JAR 파일 업로드 + - name: Upload Consumer Artifact + uses: actions/upload-artifact@v4 + with: + name: consumer-jar + path: coupon-consumer/build/libs/*.jar # 2. 도커 이미지 빌드 및 푸시 docker-build-and-push: - name: Build & Push Docker Image + name: Build & Push Docker Images runs-on: ubuntu-latest needs: build-and-test steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Download Build Artifact + # (1) API JAR 다운로드 + - name: Download API Artifact + uses: actions/download-artifact@v4 + with: + name: api-jar + path: coupon-api/build/libs/ + + # (2) Consumer JAR 다운로드 + - name: Download Consumer Artifact uses: actions/download-artifact@v4 with: - name: build-artifacts - path: build/libs/ + name: consumer-jar + path: coupon-consumer/build/libs/ - name: Log in to Docker Hub uses: docker/login-action@v3 @@ -67,10 +83,17 @@ jobs: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - name: Build and Push Docker Image + # (3) API 이미지 빌드 & 푸시 + - name: Build & Push API Image + run: | + docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/coupon-api:latest -f coupon-api/Dockerfile coupon-api + docker push ${{ secrets.DOCKER_HUB_USERNAME }}/coupon-api:latest + + # (4) Consumer 이미지 빌드 & 푸시 + - name: Build & Push Consumer Image run: | - docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/couponsystem:latest . - docker push ${{ secrets.DOCKER_HUB_USERNAME }}/couponsystem:latest + docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/coupon-consumer:latest -f coupon-consumer/Dockerfile coupon-consumer + docker push ${{ secrets.DOCKER_HUB_USERNAME }}/coupon-consumer:latest # 3. 서버 배포 deploy: @@ -84,18 +107,19 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - # (1) 설정 파일들 서버로 전송 (SCP) + # (1) 설정 파일 전송 (docker-compose 폴더 등) - name: Copy Config Files via SCP uses: appleboy/scp-action@master with: host: ${{ secrets.AWS_EC2_HOST }} username: ${{ secrets.AWS_EC2_USER }} key: ${{ secrets.AWS_SSH_PRIVATE_KEY }} + # docker-compose 폴더, nginx, deploy 스크립트 등 필요한 파일 전송 source: "docker-compose/*,nginx/*,deploy/*" target: "/home/${{ secrets.AWS_EC2_USER }}/deploy" strip_components: 1 - # (2) 서버 접속, .env 생성 및 배포 스크립트 실행 + # (2) 배포 스크립트 실행 - name: Execute Deploy Script uses: appleboy/ssh-action@v1.0.3 env: @@ -107,8 +131,14 @@ jobs: envs: ENV_FILE_CONTENT script: | cd /home/${{ secrets.AWS_EC2_USER }}/deploy - + + # .env 파일 생성 echo "$ENV_FILE_CONTENT" > .env - + + # 스크립트 위치 확인 및 실행 + if [ -f "deploy/deploy.sh" ]; then + cd deploy + fi + chmod +x deploy.sh ./deploy.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 726f44e..104b6c6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ out/ /mysql-data-local/ /src/main/resources/.env /src/main/resources/application.yml +/coupon-consumer/src/main/resources/.env +/coupon-api/src/main/resources/.env diff --git a/build.gradle.kts b/build.gradle.kts index dfd0bac..f535d84 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,57 +1,49 @@ -plugins { - kotlin("jvm") version "2.2.21" - kotlin("plugin.spring") version "2.2.21" - kotlin("plugin.jpa") version "2.2.21" - id("org.springframework.boot") version "4.0.0" - id("io.spring.dependency-management") version "1.1.7" -} +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -group = "com.woong2e" -version = "0.0.1-SNAPSHOT" -description = "FCFS-EventSystem" - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -repositories { - mavenCentral() +plugins { + kotlin("jvm") version "1.9.25" apply false + kotlin("plugin.spring") version "1.9.25" apply false + kotlin("plugin.jpa") version "1.9.25" apply false + id("org.springframework.boot") version "3.4.1" apply false + id("io.spring.dependency-management") version "1.1.7" apply false } -dependencies { - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springframework.boot:spring-boot-starter-validation") - - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.jetbrains.kotlin:kotlin-stdlib") +allprojects { + group = "com.woong2e" + version = "0.0.1-SNAPSHOT" - implementation("org.springframework.boot:spring-boot-starter-jdbc") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - runtimeOnly("com.mysql:mysql-connector-j") - runtimeOnly("com.h2database:h2") - - implementation("org.springframework.boot:spring-boot-starter-actuator") - implementation("io.micrometer:micrometer-registry-prometheus") - - implementation("com.github.f4b6a3:ulid-creator:5.2.2") - - implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.redisson:redisson-spring-boot-starter:4.1.0") - - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + repositories { + mavenCentral() + } } -kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property") - } -} - -tasks.withType { - useJUnitPlatform() -} +subprojects { + // 하위 모듈에 플러그인 적용 + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.spring") + apply(plugin = "org.jetbrains.kotlin.plugin.jpa") + apply(plugin = "org.springframework.boot") + apply(plugin = "io.spring.dependency-management") + + // Java 21 설정 + configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + // 컴파일 옵션 + tasks.withType { + compilerOptions { + freeCompilerArgs.add("-Xjsr305=strict") + jvmTarget.set(JvmTarget.JVM_21) + } + } + + tasks.withType { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/Dockerfile b/coupon-api/Dockerfile similarity index 52% rename from Dockerfile rename to coupon-api/Dockerfile index d26603b..851d736 100644 --- a/Dockerfile +++ b/coupon-api/Dockerfile @@ -1,11 +1,8 @@ -# 1. Base Image (JDK 21 환경) +# coupon-api/Dockerfile FROM eclipse-temurin:21-jdk-alpine -# 2. 작업 디렉토리 설정 WORKDIR /app -# 3. JAR 파일 복사 COPY build/libs/*.jar app.jar -# 4. 실행 명령어 ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/coupon-api/build.gradle.kts b/coupon-api/build.gradle.kts new file mode 100644 index 0000000..8f5702b --- /dev/null +++ b/coupon-api/build.gradle.kts @@ -0,0 +1,35 @@ +dependencies { + // Kotlin & Core + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib") + + // Web & Validation + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + + // Kafka + implementation("org.springframework.kafka:spring-kafka") + + // DB & JPA + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + runtimeOnly("com.mysql:mysql-connector-j") + runtimeOnly("com.h2database:h2") + + // Redis & Redisson + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.redisson:redisson-spring-boot-starter:3.25.0") + + // Monitoring + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + // Utils + implementation("com.github.f4b6a3:ulid-creator:5.2.2") + + // Test + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/CouponsystemApplication.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/CouponsystemApiApplication.kt similarity index 57% rename from src/main/kotlin/com/woong2e/couponsystem/CouponsystemApplication.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/CouponsystemApiApplication.kt index b4bed82..495567b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/CouponsystemApplication.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/CouponsystemApiApplication.kt @@ -1,11 +1,11 @@ -package com.woong2e.couponsystem +package main.kotlin.com.woong2e.couponsystem import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class CouponsystemApplication +class CouponsystemApiApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt similarity index 65% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt index 86d4d23..65ec45b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/controller/CouponController.kt @@ -1,16 +1,16 @@ -package com.woong2e.couponsystem.coupon.api.controller +package main.kotlin.com.woong2e.couponsystem.coupon.api.controller -import com.woong2e.couponsystem.coupon.api.request.CouponCreateRequest -import com.woong2e.couponsystem.coupon.api.request.CouponIssueRequest -import com.woong2e.couponsystem.coupon.api.request.CouponStockInitRequest -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.response.CouponResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.application.service.CouponService -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.response.ApiResponse -import com.woong2e.couponsystem.global.response.status.SuccessStatus +import main.kotlin.com.woong2e.couponsystem.coupon.api.request.CouponCreateRequest +import main.kotlin.com.woong2e.couponsystem.coupon.api.request.CouponIssueRequest +import main.kotlin.com.woong2e.couponsystem.coupon.api.request.CouponStockInitRequest +import main.kotlin.com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse +import main.kotlin.com.woong2e.couponsystem.coupon.application.response.CouponResponse +import main.kotlin.com.woong2e.couponsystem.coupon.application.service.CouponIssueService +import main.kotlin.com.woong2e.couponsystem.coupon.application.service.CouponService +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.response.ApiResponse +import main.kotlin.com.woong2e.couponsystem.global.response.status.SuccessStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.PathVariable diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt similarity index 56% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt index e9ac79d..981f9b5 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponCreateRequest.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.api.request +package main.kotlin.com.woong2e.couponsystem.coupon.api.request data class CouponCreateRequest( val title: String, diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt similarity index 61% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt index c1479be..c9d3a2b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponIssueRequest.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.api.request +package main.kotlin.com.woong2e.couponsystem.coupon.api.request import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt similarity index 62% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt index fae2c4f..66fa95c 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/api/request/CouponStockInitRequest.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.api.request +package main.kotlin.com.woong2e.couponsystem.coupon.api.request import java.util.UUID diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueDltEvent.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueDltEvent.kt new file mode 100644 index 0000000..0c5a179 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueDltEvent.kt @@ -0,0 +1,11 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.event + +import main.kotlin.com.woong2e.couponsystem.coupon.value.DltSource +import java.util.UUID + +data class CouponIssueDltEvent( + val source: DltSource, + val couponId: UUID, + val userId: Long, + val reason: String? = null, +) \ No newline at end of file diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueEvent.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueEvent.kt new file mode 100644 index 0000000..8251415 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/event/CouponIssueEvent.kt @@ -0,0 +1,8 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.event + +import java.util.UUID + +data class CouponIssueEvent( + val couponId: UUID, + val userId: Long +) diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueEventPublisher.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueEventPublisher.kt new file mode 100644 index 0000000..253daef --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueEventPublisher.kt @@ -0,0 +1,7 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.port.out + +import java.util.UUID + +interface CouponIssueEventPublisher { + fun publish(couponId: UUID, userId: Long) +} \ No newline at end of file diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt new file mode 100644 index 0000000..b81a730 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt @@ -0,0 +1,27 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.response + +import java.util.UUID + +data class CouponIssueResponse( + val isSuccess: Boolean = true, + val issuedCouponId: UUID? = null, + val message: String? = null +) { + companion object { + fun asyncIssued(): CouponIssueResponse { + return CouponIssueResponse( + isSuccess = true, + issuedCouponId = null, + message = "쿠폰 발급 요청이 대기열에 추가되었습니다. 잠시 후 보관함을 확인해주세요." + ) + } + + fun syncIssued(id: UUID): CouponIssueResponse { + return CouponIssueResponse( + isSuccess = true, + issuedCouponId = id, + message = "쿠폰 발급이 완료되었습니다." + ) + } + } +} diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt similarity index 67% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt index e9ae8e6..dd2c65d 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponResponse.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.application.response +package main.kotlin.com.woong2e.couponsystem.coupon.application.response import java.util.UUID diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt new file mode 100644 index 0000000..018c30e --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt @@ -0,0 +1,9 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.service + +import main.kotlin.com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse +import java.util.UUID + +interface CouponIssueService { + + fun issue(couponId: UUID, userId: Long): CouponIssueResponse? +} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt similarity index 63% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt index 95e6141..45dd7e6 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponService.kt @@ -1,13 +1,13 @@ -package com.woong2e.couponsystem.coupon.application.service - -import com.woong2e.couponsystem.coupon.api.request.CouponCreateRequest -import com.woong2e.couponsystem.coupon.application.response.CouponResponse -import com.woong2e.couponsystem.coupon.domain.entity.Coupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.infra.CouponRedisRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +package main.kotlin.com.woong2e.couponsystem.coupon.application.service + +import main.kotlin.com.woong2e.couponsystem.coupon.api.request.CouponCreateRequest +import main.kotlin.com.woong2e.couponsystem.coupon.application.response.CouponResponse +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.Coupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +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 import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.UUID 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 new file mode 100644 index 0000000..c52f661 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AsyncLuaCouponIssueService.kt @@ -0,0 +1,33 @@ +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.redis.CouponRedisRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus +import org.springframework.stereotype.Service +import java.util.UUID + +@Service("asyncLua") +class AsyncLuaCouponIssueService( + private val couponRedisRepository: CouponRedisRepository, + private val couponIssueEventPublisher: CouponIssueEventPublisher +) : CouponIssueService { + + override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { + val result = couponRedisRepository.issueRequest(couponId.toString(), userId) + + when (result) { + "DUPLICATED" -> throw CustomException(CouponErrorStatus.COUPON_ALREADY_ISSUED) + "SOLD_OUT" -> throw CustomException(CouponErrorStatus.COUPON_OUT_OF_STOCK) + "SUCCESS" -> { + couponIssueEventPublisher.publish(couponId, userId) + + return CouponIssueResponse.asyncIssued() + } + else -> throw CustomException(ErrorStatus.SYSTEM_BUSY) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt similarity index 75% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt index fc5c854..3572a1b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicCouponIssueService.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionTemplate import java.util.UUID @@ -27,7 +27,7 @@ class AtomicCouponIssueService( private val couponCounts = ConcurrentHashMap() - override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { + override fun issue(couponId: UUID, userId: Long): CouponIssueResponse? { val state = couponCounts.computeIfAbsent(couponId) { id -> val coupon = couponRepository.findById(id) diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt similarity index 59% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt index 40135dc..69da56a 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/AtomicQueryCouponIssueService.kt @@ -1,15 +1,14 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.annotaion.Bulkhead -import com.woong2e.couponsystem.global.exception.CustomException +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.annotaion.Bulkhead +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.support.TransactionTemplate import java.util.UUID @@ -21,7 +20,7 @@ class AtomicQueryCouponIssueService( ) : CouponIssueService { @Bulkhead(permits = 20, fair = true) - override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { + override fun issue(couponId: UUID, userId: Long): CouponIssueResponse? { return transactionTemplate.execute { val affectedRows = couponRepository.increaseIssuedQuantity(couponId) diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt similarity index 61% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt index 54011e1..ebf0733 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/DistributedLockCouponIssueService.kt @@ -1,11 +1,11 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.repository.AppliedUserRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.infra.lock.DistributedLockExecutor +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.domain.repository.AppliedUserRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.infra.lock.DistributedLockExecutor import org.springframework.stereotype.Service import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt similarity index 56% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt index 8c8a0a5..098f4ed 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/LuaCouponIssueService.kt @@ -1,13 +1,13 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.infra.CouponRedisRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.response.status.ErrorStatus +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +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 +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus import org.springframework.stereotype.Service import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt similarity index 59% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt index bccad20..5c005bb 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/NoLockCouponIssueService.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt similarity index 60% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt index c4ba976..f1b0131 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/PessimisticLockCouponIssueService.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt similarity index 65% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt index 126fcf5..0838a41 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/ReentrantLockCouponIssueService.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem.coupon.application.service.impl - -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl + +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionTemplate import java.util.UUID @@ -21,7 +21,7 @@ class ReentrantLockCouponIssueService( private val lock = ReentrantLock(true) - override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { + override fun issue(couponId: UUID, userId: Long): CouponIssueResponse? { lock.lock() try { diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt similarity index 56% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt index ae25758..a98e0f7 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SemaphoreCouponIssueService.kt @@ -1,8 +1,8 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.global.annotaion.Bulkhead +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.global.annotaion.Bulkhead import org.springframework.stereotype.Service import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt similarity index 59% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt index 4c2bb45..f86400f 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/Synchronized2CouponIssueService.kt @@ -1,15 +1,14 @@ -package com.woong2e.couponsystem.coupon.application.service.impl +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import org.springframework.transaction.support.TransactionTemplate import java.util.UUID @Service("synchronized2") diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt similarity index 61% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt index f525dd6..0c7ef66 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/impl/SynchronizedCouponIssueService.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem.coupon.application.service.impl - -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import com.woong2e.couponsystem.coupon.application.service.CouponIssueService -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException +package main.kotlin.com.woong2e.couponsystem.coupon.application.service.impl + +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.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionTemplate import java.util.UUID @@ -19,7 +19,7 @@ class SynchronizedCouponIssueService( ) : CouponIssueService { @Synchronized - override fun issue(couponId: UUID, userId: Long): CouponIssueResponse { + override fun issue(couponId: UUID, userId: Long): CouponIssueResponse? { return transactionTemplate.execute { val coupon = couponRepository.findById(couponId) diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt similarity index 62% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt index 8060ad3..5b78e15 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt @@ -1,10 +1,10 @@ -package com.woong2e.couponsystem.coupon.domain.entity +package main.kotlin.com.woong2e.couponsystem.coupon.domain.entity -import com.woong2e.couponsystem.coupon.status.CouponErrorStatus -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity import jakarta.persistence.Entity import jakarta.persistence.Table +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity @Entity @Table(name = "coupons") diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt similarity index 84% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt index ac2ccf7..6370257 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt @@ -1,7 +1,7 @@ -package com.woong2e.couponsystem.coupon.domain.entity +package main.kotlin.com.woong2e.couponsystem.coupon.domain.entity -import com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity import jakarta.persistence.* +import main.kotlin.com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity import org.springframework.data.annotation.CreatedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.time.LocalDateTime diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt similarity index 66% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt index ee49afa..a1d3e84 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/AppliedUserRepository.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.domain.repository +package main.kotlin.com.woong2e.couponsystem.coupon.domain.repository interface AppliedUserRepository { diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt similarity index 68% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt index 7e765cf..475055e 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/CouponRepository.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.coupon.domain.repository +package main.kotlin.com.woong2e.couponsystem.coupon.domain.repository -import com.woong2e.couponsystem.coupon.domain.entity.Coupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.Coupon import java.util.Optional import java.util.UUID diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt similarity index 61% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt index 5c529eb..70621ca 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.coupon.domain.repository +package main.kotlin.com.woong2e.couponsystem.coupon.domain.repository -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon import java.util.UUID interface IssuedCouponRepository { diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaCouponRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/CouponJpaRepository.kt similarity index 74% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaCouponRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/CouponJpaRepository.kt index 9215721..7a84d18 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaCouponRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/CouponJpaRepository.kt @@ -1,8 +1,8 @@ -package com.woong2e.couponsystem.coupon.infra +package main.kotlin.com.woong2e.couponsystem.coupon.infra.jpa -import com.woong2e.couponsystem.coupon.domain.entity.Coupon -import com.woong2e.couponsystem.coupon.domain.repository.CouponRepository import jakarta.persistence.LockModeType +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.Coupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.CouponRepository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Modifying @@ -11,7 +11,7 @@ import org.springframework.data.repository.query.Param import java.util.Optional import java.util.UUID -interface JpaCouponRepository : CouponRepository, JpaRepository { +interface CouponJpaRepository : CouponRepository, JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select c from Coupon c where c.id = :id") diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaIssuedCouponRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt similarity index 55% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaIssuedCouponRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt index 3452004..a8db2b1 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/JpaIssuedCouponRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt @@ -1,13 +1,13 @@ -package com.woong2e.couponsystem.coupon.infra +package main.kotlin.com.woong2e.couponsystem.coupon.infra.jpa -import com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon -import com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import java.util.UUID -interface JpaIssuedCouponRepository : IssuedCouponRepository, JpaRepository { +interface IssuedCouponJpaRepository : IssuedCouponRepository, JpaRepository { @Modifying @Query("DELETE FROM IssuedCoupon ic WHERE ic.couponId = :couponId") diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/producer/IssuedCouponProducer.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/producer/IssuedCouponProducer.kt new file mode 100644 index 0000000..b3c951f --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/producer/IssuedCouponProducer.kt @@ -0,0 +1,101 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.infra.producer + +import com.fasterxml.jackson.databind.ObjectMapper +import main.kotlin.com.woong2e.couponsystem.coupon.application.event.CouponIssueDltEvent +import main.kotlin.com.woong2e.couponsystem.coupon.application.event.CouponIssueEvent +import main.kotlin.com.woong2e.couponsystem.coupon.application.port.out.CouponIssueEventPublisher +import main.kotlin.com.woong2e.couponsystem.coupon.value.DltSource +import org.slf4j.LoggerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class IssuedCouponProducer( + private val kafkaTemplate: KafkaTemplate, + private val objectMapper: ObjectMapper +) : CouponIssueEventPublisher { + + private val log = LoggerFactory.getLogger(this::class.java) + + companion object { + private const val TOPIC = "coupon-issue-topic" + private const val TOPIC_DLT = "coupon-issue-dlt-topic" + } + + override fun publish(couponId: UUID, userId: Long) { + val event = CouponIssueEvent(couponId, userId) + val key = userId.toString() + + val messageJson = try { + objectMapper.writeValueAsString(event) + } catch (e: Exception) { + log.error("JSON serialization failed for event: $event", e) + return + } + + runCatching { kafkaTemplate.send(TOPIC, key, messageJson) } + .onFailure { ex -> + log.error("Kafka send() call failed -> send to DLT. couponId={}, userId={}", couponId, userId, ex) + sendToDlt( + key = key, + dltEvent = CouponIssueDltEvent( + source = DltSource.PRODUCER, + couponId = couponId, + userId = userId, + reason = ex.message ?: ex::class.java.simpleName + ) + ) + } + .onSuccess { future -> + future.whenComplete { result, ex -> + if (ex == null) { + log.info( + "Producer sent: topic={}, partition={}, offset={}, couponId={}, userId={}", + result.recordMetadata.topic(), + result.recordMetadata.partition(), + result.recordMetadata.offset(), + couponId, + userId + ) + } else { + log.error("Producer async send failed -> send to DLT. couponId={}, userId={}", couponId, userId, ex) + sendToDlt( + key = key, + dltEvent = CouponIssueDltEvent( + source = DltSource.PRODUCER, + couponId = couponId, + userId = userId, + reason = ex.message ?: ex::class.java.simpleName + ) + ) + } + } + } + } + + private fun sendToDlt(key: String, dltEvent: CouponIssueDltEvent) { + val dltJson = try { + objectMapper.writeValueAsString(dltEvent) + } catch (e: Exception) { + log.error("Failed to serialize DLT event", e) + return + } + + kafkaTemplate.send(TOPIC_DLT, key, dltJson).whenComplete { result, ex -> + if (ex == null) { + log.warn( + "Sent to DLT: topic={}, partition={}, offset={}, source={}, couponId={}, userId={}", + result.recordMetadata.topic(), + result.recordMetadata.partition(), + result.recordMetadata.offset(), + dltEvent.source, + dltEvent.couponId, + dltEvent.userId + ) + } else { + log.error("Failed to send to DLT: source={}, couponId={}, userId={}", dltEvent.source, dltEvent.couponId, dltEvent.userId, ex) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/AppliedUserRedisRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/AppliedUserRedisRepository.kt similarity index 82% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/infra/AppliedUserRedisRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/AppliedUserRedisRepository.kt index 0dc2b85..69d864f 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/AppliedUserRedisRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/AppliedUserRedisRepository.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.coupon.infra +package main.kotlin.com.woong2e.couponsystem.coupon.infra.redis -import com.woong2e.couponsystem.coupon.domain.repository.AppliedUserRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.AppliedUserRepository import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Repository import java.time.Duration diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/CouponRedisRepository.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/CouponRedisRepository.kt similarity index 96% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/infra/CouponRedisRepository.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/CouponRedisRepository.kt index 090f8de..395b2c2 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/CouponRedisRepository.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/redis/CouponRedisRepository.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.coupon.infra +package main.kotlin.com.woong2e.couponsystem.coupon.infra.redis import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.script.DefaultRedisScript diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt similarity index 83% rename from src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt index afc36f9..b261013 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.coupon.status +package main.kotlin.com.woong2e.couponsystem.coupon.status -import com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus import org.springframework.http.HttpStatus enum class CouponErrorStatus( diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt new file mode 100644 index 0000000..bf0c4bc --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt @@ -0,0 +1,5 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.value + +enum class DltSource { + PRODUCER, CONSUMER +} diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt similarity index 77% rename from src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt index 4959f9d..f7cfe5c 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/annotaion/Bulkhead.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.global.annotaion +package main.kotlin.com.woong2e.couponsystem.global.annotaion @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt similarity index 86% rename from src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt index 3070d42..cb8df2e 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/aop/BulkheadAspect.kt @@ -1,8 +1,8 @@ -package com.woong2e.couponsystem.global.aop +package main.kotlin.com.woong2e.couponsystem.global.aop -import com.woong2e.couponsystem.global.annotaion.Bulkhead -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.response.status.ErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.annotaion.Bulkhead +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt new file mode 100644 index 0000000..f4bc0e4 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt @@ -0,0 +1,7 @@ +package main.kotlin.com.woong2e.couponsystem.global.exception + +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus + +class CustomException( + val errorCode: BaseErrorStatus +) : RuntimeException(errorCode.message) \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt similarity index 77% rename from src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt index ef08744..bc4541a 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt @@ -1,16 +1,12 @@ -package com.woong2e.couponsystem.global.exception +package main.kotlin.com.woong2e.couponsystem.global.exception -import com.woong2e.couponsystem.global.response.ApiResponse -import com.woong2e.couponsystem.global.response.status.ErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.response.ApiResponse +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus import org.slf4j.LoggerFactory -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatusCode import org.springframework.http.ResponseEntity import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice -import org.springframework.web.context.request.WebRequest -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler @RestControllerAdvice class GlobalExceptionHandler { diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt similarity index 76% rename from src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt index 8f0b98e..8ddb111 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.global.jpa +package main.kotlin.com.woong2e.couponsystem.global.jpa import org.springframework.context.annotation.Configuration import org.springframework.data.jpa.repository.config.EnableJpaAuditing diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt similarity index 95% rename from src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt index 0d37e99..5cdef72 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.global.jpa +package main.kotlin.com.woong2e.couponsystem.global.jpa import com.github.f4b6a3.ulid.UlidCreator import jakarta.persistence.* diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt similarity index 85% rename from src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt index e842919..feeeb2d 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt @@ -1,15 +1,15 @@ -package com.woong2e.couponsystem.global.response +package main.kotlin.com.woong2e.couponsystem.global.response import com.fasterxml.jackson.annotation.JsonInclude -import com.woong2e.couponsystem.global.response.code.BaseCode -import com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseCode +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus import org.springframework.http.ResponseEntity data class ApiResponse( val isSuccess: Boolean, val code: String, val message: String, - @JsonInclude(JsonInclude.Include.NON_NULL) + @param:JsonInclude(JsonInclude.Include.NON_NULL) val result: T? = null ) { companion object { diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt similarity index 67% rename from src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt index c7729ee..2f0b6fa 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.global.response.code +package main.kotlin.com.woong2e.couponsystem.global.response.code import org.springframework.http.HttpStatus diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt similarity index 68% rename from src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt index 5b93244..cfa3cdf 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.global.response.code +package main.kotlin.com.woong2e.couponsystem.global.response.code import org.springframework.http.HttpStatus diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt similarity index 87% rename from src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt index 0bef3ba..a3147f8 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.global.response.status +package main.kotlin.com.woong2e.couponsystem.global.response.status -import com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus import org.springframework.http.HttpStatus enum class ErrorStatus( diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt similarity index 80% rename from src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt index 75f0754..633eea3 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt @@ -1,6 +1,6 @@ -package com.woong2e.couponsystem.global.response.status +package main.kotlin.com.woong2e.couponsystem.global.response.status -import com.woong2e.couponsystem.global.response.code.BaseCode +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseCode import org.springframework.http.HttpStatus enum class SuccessStatus( diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaProducerConfig.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaProducerConfig.kt new file mode 100644 index 0000000..bea8def --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaProducerConfig.kt @@ -0,0 +1,46 @@ +package main.kotlin.com.woong2e.couponsystem.infra.kafka + +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.boot.autoconfigure.kafka.KafkaProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory + +@Configuration +class KafkaProducerConfig( + private val kafkaProperties: KafkaProperties +) { + + companion object { + private const val DEFAULT_LINGER_MS = 50 + private const val DEFAULT_BATCH_SIZE = 1000000 + } + + @Bean + fun producerFactory(): ProducerFactory { + val props = HashMap() + + props[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers + + props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java + props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java + + props[ProducerConfig.ACKS_CONFIG] = kafkaProperties.producer.acks + props[ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG] = kafkaProperties.producer.properties["enable.idempotence"] ?: true + + props[ProducerConfig.LINGER_MS_CONFIG] = DEFAULT_LINGER_MS + props[ProducerConfig.BATCH_SIZE_CONFIG] = DEFAULT_BATCH_SIZE + + props[ProducerConfig.COMPRESSION_TYPE_CONFIG] = kafkaProperties.producer.compressionType + + return DefaultKafkaProducerFactory(props) + } + + @Bean + fun kafkaTemplate(): KafkaTemplate { + return KafkaTemplate(producerFactory()) + } +} \ No newline at end of file diff --git a/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaTopicConfig.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaTopicConfig.kt new file mode 100644 index 0000000..7812bd3 --- /dev/null +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaTopicConfig.kt @@ -0,0 +1,26 @@ +package main.kotlin.com.woong2e.couponsystem.infra.kafka + +import org.apache.kafka.clients.admin.NewTopic +import org.springframework.kafka.config.TopicBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class KafkaTopicConfig { + + @Bean + fun couponCreateTopic(): NewTopic { + return TopicBuilder.name("coupon-issue-topic") + .partitions(3) + .replicas(1) + .build() + } + + @Bean + fun couponCreateDltTopic(): NewTopic { + return TopicBuilder.name("coupon-issue-dlt-topic") + .partitions(3) + .replicas(1) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt similarity index 75% rename from src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt index 3b2d71b..204ac07 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/DistributedLockExecutor.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.infra.lock +package main.kotlin.com.woong2e.couponsystem.infra.lock interface DistributedLockExecutor { diff --git a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt similarity index 88% rename from src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt index c8fbc0e..f245a7b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/LettuceLockExecutor.kt @@ -1,8 +1,8 @@ -package com.woong2e.couponsystem.infra.lock.impl +package main.kotlin.com.woong2e.couponsystem.infra.lock.impl -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.response.status.ErrorStatus -import com.woong2e.couponsystem.infra.lock.DistributedLockExecutor +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus +import main.kotlin.com.woong2e.couponsystem.infra.lock.DistributedLockExecutor import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.script.DefaultRedisScript import org.springframework.stereotype.Component diff --git a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt similarity index 77% rename from src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt index c9eebfc..275c88a 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/lock/impl/RedissonLockExecutor.kt @@ -1,8 +1,8 @@ -package com.woong2e.couponsystem.infra.lock.impl +package main.kotlin.com.woong2e.couponsystem.infra.lock.impl -import com.woong2e.couponsystem.global.exception.CustomException -import com.woong2e.couponsystem.global.response.status.ErrorStatus -import com.woong2e.couponsystem.infra.lock.DistributedLockExecutor +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus +import main.kotlin.com.woong2e.couponsystem.infra.lock.DistributedLockExecutor import org.redisson.api.RedissonClient import org.springframework.context.annotation.Primary import org.springframework.stereotype.Component diff --git a/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt similarity index 93% rename from src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt index 4e23f5f..3bcaa2b 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedisConfig.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.infra.redis +package main.kotlin.com.woong2e.couponsystem.infra.redis import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt similarity index 72% rename from src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt index c0953d4..ec1a92e 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/infra/redis/RedissonConfig.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.infra.redis +package main.kotlin.com.woong2e.couponsystem.infra.redis import org.redisson.Redisson import org.redisson.api.RedissonClient @@ -9,8 +9,8 @@ import org.springframework.context.annotation.Configuration @Configuration class RedissonConfig( - @Value("\${spring.data.redis.host}") private val host: String, - @Value("\${spring.data.redis.port}") private val port: Int + @param:Value("\${spring.data.redis.host}") private val host: String, + @param:Value("\${spring.data.redis.port}") private val port: Int ) { @Bean diff --git a/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt similarity index 77% rename from src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt rename to coupon-api/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt index 892fd33..dcfb3c4 100644 --- a/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt +++ b/coupon-api/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt @@ -1,4 +1,4 @@ -package com.woong2e.couponsystem.user.domin +package main.kotlin.com.woong2e.couponsystem.user.domin import jakarta.persistence.* diff --git a/coupon-api/src/main/resources/application-local.yml b/coupon-api/src/main/resources/application-local.yml new file mode 100644 index 0000000..854be92 --- /dev/null +++ b/coupon-api/src/main/resources/application-local.yml @@ -0,0 +1,60 @@ +server: + tomcat: + threads: + max: 50 + +spring: + kafka: + bootstrap-servers: localhost:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + acks: all # 데이터 유실 방지 (필수) + retries: 10 + # 배치 크기 증가 (기본 16KB -> 1MB) + batch-size: 1000000 + # 압축 알고리즘 (CPU 부하가 적고 속도가 빠른 lz4 권장) + compression-type: lz4 + properties: + enable.idempotence: true + # 50ms 동안 기다렸다가 모아서 발송 + linger.ms: 50 + + datasource: + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + format_sql: true + use_sql_comments: false + hibernate: + ddl-auto: validate + defer-datasource-initialization: true + open-in-view: false + generate-ddl: false + show-sql: true + + data: + redis: + host: localhost + port: 6379 + lettuce: + pool: + max-active: 64 + max-idle: 32 + min-idle: 16 + max-wait: 3000ms + +management: + endpoints: + web: + exposure: + include: prometheus, health, info + endpoint: + prometheus: + enabled: true \ No newline at end of file diff --git a/coupon-api/src/main/resources/application-prod.yml b/coupon-api/src/main/resources/application-prod.yml new file mode 100644 index 0000000..208365b --- /dev/null +++ b/coupon-api/src/main/resources/application-prod.yml @@ -0,0 +1,71 @@ +server: + port: 8080 + tomcat: + threads: + max: 50 + +spring: + application: + name: coupon-api # 모니터링 시 식별용 이름 + config: + activate: + on-profile: prod + + # Kafka Producer 설정 + kafka: + bootstrap-servers: kafka:29092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + acks: all # 데이터 유실 방지 (필수) + retries: 10 + # 배치 크기 증가 (기본 16KB -> 1MB) + batch-size: 1000000 + # 압축 알고리즘 (CPU 부하가 적고 속도가 빠른 lz4 권장) + compression-type: lz4 + properties: + enable.idempotence: true + # 10ms 동안 기다렸다가 모아서 발송 + linger.ms: 10 + + datasource: + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + format_sql: false + use_sql_comments: false + hibernate: + ddl-auto: validate + defer-datasource-initialization: true + open-in-view: false + generate-ddl: false + show-sql: false + + data: + redis: + host: redis + port: 6379 + lettuce: + pool: + max-active: 64 + max-idle: 32 + min-idle: 16 + max-wait: 3000ms + +management: + endpoints: + web: + exposure: + include: prometheus, health, info + endpoint: + prometheus: + enabled: true + metrics: + tags: + application: coupon-api \ No newline at end of file diff --git a/coupon-api/src/main/resources/application.yml b/coupon-api/src/main/resources/application.yml new file mode 100644 index 0000000..ef1a90f --- /dev/null +++ b/coupon-api/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: couponsystem + profiles: + active: local # 기본적으로 'local' 프로필을 사용 (RDS 쓸 땐 'prod'로 변경) diff --git a/coupon-api/src/main/resources/sql/data.sql b/coupon-api/src/main/resources/sql/data.sql new file mode 100644 index 0000000..58de4b9 --- /dev/null +++ b/coupon-api/src/main/resources/sql/data.sql @@ -0,0 +1,22 @@ +-- 1. 기존 데이터 초기화 (ID를 1번부터 다시 시작하기 위해 TRUNCATE 사용) +SET FOREIGN_KEY_CHECKS = 0; -- 혹시 모를 제약조건 잠시 해제 +TRUNCATE TABLE issued_coupons; +TRUNCATE TABLE coupons; +TRUNCATE TABLE users; +SET FOREIGN_KEY_CHECKS = 1; + +-- 2. 쿠폰 생성 (선착순 100명) +INSERT INTO coupons (title, total_quantity, issued_quantity) +VALUES ('선착순 100명 치킨', 100, 0); + +-- 3. 유저 10,000명 생성 (Recursive CTE 활용) +-- MySQL 8.0의 재귀 제한(기본 1000)을 늘려줍니다. +SET SESSION cte_max_recursion_depth = 1000000; + +INSERT INTO users (name) +WITH RECURSIVE sequence AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM sequence WHERE n < 10000 +) +SELECT CONCAT('User_', n) FROM sequence; \ No newline at end of file diff --git a/coupon-api/src/main/resources/sql/schema.sql b/coupon-api/src/main/resources/sql/schema.sql new file mode 100644 index 0000000..588f130 --- /dev/null +++ b/coupon-api/src/main/resources/sql/schema.sql @@ -0,0 +1,30 @@ +DROP TABLE IF EXISTS issued_coupons; +DROP TABLE IF EXISTS coupons; +DROP TABLE IF EXISTS users; + +-- 1. 사용자 테이블 +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. 쿠폰 정책 테이블 +CREATE TABLE coupons ( + id BINARY(16) PRIMARY KEY COMMENT 'ULID (UUID)', + title VARCHAR(100) NOT NULL, + total_quantity INT NOT NULL, + issued_quantity INT NOT NULL DEFAULT 0, + created_at DATETIME(6), + updated_at DATETIME(6) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. 발급 이력 +CREATE TABLE issued_coupons ( + id BINARY(16) PRIMARY KEY COMMENT 'ULID', + coupon_id BINARY(16) NOT NULL COMMENT 'UUID', + user_id BIGINT NOT NULL COMMENT 'Long (숫자)', + status VARCHAR(20) NOT NULL DEFAULT 'ISSUED', + issued_at DATETIME(6) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- CREATE UNIQUE KEY uk_coupon_user (coupon_id, user_id); \ No newline at end of file diff --git a/src/test/kotlin/com/woong2e/couponsystem/CouponsystemApplicationTests.kt b/coupon-api/src/test/kotlin/com/woong2e/couponsystem/CouponsystemApiApplicationTests.kt similarity index 57% rename from src/test/kotlin/com/woong2e/couponsystem/CouponsystemApplicationTests.kt rename to coupon-api/src/test/kotlin/com/woong2e/couponsystem/CouponsystemApiApplicationTests.kt index f463f3b..ca2ada7 100644 --- a/src/test/kotlin/com/woong2e/couponsystem/CouponsystemApplicationTests.kt +++ b/coupon-api/src/test/kotlin/com/woong2e/couponsystem/CouponsystemApiApplicationTests.kt @@ -1,12 +1,12 @@ -package com.woong2e.couponsystem +package test.kotlin.com.woong2e.couponsystem import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles -@SpringBootTest +@SpringBootTest(classes = [CouponsystemApiApplicationTests::class]) @ActiveProfiles("test") -class CouponsystemApplicationTests { +class CouponsystemApiApplicationTests { @Test fun contextLoads() { diff --git a/src/test/resources/application-test.yml b/coupon-api/src/test/resources/application-test.yml similarity index 100% rename from src/test/resources/application-test.yml rename to coupon-api/src/test/resources/application-test.yml diff --git a/coupon-consumer/Dockerfile b/coupon-consumer/Dockerfile new file mode 100644 index 0000000..4a5ab22 --- /dev/null +++ b/coupon-consumer/Dockerfile @@ -0,0 +1,8 @@ +# coupon-consumer/Dockerfile +FROM eclipse-temurin:21-jdk-alpine + +WORKDIR /app + +COPY build/libs/*.jar app.jar + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/coupon-consumer/build.gradle.kts b/coupon-consumer/build.gradle.kts new file mode 100644 index 0000000..31e36e0 --- /dev/null +++ b/coupon-consumer/build.gradle.kts @@ -0,0 +1,31 @@ +dependencies { + // Kotlin & Core + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib") + + // Spring Boot Web & Monitoring + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + + // Kafka + implementation("org.springframework.kafka:spring-kafka") + + // DB & JPA + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + runtimeOnly("com.mysql:mysql-connector-j") + runtimeOnly("com.h2database:h2") + + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // Utils + implementation("com.github.f4b6a3:ulid-creator:5.2.2") + + // Test + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplication.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplication.kt new file mode 100644 index 0000000..d9d00c8 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplication.kt @@ -0,0 +1,11 @@ +package main.kotlin.com.woong2e.couponsystem + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class CouponsystemConsumerApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueDltPublisher.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueDltPublisher.kt new file mode 100644 index 0000000..e61da1a --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/port/out/CouponIssueDltPublisher.kt @@ -0,0 +1,7 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.port.out + +import main.kotlin.com.woong2e.couponsystem.coupon.consumer.event.CouponIssueDltEvent + +interface CouponIssueDltPublisher { + fun publish(event: CouponIssueDltEvent) +} diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueWorkerService.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueWorkerService.kt new file mode 100644 index 0000000..8d26859 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueWorkerService.kt @@ -0,0 +1,32 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.application.service + +import main.kotlin.com.woong2e.couponsystem.coupon.consumer.event.CouponIssueEvent +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponBatchRepository +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class CouponIssueWorkerService( + private val issuedCouponRepository: IssuedCouponRepository, + private val issuedCouponBatchRepository: IssuedCouponBatchRepository +) { + + @Transactional + fun issue(couponId: UUID, userId: Long) { + val issuedCoupon = IssuedCoupon( + couponId = couponId, + userId = userId + ) + + issuedCouponRepository.save(issuedCoupon) + } + + @Transactional + fun issueRequestBatch(events: List) { + val coupons = events.map { IssuedCoupon(it.couponId, it.userId) } + issuedCouponBatchRepository.batchInsert(coupons) + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueDltEvent.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueDltEvent.kt new file mode 100644 index 0000000..a522ea2 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueDltEvent.kt @@ -0,0 +1,11 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.consumer.event + +import main.kotlin.com.woong2e.couponsystem.coupon.value.DltSource +import java.util.UUID + +data class CouponIssueDltEvent( + val source: DltSource, + val couponId: UUID, + val userId: Long, + val reason: String? = null, +) diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueEvent.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueEvent.kt new file mode 100644 index 0000000..9c8937a --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/event/CouponIssueEvent.kt @@ -0,0 +1,8 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.consumer.event + +import java.util.UUID + +data class CouponIssueEvent( + val couponId: UUID, + val userId: Long +) \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/listener/CouponIssueConsumer.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/listener/CouponIssueConsumer.kt new file mode 100644 index 0000000..274be16 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/consumer/listener/CouponIssueConsumer.kt @@ -0,0 +1,95 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.consumer.listener + +import com.fasterxml.jackson.databind.ObjectMapper +import main.kotlin.com.woong2e.couponsystem.coupon.application.port.out.CouponIssueDltPublisher +import main.kotlin.com.woong2e.couponsystem.coupon.application.service.CouponIssueWorkerService +import main.kotlin.com.woong2e.couponsystem.coupon.consumer.event.CouponIssueDltEvent +import main.kotlin.com.woong2e.couponsystem.coupon.consumer.event.CouponIssueEvent +import main.kotlin.com.woong2e.couponsystem.coupon.value.DltSource +import org.slf4j.LoggerFactory +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.support.Acknowledgment +import org.springframework.stereotype.Component + +@Component +class CouponIssueConsumer( + private val couponIssueWorkerService: CouponIssueWorkerService, + private val couponIssueDltPublisher: CouponIssueDltPublisher, + private val objectMapper: ObjectMapper // [추가] JSON 파싱용 +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + companion object { + private const val TOPIC = "coupon-issue-topic" + private const val TOPIC_DLT = "coupon-issue-dlt-topic" + private const val GROUP_ID = "coupon-issue-group" + private const val GROUP_ID_DLT = "coupon-issue-dlt-group" + } + + @KafkaListener( + topics = [TOPIC], + groupId = GROUP_ID, + containerFactory = "kafkaListenerContainerFactory" + ) + fun onMessage(messages: List, ack: Acknowledgment) { + val events = mutableListOf() + + messages.forEach { json -> + try { + val event = objectMapper.readValue(json, CouponIssueEvent::class.java) + events.add(event) + } catch (e: Exception) { + log.error("Failed to parse event json: {}", json, e) + } + } + + if (events.isEmpty()) { + ack.acknowledge() + return + } + + runCatching { + log.info("Consumer Batch Listen: size={}", events.size) + couponIssueWorkerService.issueRequestBatch(events) + }.onSuccess { + ack.acknowledge() + }.onFailure { ex -> + log.error("Batch Consumer Failed -> Send all to DLT. size={}", events.size, ex) + + events.forEach { event -> + couponIssueDltPublisher.publish( + CouponIssueDltEvent( + source = DltSource.CONSUMER, + couponId = event.couponId, + userId = event.userId, + reason = ex.message ?: ex::class.java.simpleName + ) + ) + } + ack.acknowledge() + } + } + + @KafkaListener( + topics = [TOPIC_DLT], + groupId = GROUP_ID_DLT, + containerFactory = "kafkaListenerContainerFactory" + ) + fun onDltMessage(message: String, ack: Acknowledgment) { + try { + val event = objectMapper.readValue(message, CouponIssueDltEvent::class.java) + log.warn( + "[DLT][{}] couponId={}, userId={}, reason={}", + event.source, + event.couponId, + event.userId, + event.reason + ) + } catch (e: Exception) { + log.error("Failed to parse DLT event json: {}", message, e) + } finally { + ack.acknowledge() + } + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt new file mode 100644 index 0000000..5b78e15 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/Coupon.kt @@ -0,0 +1,27 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.domain.entity + +import jakarta.persistence.Entity +import jakarta.persistence.Table +import main.kotlin.com.woong2e.couponsystem.coupon.status.CouponErrorStatus +import main.kotlin.com.woong2e.couponsystem.global.exception.CustomException +import main.kotlin.com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity + +@Entity +@Table(name = "coupons") +class Coupon( + val title: String, + val totalQuantity: Int, + var issuedQuantity: Int = 0 +) : PrimaryKeyEntity() { + + fun available(): Boolean { + return issuedQuantity < totalQuantity + } + + fun issue() { + if (!available()) { + throw CustomException(CouponErrorStatus.COUPON_OUT_OF_STOCK) + } + this.issuedQuantity++ + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt new file mode 100644 index 0000000..2128f17 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/entity/IssuedCoupon.kt @@ -0,0 +1,27 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.domain.entity + +import jakarta.persistence.* +import main.kotlin.com.woong2e.couponsystem.global.jpa.PrimaryKeyEntity +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.LocalDateTime +import java.util.* + +@Entity +@Table(name = "issued_coupons") +@EntityListeners(AuditingEntityListener::class) +class IssuedCoupon( + @Column(name = "coupon_id", nullable = false, columnDefinition = "BINARY(16)") + val couponId: UUID, + + @Column(name = "user_id", nullable = false, columnDefinition = "BIGINT") + val userId: Long +) : PrimaryKeyEntity() { + + @Column(nullable = false) + var status: String = "ISSUED" + + @CreatedDate + @Column(name = "issued_at", updatable = false) + var issuedAt: LocalDateTime? = null +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponBatchRepository.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponBatchRepository.kt new file mode 100644 index 0000000..33369a6 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponBatchRepository.kt @@ -0,0 +1,8 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.domain.repository + +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon + +interface IssuedCouponBatchRepository { + + fun batchInsert(coupons: List) +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt new file mode 100644 index 0000000..713c1c4 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/domain/repository/IssuedCouponRepository.kt @@ -0,0 +1,8 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.domain.repository + +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon + +interface IssuedCouponRepository { + + fun save(issuedCoupon: IssuedCoupon): IssuedCoupon +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/kafka/KafkaCouponIssueDltPublisher.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/kafka/KafkaCouponIssueDltPublisher.kt new file mode 100644 index 0000000..a7ca296 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/kafka/KafkaCouponIssueDltPublisher.kt @@ -0,0 +1,29 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.infra.kafka + +import main.kotlin.com.woong2e.couponsystem.coupon.application.port.out.CouponIssueDltPublisher +import main.kotlin.com.woong2e.couponsystem.coupon.consumer.event.CouponIssueDltEvent +import org.slf4j.LoggerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component + +@Component +class KafkaCouponIssueDltPublisher( + private val kafkaTemplate: KafkaTemplate, +) : CouponIssueDltPublisher { + + private val log = LoggerFactory.getLogger(javaClass) + + companion object { + private const val TOPIC_DLT = "coupon-issue-dlt-topic" + } + + override fun publish(event: CouponIssueDltEvent) { + val key = event.userId.toString() + + kafkaTemplate.send(TOPIC_DLT, key, event).whenComplete { _, ex -> + if (ex != null) { + log.error("Failed to send to DLT. couponId={}, userId={}", event.couponId, event.userId, ex) + } + } + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJdbcRepository.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJdbcRepository.kt new file mode 100644 index 0000000..8a8a21e --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJdbcRepository.kt @@ -0,0 +1,49 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.infra.persistence + +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponBatchRepository +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Repository +import java.nio.ByteBuffer +import java.sql.PreparedStatement +import java.sql.Timestamp +import java.time.LocalDateTime +import java.util.* + +@Repository +class IssuedCouponJdbcRepository( + private val jdbcTemplate: JdbcTemplate +) : IssuedCouponBatchRepository { + + override fun batchInsert(coupons: List) { + val sql = """ + INSERT INTO issued_coupons (id, coupon_id, user_id, status, issued_at) + VALUES (?, ?, ?, ?, ?) + """.trimIndent() + + val now = Timestamp.valueOf(LocalDateTime.now()) + + jdbcTemplate.batchUpdate( + sql, + coupons, + coupons.size + ) { ps: PreparedStatement, coupon: IssuedCoupon -> + ps.setBytes(1, uuidToBytes(coupon.id)) + + ps.setBytes(2, uuidToBytes(coupon.couponId)) + + ps.setLong(3, coupon.userId) + + ps.setString(4, coupon.status) + + ps.setTimestamp(5, now) + } + } + + private fun uuidToBytes(uuid: UUID): ByteArray { + val bb = ByteBuffer.wrap(ByteArray(16)) + bb.putLong(uuid.mostSignificantBits) + bb.putLong(uuid.leastSignificantBits) + return bb.array() + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt new file mode 100644 index 0000000..623de88 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/infra/persistence/IssuedCouponJpaRepository.kt @@ -0,0 +1,9 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.infra.persistence + +import main.kotlin.com.woong2e.couponsystem.coupon.domain.entity.IssuedCoupon +import main.kotlin.com.woong2e.couponsystem.coupon.domain.repository.IssuedCouponRepository +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface IssuedCouponJpaRepository : IssuedCouponRepository, JpaRepository { +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt new file mode 100644 index 0000000..b261013 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/status/CouponErrorStatus.kt @@ -0,0 +1,17 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.status + +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import org.springframework.http.HttpStatus + +enum class CouponErrorStatus( + override val status: HttpStatus, + override val code: String, + override val message: String +) : BaseErrorStatus { + + COUPON_NOT_FOUND(HttpStatus.NOT_FOUND, "C001", "존재하지 않는 쿠폰입니다."), + COUPON_ALREADY_ISSUED(HttpStatus.CONFLICT, "C002", "이미 쿠폰이 발급되었습니다."), + COUPON_OUT_OF_STOCK(HttpStatus.CONFLICT, "C003", "쿠폰이 모두 소진되었습니다."), + INVALID_COUPON_ISSUE_SERVICE_TYPE(HttpStatus.BAD_REQUEST, "C004", "지원하지 않는 쿠폰 발급 방식입니다."), + DUPLICATED_COUPON_ISSUE(HttpStatus.CONFLICT, "C004", "이미 발급된 쿠폰입니다."); +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt new file mode 100644 index 0000000..bf0c4bc --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/coupon/value/DltSource.kt @@ -0,0 +1,5 @@ +package main.kotlin.com.woong2e.couponsystem.coupon.value + +enum class DltSource { + PRODUCER, CONSUMER +} diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt new file mode 100644 index 0000000..f4bc0e4 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt @@ -0,0 +1,7 @@ +package main.kotlin.com.woong2e.couponsystem.global.exception + +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus + +class CustomException( + val errorCode: BaseErrorStatus +) : RuntimeException(errorCode.message) \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..bc4541a --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/exception/GlobalExceptionHandler.kt @@ -0,0 +1,37 @@ +package main.kotlin.com.woong2e.couponsystem.global.exception + +import main.kotlin.com.woong2e.couponsystem.global.response.ApiResponse +import main.kotlin.com.woong2e.couponsystem.global.response.status.ErrorStatus +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + + private val log = LoggerFactory.getLogger(this::class.java) + + // [1] 커스텀 예외 + @ExceptionHandler(CustomException::class) + fun handleCustomException(e: CustomException): ResponseEntity> { + log.warn("CustomException: {}", e.errorCode.message) + return ApiResponse.onFailure(e.errorCode) + } + + // [2] 유효성 검사 실패 (@Valid) + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity> { + val errorMessage = ex.bindingResult.allErrors.firstOrNull()?.defaultMessage ?: "요청 입력값이 올바르지 않습니다." + log.warn("Validation Failed: {}", errorMessage) + return ApiResponse.onFailure(ErrorStatus.INVALID_INPUT_VALUE, errorMessage) + } + + // [3] 나머지 예외 처리 + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity> { + log.error("UnhandledException: ", e) + return ApiResponse.onFailure(ErrorStatus.INTERNAL_SERVER_ERROR) + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt new file mode 100644 index 0000000..8ddb111 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/JpaConfig.kt @@ -0,0 +1,8 @@ +package main.kotlin.com.woong2e.couponsystem.global.jpa + +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing + +@Configuration +@EnableJpaAuditing +class JpaConfig \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt new file mode 100644 index 0000000..5cdef72 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/jpa/PrimaryKeyEntity.kt @@ -0,0 +1,43 @@ +package main.kotlin.com.woong2e.couponsystem.global.jpa + +import com.github.f4b6a3.ulid.UlidCreator +import jakarta.persistence.* +import org.hibernate.proxy.HibernateProxy +import org.springframework.data.domain.Persistable +import java.util.* + +@MappedSuperclass +abstract class PrimaryKeyEntity : Persistable { + @Id + @Column(columnDefinition = "BINARY(16)") + private val id: UUID = UlidCreator.getMonotonicUlid().toUuid() + + @Transient + private var _isNew = true + + override fun getId(): UUID = id + + override fun isNew(): Boolean = _isNew + + override fun equals(other: Any?): Boolean { + if (other == null) return false + if (other !is HibernateProxy && this::class != other::class) return false + return id == getIdentifier(other) + } + + private fun getIdentifier(obj: Any): UUID { + return if (obj is HibernateProxy) { + obj.hibernateLazyInitializer.identifier as UUID + } else { + (obj as PrimaryKeyEntity).id + } + } + + override fun hashCode() = Objects.hashCode(id) + + @PostPersist + @PostLoad + protected fun load() { + _isNew = false + } +} diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt new file mode 100644 index 0000000..feeeb2d --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/ApiResponse.kt @@ -0,0 +1,50 @@ +package main.kotlin.com.woong2e.couponsystem.global.response + +import com.fasterxml.jackson.annotation.JsonInclude +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseCode +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import org.springframework.http.ResponseEntity + +data class ApiResponse( + val isSuccess: Boolean, + val code: String, + val message: String, + @param:JsonInclude(JsonInclude.Include.NON_NULL) + val result: T? = null +) { + companion object { + // [성공] 데이터가 있는 경우 & 없는 경우 (result 기본값 null 활용) + fun onSuccess(code: + BaseCode, result: T? = null): ResponseEntity> { + return ResponseEntity + .status(code.status) + .body( + ApiResponse( + isSuccess = true, + code = code.code, + message = code.message, + result = result + ) + ) + } + + // [실패] 커스텀 메시지 없이 기본 메시지 사용 + fun onFailure(code: BaseErrorStatus): ResponseEntity> { + return onFailure(code, code.message) + } + + // [실패] 커스텀 메시지 사용 + fun onFailure(code: BaseErrorStatus, message: String): ResponseEntity> { + return ResponseEntity + .status(code.status) + .body( + ApiResponse( + isSuccess = false, + code = code.code, + message = message, + result = null + ) + ) + } + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt new file mode 100644 index 0000000..2f0b6fa --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseCode.kt @@ -0,0 +1,9 @@ +package main.kotlin.com.woong2e.couponsystem.global.response.code + +import org.springframework.http.HttpStatus + +interface BaseCode { + val status: HttpStatus + val code: String + val message: String +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt new file mode 100644 index 0000000..cfa3cdf --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/code/BaseErrorStatus.kt @@ -0,0 +1,9 @@ +package main.kotlin.com.woong2e.couponsystem.global.response.code + +import org.springframework.http.HttpStatus + +interface BaseErrorStatus { + val status: HttpStatus + val code: String + val message: String +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt new file mode 100644 index 0000000..a3147f8 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/ErrorStatus.kt @@ -0,0 +1,19 @@ +package main.kotlin.com.woong2e.couponsystem.global.response.status + +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseErrorStatus +import org.springframework.http.HttpStatus + +enum class ErrorStatus( + override val status: HttpStatus, + override val code: String, + override val message: String + ) : BaseErrorStatus { + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "G001", "서버 내부 오류입니다."), + INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "G002", "올바르지 않은 입력값입니다."), + SYSTEM_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S001", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), + // 세마포어/트래픽 초과 시 (Fail-Fast 또는 타임아웃 발생 시) + SYSTEM_BUSY(HttpStatus.SERVICE_UNAVAILABLE, "S002", "현재 접속량이 많아 처리가 지연되고 있습니다. 잠시 후 다시 시도해주세요."), + // 대기 시간 초과 (Blocking 방식에서 타임아웃 발생 시) + COUPON_ISSUE_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "S003", "대기 시간이 초과되었습니다. 다시 시도해주세요."); +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt new file mode 100644 index 0000000..633eea3 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/global/response/status/SuccessStatus.kt @@ -0,0 +1,18 @@ +package main.kotlin.com.woong2e.couponsystem.global.response.status + +import main.kotlin.com.woong2e.couponsystem.global.response.code.BaseCode +import org.springframework.http.HttpStatus + +enum class SuccessStatus( + override val status: HttpStatus, + override val code: String, + override val message: String +) : BaseCode { + + // 일반적인 성공 (200 OK) + OK(HttpStatus.OK, "200", "요청이 성공적으로 처리되었습니다."), + // 생성 성공 (201 Created) + CREATED(HttpStatus.CREATED, "201", "리소스가 성공적으로 생성되었습니다."), + // 삭제 요청 성공 (204 NO_CONTENT) + NO_CONTENT(HttpStatus.NO_CONTENT, "204", "요청한 리소스가 성공적으로 삭제되었습니다."); +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaConsumerConfig.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaConsumerConfig.kt new file mode 100644 index 0000000..689b281 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/infra/kafka/KafkaConsumerConfig.kt @@ -0,0 +1,47 @@ +package main.kotlin.com.woong2e.couponsystem.infra.kafka + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.boot.autoconfigure.kafka.KafkaProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.annotation.EnableKafka +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.listener.ContainerProperties + +@EnableKafka +@Configuration +class KafkaConsumerConfig( + private val kafkaProperties: KafkaProperties +) { + + @Bean + fun consumerFactory(): ConsumerFactory { + val props = kafkaProperties.buildConsumerProperties() + + props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers + props[ConsumerConfig.GROUP_ID_CONFIG] = kafkaProperties.consumer.groupId + props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = kafkaProperties.consumer.autoOffsetReset + + props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = false + + props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java + props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java + + + return DefaultKafkaConsumerFactory(props) + } + + @Bean + fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory { + val factory = ConcurrentKafkaListenerContainerFactory() + factory.consumerFactory = consumerFactory() + + factory.setConcurrency(3) + factory.isBatchListener = true + factory.containerProperties.ackMode = kafkaProperties.listener.ackMode ?: ContainerProperties.AckMode.MANUAL + return factory + } +} \ No newline at end of file diff --git a/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt new file mode 100644 index 0000000..dcfb3c4 --- /dev/null +++ b/coupon-consumer/src/main/kotlin/com/woong2e/couponsystem/user/domin/User.kt @@ -0,0 +1,13 @@ +package main.kotlin.com.woong2e.couponsystem.user.domin + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +class User( + val name: String +) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null +} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/coupon-consumer/src/main/resources/application-local.yml similarity index 70% rename from src/main/resources/application-local.yml rename to coupon-consumer/src/main/resources/application-local.yml index 7a87396..ca63db6 100644 --- a/src/main/resources/application-local.yml +++ b/coupon-consumer/src/main/resources/application-local.yml @@ -1,4 +1,14 @@ spring: + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: coupon-worker-group + auto-offset-reset: latest + enable-auto-commit: false + listener: + ack-mode: manual + type: batch + datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} @@ -22,6 +32,9 @@ spring: redis: host: localhost port: 6379 + max-active: 64 + max-idle: 32 + min-idle: 16 management: endpoints: diff --git a/src/main/resources/application-prod.yml b/coupon-consumer/src/main/resources/application-prod.yml similarity index 64% rename from src/main/resources/application-prod.yml rename to coupon-consumer/src/main/resources/application-prod.yml index c7ec4a6..9c7c0c9 100644 --- a/src/main/resources/application-prod.yml +++ b/coupon-consumer/src/main/resources/application-prod.yml @@ -1,15 +1,28 @@ +server: + port: 8081 + spring: config: activate: on-profile: prod + kafka: + bootstrap-servers: kafka:29092 + consumer: + group-id: coupon-worker-group + auto-offset-reset: latest + enable-auto-commit: false + listener: + ack-mode: manual + type: batch + datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: - maximum-pool-size: 15 + maximum-pool-size: 20 connection-timeout: 30000 jpa: @@ -29,6 +42,9 @@ spring: redis: host: redis port: 6379 + max-active: 64 + max-idle: 32 + min-idle: 16 management: endpoints: @@ -37,4 +53,7 @@ management: include: prometheus, health, info endpoint: prometheus: - enabled: true \ No newline at end of file + enabled: true + metrics: + tags: + application: coupon-consumer \ No newline at end of file diff --git a/coupon-consumer/src/main/resources/application.yml b/coupon-consumer/src/main/resources/application.yml new file mode 100644 index 0000000..ef1a90f --- /dev/null +++ b/coupon-consumer/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: couponsystem + profiles: + active: local # 기본적으로 'local' 프로필을 사용 (RDS 쓸 땐 'prod'로 변경) diff --git a/coupon-consumer/src/test/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplicationTests.kt b/coupon-consumer/src/test/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplicationTests.kt new file mode 100644 index 0000000..cde211d --- /dev/null +++ b/coupon-consumer/src/test/kotlin/com/woong2e/couponsystem/CouponsystemConsumerApplicationTests.kt @@ -0,0 +1,16 @@ +package test.kotlin.com.woong2e.couponsystem + +import main.kotlin.com.woong2e.couponsystem.CouponsystemConsumerApplication +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles + +@SpringBootTest(classes = [CouponsystemConsumerApplication::class]) +@ActiveProfiles("test") +class CouponsystemConsumerApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/coupon-consumer/src/test/resources/application-test.yml b/coupon-consumer/src/test/resources/application-test.yml new file mode 100644 index 0000000..0fbc799 --- /dev/null +++ b/coupon-consumer/src/test/resources/application-test.yml @@ -0,0 +1,30 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL + driver-class-name: org.h2.Driver + username: sa + password: + + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + database-platform: org.hibernate.dialect.H2Dialect + + data: + redis: + host: localhost + port: 6379 + + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: test-group + auto-offset-reset: latest + enable-auto-commit: false + listener: + ack-mode: manual + type: batch \ No newline at end of file diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 2b92a61..f63696b 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -16,24 +16,42 @@ echo "🚀 배포 시작..." # 2. ✅ 공용 네트워크 생성 docker network create app-network 2>/dev/null || true -# 3. ✅DB & Redis 인프라 배포 (변경 사항이 있을 때만 재시작됨) +# 3. ✅ Kafka 인프라 배포 (Consumer 실행을 위해 필수!) +if [ -f "docker-compose-kafka.yml" ]; then + echo "🐦 Kafka 인프라 배포 중..." + docker-compose -f docker-compose-kafka.yml pull + docker-compose -f docker-compose-kafka.yml up -d + + echo "⏳ Kafka 초기화 대기 (10초)..." + sleep 10 +else + echo "⚠️ docker-compose-kafka.yml 없음. Kafka 배포 스킵." +fi + +# 4. ✅ DB & Redis 인프라 배포 if [ -f "docker-compose-database.yml" ]; then + echo "💾 DB & Redis 인프라 배포 중..." docker-compose -f docker-compose-database.yml pull docker-compose -f docker-compose-database.yml up -d else - echo "⚠️ docker-compose-database.yml 파일이 없습니다. DB 인프라 배포 스킵." + echo "⚠️ docker-compose-database.yml 없음. DB 배포 스킵." fi -# 4. App 및 Nginx 배포 -echo "Start Application..." +# 5. App 및 Nginx 배포 +echo "☕ Application (API & Worker) 배포 시작..." -# (1) 최신 이미지 Pull (App만) -docker-compose -f docker-compose.yml pull app +if [ -f "docker-compose.yml" ]; then + # (1) 최신 이미지 Pull (API, Consumer, Nginx 등 모두 다운로드) + docker-compose -f docker-compose.yml pull -# (2) 컨테이너 실행 -docker-compose -f docker-compose.yml up -d --scale app=2 + # (2) 컨테이너 실행 + docker-compose -f docker-compose.yml up -d --scale api-server=2 +else + echo "❌ docker-compose.yml 파일이 없습니다!" + exit 1 +fi -# 5. 미사용 이미지 정리 +# 6. 미사용 이미지 정리 docker image prune -f echo "✅ 배포 완료!" \ No newline at end of file diff --git a/docker-compose/docker-compose-database.yml b/docker-compose/docker-compose-database.yml index 4ab8ecd..21727a6 100644 --- a/docker-compose/docker-compose-database.yml +++ b/docker-compose/docker-compose-database.yml @@ -49,6 +49,7 @@ services: - "9121:9121" networks: - app-network + restart: always networks: app-network: diff --git a/docker-compose/docker-compose-kafka.yml b/docker-compose/docker-compose-kafka.yml new file mode 100644 index 0000000..0c5bf32 --- /dev/null +++ b/docker-compose/docker-compose-kafka.yml @@ -0,0 +1,69 @@ +version: '3.8' + +services: + kafka: + image: confluentinc/cp-kafka:7.6.1 + container_name: kafka + hostname: kafka + environment: + # KRaft 설정 + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' + CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' + + # 리스너 설정 (내부 통신용) + KAFKA_LISTENERS: 'PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + + # 데이터 경로 + KAFKA_LOG_DIRS: '/var/lib/kafka/data' + + # 브로커 설정 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + + KAFKA_HEAP_OPTS: "-Xmx400m -Xms400m" + + volumes: + - ./kafka-data:/var/lib/kafka/data + + networks: + - app-network + restart: always + + kafka-ui: + image: provectuslabs/kafka-ui:latest + container_name: kafka-ui + ports: + - "8090:8080" + environment: + KAFKA_CLUSTERS_0_NAME: prod-kraft + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + networks: + - app-network + depends_on: + - kafka + restart: always + + kafka-exporter: + image: danielqsj/kafka-exporter + container_name: kafka-exporter + command: ["--kafka.server=kafka:29092"] + ports: + - "9308:9308" + networks: + - app-network + depends_on: + - kafka + restart: always + +networks: + app-network: + external: true \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 4837732..8989cde 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -15,7 +15,7 @@ services: networks: - app-network depends_on: - - app + - api-server restart: always # 2. Certbot @@ -61,22 +61,37 @@ services: networks: - app-network - # 4. Spring Boot App - app: - image: ${DOCKER_HUB_USERNAME}/couponsystem:latest + # 4. [API] Producer Server + api-server: + image: ${DOCKER_HUB_USERNAME}/coupon-api:latest restart: always deploy: replicas: 2 env_file: - .env environment: - - JAVA_TOOL_OPTIONS=-Xms512m -Xmx800m + - JAVA_TOOL_OPTIONS=-Xms512m -Xmx512m ports: - "8081-8082:8080" networks: - app-network -# ✅ 네트워크 설정 변경 + # 5. [Worker] Consumer Server + consumer-worker: + image: ${DOCKER_HUB_USERNAME}/coupon-consumer:latest + container_name: consumer-worker + restart: always + deploy: + replicas: 1 + env_file: + - .env + environment: + - JAVA_TOOL_OPTIONS=-Xms400m -Xmx400m + ports: + - "8083:8081" + networks: + - app-network + networks: app-network: external: true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -86,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,6 +115,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -171,6 +173,7 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,14 +206,15 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index e509b2d..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,10 +70,11 @@ goto fail :execute @rem Setup the command line +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 7abdd12..6418e49 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,6 +1,6 @@ # ✅ Load Balancing 설정 upstream spring_app { - server app:8080; + server api-server:8080; } server { diff --git a/settings.gradle.kts b/settings.gradle.kts index 43a4704..067996f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,4 @@ -rootProject.name = "couponsystem" \ No newline at end of file +rootProject.name = "couponsystem" + +include("coupon-api") +include("coupon-consumer") \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt b/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt deleted file mode 100644 index 385eab4..0000000 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/response/CouponIssueResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.woong2e.couponsystem.coupon.application.response - -import java.util.UUID - -data class CouponIssueResponse( - val result: String = "SUCCESS", - val issuedCouponId: UUID? = null -) diff --git a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt b/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt deleted file mode 100644 index d7717d9..0000000 --- a/src/main/kotlin/com/woong2e/couponsystem/coupon/application/service/CouponIssueService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.woong2e.couponsystem.coupon.application.service - -import com.woong2e.couponsystem.coupon.application.response.CouponIssueResponse -import java.util.UUID - -interface CouponIssueService { - - fun issue(couponId: UUID, userId: Long): CouponIssueResponse -} \ No newline at end of file diff --git a/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt b/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt deleted file mode 100644 index 088f5df..0000000 --- a/src/main/kotlin/com/woong2e/couponsystem/global/exception/CustomException.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.woong2e.couponsystem.global.exception - -import com.woong2e.couponsystem.global.response.code.BaseErrorStatus - -class CustomException( - val errorCode: BaseErrorStatus -) : RuntimeException(errorCode.message) \ No newline at end of file