Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8cc7060
[chore]: gitignore 설정을 추가한다 (#11)
woong2e Jan 4, 2026
4296049
[refactor]: 단일 모듈을 api와 consumer 모듈로 분리한다 (#11)
woong2e Jan 4, 2026
eafa4ef
[config]: consumer application.yml 설정을 추가한다 (#11)
woong2e Jan 4, 2026
b6c401d
[ci]: 멀티 모듈 빌드 및 배포 파이프라인을 구성한다 (#11)
woong2e Jan 4, 2026
5bdf5a5
[feat]: coupon-consumer 모듈을 추가한다 (#11)
woong2e Jan 4, 2026
f6c4015
[build]: 멀티 모듈 Gradle 환경 구성 및 모듈을 정의한다 (#11)
woong2e Jan 4, 2026
db9aa53
[chore]: 배포 스크립트를 수정한다 (#11)
woong2e Jan 4, 2026
e5a0d8f
[chore]: nginx 설정을 변경한다 (#11)
woong2e Jan 4, 2026
903c2b6
[chore]: Kafka docker-compose 설정을 추가한다 (KRaft, UI, Exporter) (#11)
woong2e Jan 4, 2026
b5742d2
[ci]: DockerFile 설정을 수정한다 (#11)
woong2e Jan 4, 2026
f94e875
[chore]: nginx 설정파일 오타를 수정한다 (#11)
woong2e Jan 4, 2026
0e2eb49
[chore]: 애플리케이션 서버 배포시 --remove-orphans 옵션을 제거한다 (#11)
woong2e Jan 4, 2026
838b525
[chore]: kafka 포맷팅 스크립트를 추가한다 (#11)
woong2e Jan 4, 2026
cb98ea2
[chore]: kafka 포맷팅 스크립트를 수정한다 (#11)
woong2e Jan 4, 2026
b297013
[chore]: kafka 포맷팅 커멘드를 삭제한다 (#11)
woong2e Jan 4, 2026
0483b8c
[refactor]: coupon의 infra 계층 구조를 개선한다 (#11)
woong2e Jan 4, 2026
d6a8aac
[feat]: 동기, 비동기 처리 방식의 응답에 대한 팩토리 메서드를 추가한다 (#11)
woong2e Jan 5, 2026
47510aa
[feat]: Redis Lua + Kafka 기반 비동기 쿠폰 발급 방식을 구현한다(Producer) (#11)
woong2e Jan 5, 2026
b43619b
[feat]: Redis Lua + Kafka 기반 비동기 쿠폰 발급 방식을 구현한다(Consumer) (#11)
woong2e Jan 5, 2026
c497dc4
[feat]: Kafka Producer 설정파일을 등록한다 (#11)
woong2e Jan 5, 2026
6536a7f
[feat]: Kafka Consumer 설정파일을 등록한다 (#11)
woong2e Jan 5, 2026
b3846db
[feat]: Kafka Topic 설정파일을 등록한다 (#11)
woong2e Jan 5, 2026
38d1120
[fix]: CouponIssueWorkerService이 Component 스캔 대상이 되도록 `@Service`를 추가한…
woong2e Jan 5, 2026
d94286c
[feat]: 로컬 kafka 설정을 추가한다 (#11)
woong2e Jan 5, 2026
f413eea
[fix]: Json 직렬화, 역직렬화 도구를 버전에 맞게 수정한다 (#11)
woong2e Jan 5, 2026
2bb0eb9
[chore]: 테스트 kafka 설정을 추가한다 (#11)
woong2e Jan 5, 2026
0d9bbf7
[build]: SpringBoot 버전을 안정 버전으로 다운그레이드 한다 (#11)
woong2e Jan 5, 2026
e937547
[chore]: 설정파일을 수정한다 (#11)
woong2e Jan 5, 2026
7dde18e
[refactor]: 변경된 SpringBoot 버전에 맞게 Kafka 설정을 변경한다 (#11)
woong2e Jan 5, 2026
9df0ba5
[build]: gradle 버전을 변경한다 (#11)
woong2e Jan 5, 2026
8ccd714
[fix]: 적절한 JsonSerializer import하도록 수정한다 (#11)
woong2e Jan 5, 2026
1260e5c
[fix]: 메시지 헤더의 클래스 정보를 무시하고 Consumer가 지정한 Event 객체로 내용을 받도록 수정한다 (#11)
woong2e Jan 5, 2026
49e353d
[chore]: prometheus scraping을 위해 8083 포트를 외부에 오픈한다 (#11)
woong2e Jan 5, 2026
228568a
[chore]: gradlew 파일 변경(#11)
woong2e Jan 5, 2026
1d8ad40
[fix]: 클래스명을 직접 명시하여 매핑하도록 변경한다 (#11)
woong2e Jan 5, 2026
00ddf93
[chore]: listener의 ack-mode를 manual로 변경하여 수동으로 커밋하도록 변경한다 (#11)
woong2e Jan 5, 2026
13562d7
[pref]: Kafka Producer 배치 전송 및 압축 설정을 적용한다 (#11)
woong2e Jan 11, 2026
78c7383
[chore]: 서버 재시작 시 redis-exporter가 항상 재시작할 수 있도록 설정한다 (#11)
woong2e Jan 11, 2026
ce09098
[fix]: 배치 사이즈를 Int로 직접 지정한다 (#11)
woong2e Jan 11, 2026
feeed81
[chore]: 배치 사이즈, linger.ms를 변경한다 (#11)
woong2e Jan 11, 2026
71791b7
[feat]: 배치 사이즈, linger.ms를 변경한다 (#11)
woong2e Jan 11, 2026
fe7a506
[feat]: 처리되지 않은 event를 dlt 토픽으로 추가하여 처리한다 (#11)
woong2e Jan 11, 2026
3f2f2be
[feat]: consumer가 쿠폰 저장을 배치처리 하도록 구현한다 (#11)
woong2e Jan 12, 2026
bdb71c2
[rename]: JPA 클래스명 컨벤션을 변경한다 (#11)
woong2e Jan 12, 2026
3f8c843
[fix]: AckMode를 MANUAL로 수정한다 (#11)
woong2e Jan 12, 2026
53c4342
[chore]: test 설정파일의 enable-auto-commit을 false로 설정한다 (#11)
woong2e Jan 12, 2026
18e52dd
[fix]: 쿼리에 테이블 명을 수정한다 (#11)
woong2e Jan 12, 2026
f7773cc
[fix]: 객체를 직렬화하여 전송하도록 수정한다 (#11)
woong2e Jan 12, 2026
da606ba
[feat]: Json -> String 직렬화/역직렬화 방식으로 변경한다 (#11)
woong2e Jan 12, 2026
bb2081c
[fix]: 컬럼 데이터 타입에 맞게 PreparedStatement를 변경한다 (#11)
woong2e Jan 12, 2026
b2500f0
[fix]: userId 정의를 DB 데이터 타입에 맞게 수정한다 (#11)
woong2e Jan 12, 2026
58eeab2
[chore]: 10ms 동안 기다렸다가 모아서 발송하도록 수정한다 (#11)
woong2e Jan 12, 2026
da17645
[chore]: redis의 Connection pool을 증가시킨다 (#11)
woong2e Jan 12, 2026
2b46e80
[chore]: producer 서버 Tomcat의 스레드를 50개로 줄인다 (#11)
woong2e Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,37 +40,60 @@ 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
with:
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:
Expand All @@ -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/[email protected]
env:
Expand All @@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 43 additions & 51 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Test> {
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<JavaPluginExtension> {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

// 컴파일 옵션
tasks.withType<KotlinCompile> {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
jvmTarget.set(JvmTarget.JVM_21)
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
}
5 changes: 1 addition & 4 deletions Dockerfile → coupon-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
35 changes: 35 additions & 0 deletions coupon-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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<String>) {
runApplication<CouponsystemApplication>(*args)
runApplication<CouponsystemApiApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.woong2e.couponsystem.coupon.api.request
package main.kotlin.com.woong2e.couponsystem.coupon.api.request

import java.util.UUID

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.woong2e.couponsystem.coupon.api.request
package main.kotlin.com.woong2e.couponsystem.coupon.api.request

import java.util.UUID

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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 = "쿠폰 발급이 완료되었습니다."
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.woong2e.couponsystem.coupon.application.response
package main.kotlin.com.woong2e.couponsystem.coupon.application.response

import java.util.UUID

Expand Down
Loading