Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
54c43fb
docs: README.md에 성능 테스트 결과 및 k6 기술 스택 추가
AlbertImKr Dec 22, 2025
cb0454a
docs: TODO.md에 성능 테스트 시스템 완료 항목 업데이트
AlbertImKr Dec 22, 2025
f11892e
feat(ci): AWS ECS에서 OCI VM 기반 배포로 전환
AlbertImKr Dec 29, 2025
1f89c0f
refactor: V1 마이그레이션 스크립트를 PostgreSQL 문법으로 완전히 재작성
AlbertImKr Dec 29, 2025
04bd566
fix: Gradle 테스트 JVM 옵션 제거
AlbertImKr Dec 29, 2025
8678422
fix: 테스트 설정 최적화
AlbertImKr Dec 29, 2025
1b79554
fix: ci 개선
AlbertImKr Dec 29, 2025
57cd63d
fix: ci 개선
AlbertImKr Dec 29, 2025
c6aa317
fix: test위한 수정
AlbertImKr Dec 29, 2025
a47b15c
fix: CI/CD 워크플로우 환경변수 및 조건 수정
AlbertImKr Dec 29, 2025
c6e1f49
fix: Docker 로그인 명령어 변수 확장 문제 해결
AlbertImKr Dec 29, 2025
334afd3
fix: Docker 로그인 및 푸시 명령어 개선
AlbertImKr Dec 29, 2025
1357087
refactor: SSH 배포 스크립트 환경변수 관리 개선
AlbertImKr Dec 29, 2025
318b5a4
refactor: SSH 배포 스크립트 환경변수 관리 개선
AlbertImKr Dec 29, 2025
ac75c1e
fix: Docker 로그인 방식 및 프로덕션 설정 개선
AlbertImKr Dec 29, 2025
f3775f0
fix: 데이터베이스 시크릿 변수명 표준화
AlbertImKr Dec 29, 2025
e7089c3
feat: OCI Object Storage 지원 및 PostgreSQL 마이그레이션 완료
AlbertImKr Dec 31, 2025
624e458
refactor: PostgreSQL 마이그레이션 스크립트 정리
AlbertImKr Dec 31, 2025
cfa661f
feat: OCI Object Storage Presigned URL 기능 구현
AlbertImKr Jan 1, 2026
8a1c4b4
fix: CI/CD 워크플로우 환경변수 추가
AlbertImKr Jan 1, 2026
7d8826e
fix: 배포 헬스체크 대기 시간 증가
AlbertImKr Jan 1, 2026
c950a31
fix: 프로덕션 Actuator 설정 개선
AlbertImKr Jan 1, 2026
4839677
fix: Docker 빌드 조건 추가
AlbertImKr Jan 1, 2026
4bb850e
refactor(domain): Member 도메인 이벤트 처리 개선
AlbertImKr Jan 2, 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
7 changes: 6 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
"Bash(mkdir:*)",
"Bash(cat:*)",
"Bash(k6 run:*)",
"Bash(chmod:*)"
"Bash(chmod:*)",
"Bash(gh run list:*)",
"Bash(./gradlew test:*)",
"Bash(./gradlew clean test:*)",
"Bash(find:*)",
"Bash(xargs ls:*)"
],
"deny": [],
"ask": []
Expand Down
277 changes: 218 additions & 59 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI/CD 파이프라인
name: CI/CD 파이프라인 (OCI Rolling 배포)

on:
push:
Expand All @@ -12,10 +12,6 @@ on:
env:
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2'
JAVA_VERSION: '21'
AWS_REGION: ap-northeast-2
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
ECR_REPOSITORY: rmrt-app
IMAGE_TAG: ${{ github.sha }}
SPRING_MAIL_USERNAME: ${{ secrets.SPRING_MAIL_USERNAME }}
SPRING_MAIL_PASSWORD: ${{ secrets.SPRING_MAIL_PASSWORD }}
SPRING_MAIL_HOST: ${{ secrets.SPRING_MAIL_HOST }}
Expand Down Expand Up @@ -46,7 +42,7 @@ jobs:
test:
name: 단위 및 통합 테스트
runs-on: ubuntu-latest

steps:
- name: 레포지토리를 체크아웃한다
uses: actions/checkout@v6
Expand Down Expand Up @@ -81,11 +77,12 @@ jobs:
path: |
build/reports/
build/test-results/

build:
name: 애플리케이션 빌드
runs-on: ubuntu-latest
needs: test

steps:
- name: 레포지토리를 체크아웃한다
uses: actions/checkout@v6
Expand Down Expand Up @@ -117,7 +114,7 @@ jobs:
runs-on: ubuntu-latest
needs: [ test ]
if: github.event_name == 'push' || github.event_name == 'pull_request'

