Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
6f82d72
wip: Wishlist CRUD 구현 중
jungu8745 Sep 7, 2026
efc0ccb
feat(product): Drop 연결 상품 삭제 제한
Sep 7, 2026
9e76874
Merge remote-tracking branch 'origin/dev' into feature/product-drop-d…
Sep 8, 2026
f239b10
Merge branch 'dev' into feature/product-drop-delete-policy
ddooyn Sep 8, 2026
18e3b2a
Merge pull request #39 from drop-it-dev/feature/product-drop-delete-p…
ddooyn Sep 8, 2026
2aff7dd
feat: Wishlist CRUD 및 테스트 구현
jungu8745 Sep 9, 2026
de3f7e4
Merge pull request #42 from drop-it-dev/feature/wishlist
jungu8745 Sep 9, 2026
2004592
feat: Notification 기본 기능 구현
jungu8745 Sep 9, 2026
7a39ce8
feat(auth): 회원가입 시 USER 및 SELLER 역할 선택 기능 추가
gustavors88 Sep 9, 2026
36b5312
Merge pull request #44 from drop-it-dev/feature/notification
ddooyn Sep 9, 2026
8b24415
Merge pull request #46 from drop-it-dev/feature/signup-role
ddooyn Sep 9, 2026
37bb36a
feat(concurrency): 상품 삭제와 Drop 생성 경쟁 상태 제어
Sep 10, 2026
6ab3aaa
test(concurrency): 동시성 검증 결과와 비교 이미지 추가
Sep 10, 2026
c7d5583
Merge pull request #49 from drop-it-dev/feature/product-drop-concurrency
ddooyn Sep 10, 2026
7060b46
fix(drop): 판매 정책 유효성 검증 보완
ddooyn Sep 10, 2026
849b671
fix(order): 동시 주문 재고 잠금 처리
ddooyn Sep 10, 2026
710b69d
test(concurrency): Testcontainers로 경합 테스트 환경 격리
ddooyn Sep 10, 2026
5137671
test(drop): 판매 값 경계 및 부분 수정 검증 보강
ddooyn Sep 10, 2026
2805892
feat(s3-images): 판매자 프로필 및 상품 이미지 삭제 기능 추가"
gustavors88 Sep 10, 2026
1aa86bd
fix(drop): preserve sold quantity when updating initial stock
coderabbitai[bot] Sep 10, 2026
17fb840
fix(order): 판매 후 재고 정합성과 동시성 테스트 보강
ddooyn Sep 10, 2026
7639868
Merge remote-tracking branch 'origin/feature/drop-order-consistency' …
ddooyn Sep 10, 2026
1af889e
Merge pull request #51 from drop-it-dev/feature/drop-order-consistency
ddooyn Sep 10, 2026
633dfb1
feat(order): 사용자별 구매량 카운터를 추가
ddooyn Sep 10, 2026
274625d
feat(order): 구매 카운터를 주문 흐름에 연결
ddooyn Sep 10, 2026
d18cd90
test(order): 사용자 구매 한도 경합을 검증
ddooyn Sep 10, 2026
99162ec
fix(order): 구매 카운터 차감 실패 시 주문 취소 롤백
ddooyn Sep 10, 2026
8147b77
test(order): 구매 제한 예외와 카운터 호출 검증 추가
ddooyn Sep 10, 2026
4ef9d57
Merge pull request #54 from drop-it-dev/feature/user-purchase-limit
ddooyn Sep 10, 2026
b7b7930
Merge pull request #52 from drop-it-dev/feature/delete-image
gustavors88 Sep 10, 2026
f0b8259
chore(docs): RestDocs 설정 추가
ddooyn Sep 10, 2026
f55c9ea
chore(docs): Swagger UI로 OpenAPI 문서 제공
ddooyn Sep 10, 2026
83bbcab
chore(docs): 빈 OpenAPI 명세는 패키징 방지
ddooyn Sep 10, 2026
8a71ba0
chore(security): Basic Auth를 swagger-ui, openapi 경로 등에 적용
ddooyn Sep 10, 2026
bb514b2
test(docs): 도메인별 API DocumentationTest 생성
ddooyn Sep 10, 2026
faefd7a
test(docs): DocumentTest에 authorization Header 보완
ddooyn Sep 10, 2026
3288f33
chore(gradle): openAPI 명세 생성 태스크 추가
ddooyn Sep 10, 2026
342d6cd
chore(docs): 스키마 이름 명시
ddooyn Sep 10, 2026
a284e01
perf(drop): 대규모 목록 조회 쿼리 성능 개선
Sep 10, 2026
a79a5c8
feat(auth): 로그아웃 API 추가
gustavors88 Sep 10, 2026
47471bc
fix(docs): 서버 URL, pathParameters 수정
ddooyn Sep 10, 2026
ade6728
fix: 미사용 import 및 경고 정리
ddooyn Sep 10, 2026
9d01df6
Merge pull request #57 from drop-it-dev/feature/simple-logout
ddooyn Sep 10, 2026
47c62e7
chore(docs): 운영환경 try-it-out 비활성화
ddooyn Sep 10, 2026
602d336
chore(performance): PR 성능 산출물 정리
Sep 11, 2026
58670d5
Merge pull request #56 from drop-it-dev/chore/restdocs-openapi
Hyiyek Sep 11, 2026
b6236ea
Merge pull request #58 from drop-it-dev/feature/drop-query-index-perf…
ddooyn Sep 11, 2026
ebee020
feat(security): 드랍 목록 및 상세 조회 공개
ddooyn Sep 12, 2026
436cf4c
feat(s3): 상품 및 판매자 프로필 공개 이미지 URL 적용
ddooyn Sep 12, 2026
64f4230
fix(security): 공개 엔드포인트 JWT 필터 제외
ddooyn Sep 12, 2026
08f811f
test(performance): Drop 상세 조회 스파이크 시나리오 추가
Sep 12, 2026
2284b51
test(performance): Drop 스파이크 테스트 실행 자동화
Sep 12, 2026
6d85448
test(performance): Drop 스파이크 비교 대시보드 추가
Sep 12, 2026
36a8fca
Merge pull request #60 from drop-it-dev/feature/public-url-image
ddooyn Sep 12, 2026
1a55a3e
docs(performance): Drop 상세 조회 Before 결과 기록
Sep 12, 2026
b9a36f7
perf(drop): 상세 조회 fetch join 쿼리 추가
Sep 12, 2026
4928f05
perf(drop): 상세 조회에 fetch join 적용
Sep 12, 2026
3c3385f
test(performance): 쿼리 개선 측정 단계 분리
Sep 12, 2026
188f6fe
docs(performance): fetch join 개선 결과 기록
Sep 12, 2026
f3cf53d
feat(cache): Drop 상세 Redis 캐시 설정 추가
Sep 12, 2026
8b4343f
feat(drop): 상세 조회 Redis 캐시 적용
Sep 12, 2026
93617e6
docs(performance): Drop 상세 캐시 개선 결과 기록
Sep 12, 2026
adba19d
test(performance): Drop 1만명 동시 조회 시나리오 추가
Sep 12, 2026
6eaf4fe
test(performance): 동시 조회 테스트 실행 자동화
Sep 12, 2026
12c020d
perf(server): Drop 오픈 연결 대기열 확장
Sep 12, 2026
262c93f
test(performance): Docker 내부 테스트 주소 지원
Sep 12, 2026
b4e76db
feat(order): OrderRequest 엔티티 구성, 복합 유니크 제약 적용
ddooyn Sep 12, 2026
c12a2e9
feat(order): OrderRequestRepository 추가, 주문 중복 요청 검사용 X-lock 조회 추가
ddooyn Sep 12, 2026
22832f7
feat(order): 최종 주문확정 멱등처리를 위한 기반 구현
ddooyn Sep 12, 2026
3fc3675
feat(order): Redis 주문예약 기반 구현, Lua 스크립트 추가
ddooyn Sep 12, 2026
7608435
chore(sqs): 주문확정 처리 목적 SQS 환경 구성
ddooyn Sep 13, 2026
66f40e9
feat(order): 조건부 재고차감 기반으로 주문확정 처리
ddooyn Sep 13, 2026
17a07c2
feat(order): Redis 주문예약 최종상태 동기화 구현
ddooyn Sep 13, 2026
92d0543
feat(order): SQS 주문확정 Consumer 연결
ddooyn Sep 13, 2026
b833f4b
refactor(order): Redis 최종상태 동기화 트랜잭션 분리
ddooyn Sep 13, 2026
a50f66c
chore(order): SQS 주문확정 Consumer에 동시처리 상한 설정
ddooyn Sep 13, 2026
8abd0a1
fix(order): Consumer 처리 여유만큼 SQS 메시지 수신
ddooyn Sep 13, 2026
5455fd5
fix(order): Redis 동기화를 트랜잭션 밖에서 실행
ddooyn Sep 13, 2026
235a9be
fix(order): order-queue-url 기본값 지정, clearAutomatically 제거 피드백 반영
ddooyn Sep 13, 2026
3434f08
perf(server): 성능 프로필에 가상 스레드 적용
Sep 13, 2026
fd70f43
fix(order): acceptedAt 정밀도를 나노초에서 마이크로초 단위로 정규화
ddooyn Sep 13, 2026
9ea61be
fix(order): Consumer 수신 설정 시작 시 검증
ddooyn Sep 13, 2026
ef88ef9
perf(server): Drop 조회 4대 분산 환경 추가
Sep 13, 2026
2cbf5e6
fix(performance): 클러스터 시작 로그 검사 안정화
Sep 13, 2026
33342cf
fix(performance): 부하 직후 인증 재시도 처리
Sep 13, 2026
27d41f6
refactor(order): SQS 주문 소비를 manual ACK listener로 전환
ddooyn Sep 13, 2026
32d8100
feat(drop): 공개 시 주문 admission 상태 준비
ddooyn Sep 13, 2026
dd44c89
feat(order): Redis 예약 후 SQS 메시지 발행
ddooyn Sep 13, 2026
91641b1
feat(order): 공개 주문 접수와 요청 상태 조회 연결
ddooyn Sep 13, 2026
5ae2b35
feat(order): 취소 Redis 복구와 재시도 상태 기록
ddooyn Sep 13, 2026
493ddb0
refactor(order): 기존 동기 공개 주문 경로 제거
ddooyn Sep 13, 2026
3dec8af
fix(order): admission 후처리 재시도 안전성 보강
ddooyn Sep 13, 2026
762b957
fix(order): Redis 재시도 요청별 실패 격리
ddooyn Sep 13, 2026
97af1d3
test(order): 비동기 주문 접수와 복구 흐름 검증
ddooyn Sep 13, 2026
43856cd
test(drop): 검색 통합 테스트를 독립 실행 가능하게 수정
ddooyn Sep 13, 2026
532a3eb
fix(order): Consumer 비활성 시 Redis 재시도 중지
ddooyn Sep 13, 2026
0b54d20
chore(performance): Compose k6 실행 환경 추가
ddooyn Sep 13, 2026
9c0a7dd
chore(performance): k6 smoke 실행 명령 추가
ddooyn Sep 13, 2026
8bfa23e
perf(drop): 캐시 적중 조회의 DB 트랜잭션 제거
Sep 13, 2026
94dc320
test(performance): 동시 조회 최적화 단계 분리
Sep 13, 2026
a662968
feat(monitoring): Drop 1만명 동시 조회 비교 대시보드 추가
Sep 14, 2026
a7af923
docs(performance): Drop 1만명 동시 조회 개선 결과 정리
Sep 14, 2026
02a8570
Merge pull request #67 from drop-it-dev/test/order-flow
ddooyn Sep 14, 2026
64c0e58
Merge branch 'dev' into feature/drop-detail-spike-test
ddooyn Sep 14, 2026
67fddfb
Merge pull request #68 from drop-it-dev/feature/drop-detail-spike-test
ddooyn Sep 14, 2026
b070768
fix(drop): visibility 변경 시 상세 캐시 무효화
ddooyn Sep 14, 2026
5e0be05
Merge pull request #69 from drop-it-dev/fix/drop-visibility-cache
ddooyn Sep 14, 2026
d0cf707
refactor(drop): 상세 캐시와 실시간 재고 조회 분리
Sep 14, 2026
d1109de
test(performance): 실시간 재고 상세 조회 부하 검증 추가
Sep 14, 2026
4276dce
Merge branch 'dev' into feature/drop-detail-live-stock
ddooyn Sep 14, 2026
36306f0
Merge pull request #70 from drop-it-dev/feature/drop-detail-live-stock
ddooyn Sep 14, 2026
7f968eb
fix(drop): 상세 조회 재고를 DB 확정값으로 복원
ddooyn Sep 14, 2026
84861c7
fix(cache): 커밋 후 캐시 삭제 실패를 격리
ddooyn Sep 14, 2026
992ffcd
Merge pull request #71 from drop-it-dev/fix/drop-detail-stock-consist…
ddooyn Sep 14, 2026
f590744
chore(aws): CloudFront 도메인 환경 설정 추가
gustavors88 Sep 14, 2026
435760f
feat(storage): CloudFront 이미지 URL 및 캐시 정책 적용
gustavors88 Sep 14, 2026
453dbe8
test(storage): CloudFront 이미지 URL 및 캐시 정책 검증
gustavors88 Sep 14, 2026
5404762
A
gustavors88 Sep 14, 2026
d4f484a
Merge pull request #72 from drop-it-dev/feature/cloudfront-image-cache
ddooyn Sep 15, 2026
311fd53
fix(order): Redis 예약 상태 조회 오류 수정
ddooyn Sep 15, 2026
9ec91e0
ci(deploy): API와 Worker ASG 배포 분리
ddooyn Sep 15, 2026
7f69e3a
fix(order): 취소된 Redis 예약 상태 재조회 수정
ddooyn Sep 15, 2026
54b0632
Merge pull request #73 from drop-it-dev/feature/improve-order
ddooyn Sep 15, 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
Binary file added .DS_Store
Binary file not shown.
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ DB_PASSWORD=
MYSQL_ROOT_PASSWORD=
REDIS_HOST=
REDIS_PORT=