steps:
- name: 레포지토리를 체크아웃한다
uses: actions/checkout@v6
Expand Down Expand Up @@ -159,70 +156,232 @@ jobs:
- name: SonarQube 분석 실행
run: ./gradlew sonar --build-cache --info

docker-build-and-deploy:
name: Docker 빌드 및 ECS 배포
docker-build-and-push:
name: Docker 빌드 및 OCIR 푸시
runs-on: ubuntu-latest
needs: [ check-src-changes, test, build, sonarqube ]
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.check-src-changes.outputs.has_src_changes == 'true'

steps:
- name: 레포지토리를 체크아웃한다
uses: actions/checkout@v6

- name: AWS 자격증명 설정
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: ECR 로그인
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: OCIR 로그인
env:
OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }}
OCI_REGION: ${{ secrets.OCI_REGION }}
OCI_TENANCY: ${{ secrets.OCI_TENANCY }}
OCI_USERNAME: ${{ secrets.OCI_USERNAME }}
run: |
docker login "$OCI_REGION.ocir.io" \
-u "${OCI_TENANCY}/${OCI_USERNAME}" \
--password-stdin <<< "$OCI_AUTH_TOKEN"

- name: Docker 이미지 빌드
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ env.IMAGE_TAG }}
OCI_REGISTRY: ${{ secrets.OCI_REGION }}.ocir.io/${{ secrets.OCI_TENANCY }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
docker build -t $OCI_REGISTRY/rmrt-app:${{ github.sha }} .
docker tag $OCI_REGISTRY/rmrt-app:${{ github.sha }} \
$OCI_REGISTRY/rmrt-app:latest

- name: Docker 이미지 ECR에 푸시
- name: Docker 이미지 OCIR에 푸시
timeout-minutes: 30
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ env.IMAGE_TAG }}
OCI_REGISTRY: ${{ secrets.OCI_REGION }}.ocir.io/${{ secrets.OCI_TENANCY }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $OCI_REGISTRY/rmrt-app --all-tags

- name: ECS 서비스 업데이트
run: |
# 기존 태스크 정의로 강제 재배포
TASK_DEFINITION_ARN=$(aws ecs describe-task-definition --task-definition rmrt-task --query 'taskDefinition.taskDefinitionArn' --output text)

aws ecs update-service \
--cluster rmrt-cluster-ec2version \
--service rmrt-task-service-o4qq7b02 \
--task-definition $TASK_DEFINITION_ARN \
--force-new-deployment

# 배포 완료 대기 (타임아웃 설정 추가)
echo "⏳ ECS 배포 완료 대기..."
timeout 300 aws ecs wait services-stable \
--cluster rmrt-cluster-ec2version \
--services rmrt-task-service-o4qq7b02 || \
echo "⚠️ 배포 안정화 대기 시간 초과 (실제 배포는 성공했을 수 있습니다)"

echo "✅ ECS 배포 완료!"

- name: 배포 상태 확인
deploy-server-1:
name: Server 1 배포
runs-on: ubuntu-latest
needs: [ docker-build-and-push ]

steps:
- name: Server 1에 배포
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0
env:
OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }}
OCI_REGION: ${{ secrets.OCI_REGION }}
OCI_TENANCY: ${{ secrets.OCI_TENANCY }}
OCI_USERNAME: ${{ secrets.OCI_USERNAME }}
OCI_PRIVATE_KEY: ${{ secrets.OCI_PRIVATE_KEY }}
OCI_USER_ID: ${{ secrets.OCI_USER_ID }}
DB_URL: ${{ secrets.SPRING_DATASOURCE_URL }}
DB_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }}
DB_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
SPRING_MAIL_HOST: ${{ secrets.SPRING_MAIL_HOST }}
SPRING_MAIL_PORT: ${{ secrets.SPRING_MAIL_PORT }}
SPRING_MAIL_USERNAME: ${{ secrets.SPRING_MAIL_USERNAME }}
SPRING_MAIL_PASSWORD: ${{ secrets.SPRING_MAIL_PASSWORD }}
APP_BASE_URL: ${{ secrets.APP_BASE_URL }}
with:
host: ${{ secrets.OCI_VM_IP_1 }}
username: ubuntu
key: ${{ secrets.OCI_SSH_KEY_1 }}
envs: OCI_AUTH_TOKEN,OCI_REGION,OCI_TENANCY,OCI_USERNAME,OCI_PRIVATE_KEY,OCI_USER_ID,DB_URL,DB_USERNAME,DB_PASSWORD,SPRING_MAIL_HOST,SPRING_MAIL_PORT,SPRING_MAIL_USERNAME,SPRING_MAIL_PASSWORD,APP_BASE_URL
script: |
echo "🚀 Server 1 배포 시작..."