JWT_SECRET=

AWS_S3_BUCKET=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SECRET_ACCESS_KEY=
AWS_CLOUDFRONT_DOMAIN=

AWS_SQS_ORDER_QUEUE_URL=
AWS_SQS_CONSUMER_ENABLED=

DOCS_AUTH_USERNAME=
DOCS_AUTH_PASSWORD=
34 changes: 26 additions & 8 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ jobs:
needs: publish
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- name: API
launch_template_id: ${{ vars.API_LAUNCH_TEMPLATE_ID }}
asg_name: ${{ vars.API_ASG_NAME }}
consumer_enabled: 'false'
- name: Worker
launch_template_id: ${{ vars.WORKER_LAUNCH_TEMPLATE_ID }}
asg_name: ${{ vars.WORKER_ASG_NAME }}
consumer_enabled: 'true'

name: Deploy ${{ matrix.target.name }}

steps:
# Step 1: AWS 자격증명 - OIDC
Expand All @@ -112,17 +126,18 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}

# Step 2: Launch Template 새 버전 생성 (User Data의 IMAGE_TAG를 새 커밋 SHA로 교체)
- name: Update Launch Template
- name: Update ${{ matrix.target.name }} Launch Template
env:
IMAGE_TAG: ${{ needs.publish.outputs.image_tag }}
AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
AWS_REGION: ${{ vars.AWS_REGION }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
LAUNCH_TEMPLATE_ID: ${{ vars.LAUNCH_TEMPLATE_ID }}
LAUNCH_TEMPLATE_ID: ${{ matrix.target.launch_template_id }}
CONSUMER_ENABLED: ${{ matrix.target.consumer_enabled }}
run: |
ECR_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}"

USER_DATA=$(cat <<'USERDATA' | sed "s|__IMAGE_TAG__|${IMAGE_TAG}|g; s|__AWS_ACCOUNT_ID__|${AWS_ACCOUNT_ID}|g; s|__AWS_REGION__|${AWS_REGION}|g; s|__ECR_REPOSITORY__|${ECR_REPOSITORY}|g" | base64 -w 0
USER_DATA=$(cat <<'USERDATA' | sed "s|__IMAGE_TAG__|${IMAGE_TAG}|g; s|__AWS_ACCOUNT_ID__|${AWS_ACCOUNT_ID}|g; s|__AWS_REGION__|${AWS_REGION}|g; s|__ECR_REPOSITORY__|${ECR_REPOSITORY}|g; s|__CONSUMER_ENABLED__|${CONSUMER_ENABLED}|g" | base64 -w 0
#!/bin/bash
set -e
AWS_REGION="__AWS_REGION__"
Expand All @@ -136,7 +151,10 @@ jobs:
systemctl start docker
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
docker pull ${ECR_URI}:${IMAGE_TAG}
docker run -d -p 8080:8080 --restart unless-stopped --name ci-demo -e SPRING_PROFILES_ACTIVE=prod ${ECR_URI}:${IMAGE_TAG}
docker run -d -p 8080:8080 --restart unless-stopped --name ci-demo \
-e SPRING_PROFILES_ACTIVE=prod \
-e AWS_SQS_CONSUMER_ENABLED=__CONSUMER_ENABLED__ \
${ECR_URI}:${IMAGE_TAG}
USERDATA
)

Expand All @@ -156,10 +174,10 @@ jobs:
echo "✅ 기본 버전 → v${NEW_VERSION} 변경 완료"

# Step 3: Instance Refresh 시작
- name: Start Instance Refresh
- name: Start ${{ matrix.target.name }} Instance Refresh
id: refresh
env:
ASG_NAME: ${{ vars.ASG_NAME }}
ASG_NAME: ${{ matrix.target.asg_name }}
run: |
REFRESH_ID=$(aws autoscaling start-instance-refresh \
--auto-scaling-group-name ${ASG_NAME} \
Expand All @@ -174,9 +192,9 @@ jobs:
echo "✅ Instance Refresh 시작: ${REFRESH_ID}"

# Step 4: Instance Refresh 완료 대기
- name: Wait for Instance Refresh
- name: Wait for ${{ matrix.target.name }} Instance Refresh
env:
ASG_NAME: ${{ vars.ASG_NAME }}
ASG_NAME: ${{ matrix.target.asg_name }}
REFRESH_ID: ${{ steps.refresh.outputs.refresh_id }}
run: |
echo "⏳ Instance Refresh 완료 대기 중..."
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ out/
!.env.example
!.env.example.DS_Store
performance/reports/*.html
performance/results/raw/
performance/results/*.zip
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,23 @@ src/main/java/com/dropit

## API 문서

<!-- 예시: [Postman API Documentation](https://documenter.getpostman.com/view/...) -->
Spring REST Docs를 사용하고, 테스트 스니펫을 OpenAPI 3.0.1 명세로 변환합니다.
Swagger UI는 이 명세 파일을 읽어 화면에 보여주는 역할만 하므로, 별도의 어노테이션 기반 API 명세가 생기지 않습니다.

_링크 추가 예정_
`bootRun`과 `bootJar`는 테스트와 OpenAPI 생성 작업을 먼저 실행한 뒤 생성된 YAML을 애플리케이션 정적 리소스에 포함합니다.
명세와 Swagger UI 리소스를 단독으로 준비하려면 `./gradlew openApiDocs`를 실행합니다.
명세만 생성하려면 `./gradlew openapi3`를 실행하며, 결과는 `build/api-spec/openapi3.yaml`에 저장됩니다. (생성물은 빌드 디렉터리에만 남으며 저장소에는 커밋하지 않습니다.)

브라우저에서 http://localhost:8080/swagger-ui/index.html를 열면 API 문서를 확인할 수 있습니다.
원본 명세는 http://localhost:8080/openapi/openapi3.yaml 경로에서도 직접 확인할 수 있습니다.

Swagger UI와 OpenAPI YAML은 HTTP Basic Auth로 보호됩니다.
로컬에서는 `.env.local`에 다음 두 값을 설정합니다.

```properties
DOCS_AUTH_USERNAME=<local username>
DOCS_AUTH_PASSWORD=<local password>
```

인증이 필요한 API는 다음 헤더를 전달합니다.

Expand Down
142 changes: 142 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ plugins {
id 'java'
id 'org.springframework.boot' version '4.1.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.epages.restdocs-api-spec' version '0.20.1'
}

group = 'com.dropit'
version = '0.0.1-SNAPSHOT'
description = 'dropit-server'

def querydslVersion = '7.5'
def restdocsApiSpecVersion = '0.20.1'

java {
toolchain {
Expand All @@ -28,10 +30,13 @@ dependencies {
implementation "io.github.openfeign.querydsl:querydsl-jpa:${querydslVersion}"
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.webjars:swagger-ui:5.32.11'

implementation platform('io.awspring.cloud:spring-cloud-aws-dependencies:4.1.0')
implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store'
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs'
implementation 'software.amazon.awssdk:url-connection-client'

implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
Expand All @@ -51,13 +56,111 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-actuator-test'
testImplementation 'org.testcontainers:testcontainers-mysql'

testImplementation platform('org.springframework.restdocs:spring-restdocs-bom:4.0.1')
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"

testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
useJUnitPlatform()
outputs.dir(file('build/generated-snippets'))
}

openapi3 {
server = '/'
title = 'DROPIT API'
description = 'DROPIT API 문서입니다.'
version = project.version.toString()
format = 'yaml'
outputDirectory = 'build/api-spec'
}

afterEvaluate {
tasks.named('openapi3') {
dependsOn(tasks.named('test'))
doLast {
File specFile = layout.buildDirectory.file('api-spec/openapi3.yaml').get().asFile
String spec = specFile.getText('UTF-8')

// restdocs-api-spec models a primitive JSON response as an empty object because
// its field-descriptor schema generator only supports object/array roots. Keep
// the endpoint contract accurate for the two Long response endpoints.
[
[path: '/drops', method: 'post', status: '201'],
[path: '/products', method: 'post', status: '201']
].each { endpoint ->
String pattern = /(?s)(\x20{2}${endpoint.path}:.*?\x20{4}${endpoint.method}:.*?responses:\n\x20{8}"${endpoint.status}":.*?application\/json:\n\x20{14}schema:\n)\x20{16}[$]ref: "[^"]+"\n/
spec = spec.replaceFirst(pattern, '$1 type: integer\n format: int64\n')
}

// Primitive response correction leaves the generator's orphaned hash schema behind.
// Remove only those generated hash components; domain schema names come from tests.
spec = spec.replaceAll(
/(?ms)^\x20{4}[A-Za-z0-9]+-\d+:\n.*?(?=^\x20{4}[A-Za-z0-9]+:|^\x20{2}securitySchemes:)/,
''
)

String multipartSchema =
/(?s)(\x20{4}MultipartImage:\n)(?:\x20{6}title: MultipartImage\n)?\x20{6}type: object\n/
spec = spec.replaceFirst(
multipartSchema,
'$1 required:\n - file\n type: object\n properties:\n file:\n type: string\n format: binary\n'
)

specFile.setText(spec, 'UTF-8')
}
}
}

def validateOpenApiSpec = tasks.register('validateOpenApiSpec') {
group = 'documentation'
description = '생성된 OpenAPI 명세가 하나 이상의 API 경로를 포함하는지 검증합니다.'
dependsOn(tasks.named('openapi3'))
doLast {
File specFile = layout.buildDirectory.file('api-spec/openapi3.yaml').get().asFile
if (!specFile.isFile() || specFile.length() == 0) {
throw new GradleException(
"OpenAPI 명세가 생성되지 않았습니다: ${specFile}. ./gradlew openapi3을 실행해 문서화 테스트를 먼저 수행하세요."
)
}

String spec = specFile.getText('UTF-8')
boolean hasPathsSection = spec.readLines().any { it.trim() == 'paths:' }
boolean hasDocumentedPath = spec.readLines().any { it ==~ /^\s{2,}\/\S+:\s*$/ }
if (!hasPathsSection || !hasDocumentedPath) {
throw new GradleException(
"OpenAPI 명세에 문서화된 API 경로가 없습니다: ${specFile}. -x test를 사용하지 말고 ./gradlew openapi3을 실행하세요."
)
}
}
}

def copyOpenApiToResources = tasks.register('copyOpenApiToResources', Copy) {
dependsOn(validateOpenApiSpec)
from(layout.buildDirectory.file('api-spec/openapi3.yaml'))
into(layout.buildDirectory.dir('resources/main/static/openapi'))
}

tasks.named('bootRun') {
dependsOn(copyOpenApiToResources)
}

tasks.named('resolveMainClassName') {
dependsOn(copyOpenApiToResources)
}

tasks.named('bootJar') {
dependsOn(copyOpenApiToResources)
}

tasks.register('openApiDocs') {
group = 'development'
description = 'OpenAPI 명세를 생성하고 검증한 뒤 정적 리소스로 복사합니다.'
dependsOn(copyOpenApiToResources)
}

tasks.register('dbUp', Exec) {
Expand All @@ -71,3 +174,42 @@ tasks.register('dbDown', Exec) {
description = '로컬 DB 컨테이너를 종료합니다.'
commandLine 'docker', 'compose', '--env-file', '.env.local', 'down'
}

def performanceComposeFile = file('performance/compose.performance.yml')
def performanceRawResultDirectory = file('performance/results/raw')

tasks.register('performanceUp', Exec) {
group = 'verification'
description = '성능 관측용 InfluxDB와 Grafana를 실행합니다.'
commandLine 'docker', 'compose', '--file', performanceComposeFile,
'up', '--detach', '--wait', 'influxdb', 'grafana'
}

tasks.register('performanceSmoke', Exec) {
group = 'verification'
description = 'k6가 성능 관측 스택에 지표와 JSON 결과를 기록하는지 확인합니다.'
dependsOn(tasks.named('performanceUp'))

doFirst {
performanceRawResultDirectory.mkdirs()
}

environment 'BASE_URL', 'http://influxdb:8086'
environment 'TEST_RUN', 'performance-stack-smoke'
environment 'K6_SCRIPT', 'performance-stack-smoke.js'
commandLine 'docker', 'compose', '--file', performanceComposeFile,
'--profile', 'load', 'run', '--rm', 'k6'

doLast {
File summary = new File(performanceRawResultDirectory, 'performance-stack-smoke.json')
if (!summary.isFile() || summary.length() == 0) {
throw new GradleException("k6 smoke 결과가 생성되지 않았습니다: ${summary}")
}
}
}

tasks.register('performanceDown', Exec) {
group = 'verification'
description = '성능 관측용 컨테이너와 네트워크를 종료합니다.'
commandLine 'docker', 'compose', '--file', performanceComposeFile, 'down'
}
59 changes: 59 additions & 0 deletions performance/compose.performance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: dropit-performance

services:
influxdb:
image: influxdb:1.8.10
environment:
INFLUXDB_DB: k6
ports:
- "127.0.0.1:8086:8086"
volumes:
- influxdb-data:/var/lib/influxdb
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8086/ping"]
interval: 5s
timeout: 3s
retries: 10

grafana:
image: grafana/grafana:12.4.10
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
GF_USERS_DEFAULT_THEME: light
ports:
- "127.0.0.1:3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
influxdb:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
interval: 5s
timeout: 3s
retries: 20

k6:
image: grafana/k6:1.7.1
profiles: ["load"]
environment:
K6_OUT: influxdb=http://influxdb:8086/k6
BASE_URL: ${BASE_URL:-http://host.docker.internal:8080}
TEST_RUN: ${TEST_RUN:-order-flow-smoke}
AUTH_TOKEN: ${AUTH_TOKEN:-}
volumes:
- ./k6:/scripts:ro
- ./results/raw:/results
depends_on:
influxdb:
condition: service_healthy
entrypoint: ["k6"]
command: ["run", "/scripts/${K6_SCRIPT:-drop-list-index.js}"]
restart: "no"

volumes:
influxdb-data:
grafana-data:
Loading