# OCIR 로그인
sudo docker login ${OCI_REGION}.ocir.io \
-u "${OCI_TENANCY}/${OCI_USERNAME}" \
--password-stdin <<< "$OCI_AUTH_TOKEN"

# 최신 이미지 풀
sudo docker pull ${OCI_REGION}.ocir.io/${OCI_TENANCY}/rmrt-app:latest

# 기존 컨테이너 중지 및 삭제
sudo docker stop rmrt-app || true
sudo docker rm rmrt-app || true

# 새 컨테이너 실행
sudo docker run -d \
--name rmrt-app \
--restart unless-stopped \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL="${DB_URL}" \
-e SPRING_DATASOURCE_USERNAME="${DB_USERNAME}" \
-e SPRING_DATASOURCE_PASSWORD="${DB_PASSWORD}" \
-e SPRING_MAIL_HOST="${SPRING_MAIL_HOST}" \
-e SPRING_MAIL_PORT="${SPRING_MAIL_PORT}" \
-e SPRING_MAIL_USERNAME="${SPRING_MAIL_USERNAME}" \
-e SPRING_MAIL_PASSWORD="${SPRING_MAIL_PASSWORD}" \
-e OCI_PRIVATE_KEY="${OCI_PRIVATE_KEY}" \
-e OCI_USER_ID="${OCI_USER_ID}" \
-e APP_BASE_URL="${APP_BASE_URL}" \
${OCI_REGION}.ocir.io/${OCI_TENANCY}/rmrt-app:latest

# 오래된 이미지 정리
sudo docker image prune -f

- name: Server 1 헬스체크
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0
with:
host: ${{ secrets.OCI_VM_IP_1 }}
username: ubuntu
key: ${{ secrets.OCI_SSH_KEY_1 }}
script: |
echo "⏳ Server 1 헬스체크 대기..."
for i in {1..100}; do
if curl -s http://localhost:8080/actuator/health | grep -q '"status":"UP"'; then
echo "✅ Server 1 헬스체크 통과!"
exit 0
fi
echo "대기 중... ($i/100)"
sleep 5
done
echo "❌ Server 1 헬스체크 실패"
exit 1

deploy-server-2:
name: Server 2 배포
runs-on: ubuntu-latest
needs: [ deploy-server-1 ]

steps:
- name: Server 2에 배포
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0
env:
OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }}
OCI_REGION: ${{ secrets.OCI_REGION }}
OCI_TENANCY: ${{ secrets.OCI_TENANCY }}
OCI_USERNAME: ${{ secrets.OCI_USERNAME }}
OCI_PRIVATE_KEY: ${{ secrets.OCI_PRIVATE_KEY }}
OCI_USER_ID: ${{ secrets.OCI_USER_ID }}
DB_URL: ${{ secrets.SPRING_DATASOURCE_URL }}
DB_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }}
DB_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
SPRING_MAIL_HOST: ${{ secrets.SPRING_MAIL_HOST }}
SPRING_MAIL_PORT: ${{ secrets.SPRING_MAIL_PORT }}
SPRING_MAIL_USERNAME: ${{ secrets.SPRING_MAIL_USERNAME }}
SPRING_MAIL_PASSWORD: ${{ secrets.SPRING_MAIL_PASSWORD }}
APP_BASE_URL: ${{ secrets.APP_BASE_URL }}
with:
host: ${{ secrets.OCI_VM_IP_2 }}
username: ubuntu
key: ${{ secrets.OCI_SSH_KEY_2 }}
envs: OCI_AUTH_TOKEN,OCI_REGION,OCI_TENANCY,OCI_USERNAME,OCI_PRIVATE_KEY,OCI_USER_ID,DB_URL,DB_USERNAME,DB_PASSWORD,SPRING_MAIL_HOST,SPRING_MAIL_PORT,SPRING_MAIL_USERNAME,SPRING_MAIL_PASSWORD,APP_BASE_URL
script: |
echo "🚀 Server 2 배포 시작..."

# OCIR 로그인
sudo docker login ${OCI_REGION}.ocir.io \
-u "${OCI_TENANCY}/${OCI_USERNAME}" \
--password-stdin <<< "$OCI_AUTH_TOKEN"

# 최신 이미지 풀
sudo docker pull ${OCI_REGION}.ocir.io/${OCI_TENANCY}/rmrt-app:latest

# 기존 컨테이너 중지 및 삭제
sudo docker stop rmrt-app || true
sudo docker rm rmrt-app || true

# 새 컨테이너 실행
sudo docker run -d \
--name rmrt-app \
--restart unless-stopped \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL="${DB_URL}" \
-e SPRING_DATASOURCE_USERNAME="${DB_USERNAME}" \
-e SPRING_DATASOURCE_PASSWORD="${DB_PASSWORD}" \
-e SPRING_MAIL_HOST="${SPRING_MAIL_HOST}" \
-e SPRING_MAIL_PORT="${SPRING_MAIL_PORT}" \
-e SPRING_MAIL_USERNAME="${SPRING_MAIL_USERNAME}" \
-e SPRING_MAIL_PASSWORD="${SPRING_MAIL_PASSWORD}" \
-e OCI_PRIVATE_KEY="${OCI_PRIVATE_KEY}" \
-e OCI_USER_ID="${OCI_USER_ID}" \
-e APP_BASE_URL="${APP_BASE_URL}" \
${OCI_REGION}.ocir.io/${OCI_TENANCY}/rmrt-app:latest

# 오래된 이미지 정리
sudo docker image prune -f

- name: Server 2 헬스체크
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0
with:
host: ${{ secrets.OCI_VM_IP_2 }}
username: ubuntu
key: ${{ secrets.OCI_SSH_KEY_2 }}
script: |
echo "⏳ Server 2 헬스체크 대기..."
for i in {1..100}; do
if curl -s http://localhost:8080/actuator/health | grep -q '"status":"UP"'; then
echo "✅ Server 2 헬스체크 통과!"
exit 0
fi
echo "대기 중... ($i/100)"
sleep 5
done
echo "❌ Server 2 헬스체크 실패"
exit 1

verify-deployment:
name: 배포 검증
runs-on: ubuntu-latest
needs: [ deploy-server-2 ]

steps:
- name: Load Balancer 헬스체크
env:
APP_BASE_URL: ${{ secrets.APP_BASE_URL }}
OCI_LB_IP: ${{ secrets.OCI_LB_IP }}
run: |
# 서비스 상태 확인
aws ecs describe-services \
--cluster rmrt-cluster-ec2version \
--services rmrt-task-service-o4qq7b02 \
--query 'services[0].{status: status, runningCount: runningCount, desiredCount: desiredCount}'

echo "🌐 실제 도메인: https://rmrt.albert-im.com"
echo "🔍 도메인 헬스 체크: https://rmrt.albert-im.com/actuator/health"
echo "🔍 Load Balancer 통해 서비스 확인..."
for i in {1..6}; do
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" ${APP_BASE_URL}/actuator/health || echo "000")
if [ "$RESPONSE" = "200" ]; then
echo "✅ 서비스 정상 동작!"
echo ""
echo "🌐 서비스 URL: ${OCI_LB_IP}"
echo "🔍 헬스체크: ${OCI_LB_IP}/actuator/health"
exit 0
fi
echo "대기 중... ($i/6) - HTTP $RESPONSE"
sleep 10
done
echo "⚠️ Load Balancer 응답 확인 필요"
exit 0
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
- `JUnit5` `MockK` (단위 테스트)
- `Testcontainers` (통합 테스트, 실제 DB 환경)
- `LocalStack` (AWS S3 로컬 테스트)
- `k6` (성능 테스트 및 부하 테스트)

**Architecture**

Expand Down Expand Up @@ -122,6 +123,19 @@
| **DB 마이그레이션** | Flyway 기반 자동 스키마 버전 관리 |
| **문서화** | 2,800줄+ 기술 문서 |

### 🚀 성능 지표 (k6 부하 테스트 결과)

| 테스트 유형 | TPS | p95 응답시간 | 에러율 | 달성률 |
|----------------------|-------|----------|-------|------------------|
| **READ** | 29.0 | 27.0ms | 0.00% | 290% (목표 10 TPS) |
| **WRITE** | 42.3 | 90.3ms | 0.00% | 846% (목표 5 TPS) |
| **STRESS** (300 VUs) | 201.3 | 1099.5ms | 0.00% | 안정성 검증 완료 |

- **READ 처리량**: 목표 대비 **290%** 초과 달성
- **WRITE 처리량**: 목표 대비 **846%** 초과 달성
- **스트레스 테스트**: 300명 동시 접속 17분간 **에러 없이** 안정적 처리
- **총 처리량**: 252,176건 요청 처리 (34분간)

### ☁️ 클라우드 아키텍처

```
Expand Down Expand Up @@ -175,6 +189,7 @@
- [📖 API 문서](docs/API_DOCUMENTATION.md)
- [📷 이미지 관리 시스템](docs/IMAGE_MANAGEMENT.md)
- [🧪 테스트 가이드](docs/TESTING_GUIDE.md)
- [📊 성능 테스트 가이드](performance-tests/README.md)
- [✅ TODO 리스트](docs/TODO.md)

## 🚀 빠른 시작
Expand Down
Loading