From 86fa5f7a7b5dc4e8b84f23834aba38165c1b05e1 Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 18:47:25 +0900 Subject: [PATCH 01/17] refactor: docker-compose-prod.yml --- backend/docker-compose-prod.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/docker-compose-prod.yml b/backend/docker-compose-prod.yml index 127e43c..dcf939e 100644 --- a/backend/docker-compose-prod.yml +++ b/backend/docker-compose-prod.yml @@ -1,4 +1,22 @@ services: + # Spring Boot 애플리케이션 서비스 추가 + app-prod: + image: ghcr.io/wonseokyoon/catch-course:latest + container_name: app-prod + restart: always + depends_on: + - mysql-db-prod + - redis-prod + - kafka-prod + ports: + - "8080:8080" + networks: + - prod-network + environment: + - SPRING_PROFILES_ACTIVE=prod + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + mysql-db-prod: image: mysql:8.0 container_name: mysql-db-prod @@ -71,4 +89,4 @@ networks: driver: bridge volumes: - mysql_prod_data: + mysql_prod_data: \ No newline at end of file From 1e7d6490e9b5b3e5fc02346eccaabf14f36e081f Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 18:49:10 +0900 Subject: [PATCH 02/17] =?UTF-8?q?temp:=20=EC=9E=84=EC=8B=9C=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d74aff4..c5f11dc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -77,7 +77,7 @@ jobs: # 2. 릴리스 : main/develop 브랜치로 Push될 때만 실행 makeTagAndRelease: name: Create Tag and Release - if: github.event_name == 'push' + # if: github.event_name == 'push' needs: backend-ci runs-on: ubuntu-latest permissions: @@ -107,7 +107,7 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: name: 도커 이미지 빌드와 푸시 - if: github.event_name == 'push' + # if: github.event_name == 'push' needs: makeTagAndRelease runs-on: ubuntu-latest steps: From 65b44e25c08cb7dd29f7c101ed81f9bfe8527808 Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 19:24:18 +0900 Subject: [PATCH 03/17] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=EC=BA=90=EC=8B=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c5f11dc..2e265dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,14 +29,14 @@ jobs: java-version: '21' distribution: 'temurin' - # Gradle 캐싱을 추가하여 빌드 속도를 개선합니다. + # Gradle 캐싱 제거 -> 처음부터 빌드 - name: Cache Gradle packages uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-v2 restore-keys: | ${{ runner.os }}-gradle- From 75fd35cf8d84701b51bac1b95c42710af9126e6d Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 19:44:54 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84:=20Dockerfile=20->=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Dockerfile | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index ff298d7..c612d77 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,18 +1,18 @@ -# --- Stage 1: 애플리케이션 빌드 단계 --- -# 소스 코드 컴파일 -FROM openjdk:21-jdk-slim AS builder -# 컨테이너 내의 작업 디렉터리를 /app으로 설정 -WORKDIR /app -# 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 -COPY . . -# Gradle Wrapper 스크립트에 실행 권한을 부여 -RUN chmod +x ./gradlew -# Gradle을 사용하여 애플리케이션을 빌드 -# - clean: 이전 빌드 결과물(build 폴더)을 모두 삭제하여 깨끗한 상태에서 빌드를 시작 -# - build: 소스 코드를 컴파일하고 실행 가능한 JAR 파일을 생성 -# - --no-daemon: CI/CD 환경에 적합하도록 Gradle 데몬을 사용하지 않고 일회성으로 빌드를 실행 -# - -x test: CI 단계에서 이미 테스트를 수행했으므로, Docker 이미지를 만드는 과정에서는 테스트를 건너뜀 -RUN ./gradlew clean build --no-daemon -x test +## --- Stage 1: 애플리케이션 빌드 단계 --- +## 소스 코드 컴파일 +#FROM openjdk:21-jdk-slim AS builder +## 컨테이너 내의 작업 디렉터리를 /app으로 설정 +#WORKDIR /app +## 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 +#COPY . . +## Gradle Wrapper 스크립트에 실행 권한을 부여 +#RUN chmod +x ./gradlew +## Gradle을 사용하여 애플리케이션을 빌드 +## - clean: 이전 빌드 결과물(build 폴더)을 모두 삭제하여 깨끗한 상태에서 빌드를 시작 +## - build: 소스 코드를 컴파일하고 실행 가능한 JAR 파일을 생성 +## - --no-daemon: CI/CD 환경에 적합하도록 Gradle 데몬을 사용하지 않고 일회성으로 빌드를 실행 +## - -x test: CI 단계에서 이미 테스트를 수행했으므로, Docker 이미지를 만드는 과정에서는 테스트를 건너뜀 +#RUN ./gradlew clean build --no-daemon -x test # --- Stage 2: 이미지 생성 --- @@ -21,7 +21,7 @@ FROM eclipse-temurin:21-jre # 컨테이너 내의 작업 디렉터리 WORKDIR /app # 현재 이미지의 /app 디렉터리로 복사하고, 이름을 app.jar로 변경 -COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar +COPY build/libs/*-SNAPSHOT.jar app.jar # 8080 포트를 외부에 노출 EXPOSE 8080 # "java -jar app.jar" 명령어로 Spring Boot 애플리케이션을 실행 From 2442385e8d55b4d98be2602a5c6a26eb66252a9d Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 19:46:09 +0900 Subject: [PATCH 05/17] fix:CI --- .github/workflows/deploy.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2e265dd..d1d2483 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -112,6 +112,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + working-directory: ./backend + run: chmod +x ./gradlew + + - name: Build JAR with Gradle + working-directory: ./backend + run: ./gradlew clean build -x test - name: 빌드 컨텍스트 확인 (backend 폴더 내용) run: | From be754b9dc92a5d2c14ef8d66b73ee25dd4a25e87 Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 19:47:29 +0900 Subject: [PATCH 06/17] fix:CI --- .github/workflows/deploy.yml | 51 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d1d2483..f3b1e39 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -106,61 +106,52 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: - name: 도커 이미지 빌드와 푸시 - # if: github.event_name == 'push' + name: 도커 이미지 빌드와 푸시 (디버깅 모드) needs: makeTagAndRelease runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 21 + - name: 1. 소스 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 2. JDK 21 설치 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - - name: Grant execute permission for gradlew + - name: 3. 체크아웃된 파일 목록 확인 (prod.yml 존재 여부) + run: | + echo "--- 전체 파일 목록 ---" + ls -laR ./backend + echo "--- prod.yml 파일 검색 ---" + find ./backend -name "application-prod.yml" + + - name: 4. Gradle 실행 권한 부여 working-directory: ./backend run: chmod +x ./gradlew - - name: Build JAR with Gradle + - name: 5. CI 환경에서 직접 Gradle 빌드 실행 working-directory: ./backend run: ./gradlew clean build -x test - - name: 빌드 컨텍스트 확인 (backend 폴더 내용) + - name: 6. 생성된 JAR 파일 내용물 확인 (가장 중요) run: | - echo "--- Docker 빌드에 사용될 backend 폴더의 전체 파일 목록입니다 ---" - ls -laR ./backend + echo "--- 생성된 JAR 파일 내부 목록 확인 ---" + jar -tf ./backend/build/libs/*.jar | grep prod - - name: application-secret.yml 생성 + - name: 7. application-secret.yml 생성 (Docker 빌드용) env: APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} run: | mkdir -p ./backend/src/main/resources echo "$APPLICATION_SECRET" > ./backend/src/main/resources/application-secret.yml - - name: Docker Buildx 설치 - uses: docker/setup-buildx-action@v2 - - name: 레지스트리 로그인 - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: set lower case owner name - id: set_owner - run: | - echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV} - env: - OWNER: '${{ github.repository_owner }}' - - - name: 빌드 앤 푸시 + - name: 8. Docker 빌드 및 푸시 uses: docker/build-push-action@v3 with: context: ./backend push: true - # 이 옵션을 추가하여 Docker 빌드 캐시를 사용하지 않도록 강제합니다. no-cache: true tags: | - ghcr.io/${{ env.OWNER_LC }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }} - ghcr.io/${{ env.OWNER_LC }}/catch-course:latest + ghcr.io/${{ github.repository_owner }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }} + ghcr.io/${{ github.repository_owner }}/catch-course:latest \ No newline at end of file From ed870efaef343d7e6ff35a4b02797ec177ccf459 Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 20:08:49 +0900 Subject: [PATCH 07/17] fix:CI --- .github/workflows/deploy.yml | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f3b1e39..01dfcd6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -106,7 +106,7 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: - name: 도커 이미지 빌드와 푸시 (디버깅 모드) + name: 도커 이미지 빌드와 푸시 needs: makeTagAndRelease runs-on: ubuntu-latest steps: @@ -119,34 +119,20 @@ jobs: java-version: '21' distribution: 'temurin' - - name: 3. 체크아웃된 파일 목록 확인 (prod.yml 존재 여부) - run: | - echo "--- 전체 파일 목록 ---" - ls -laR ./backend - echo "--- prod.yml 파일 검색 ---" - find ./backend -name "application-prod.yml" - - - name: 4. Gradle 실행 권한 부여 + - name: 3. Gradle 실행 권한 부여 working-directory: ./backend run: chmod +x ./gradlew + + - name: 4. application-secret.yml 생성 (빌드 전) + env: + APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} + run: echo "$APPLICATION_SECRET" > ./backend/src/main/resources/application-secret.yml - - name: 5. CI 환경에서 직접 Gradle 빌드 실행 + - name: 5. CI 환경에서 Gradle 빌드 실행 working-directory: ./backend run: ./gradlew clean build -x test - - name: 6. 생성된 JAR 파일 내용물 확인 (가장 중요) - run: | - echo "--- 생성된 JAR 파일 내부 목록 확인 ---" - jar -tf ./backend/build/libs/*.jar | grep prod - - - name: 7. application-secret.yml 생성 (Docker 빌드용) - env: - APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} - run: | - mkdir -p ./backend/src/main/resources - echo "$APPLICATION_SECRET" > ./backend/src/main/resources/application-secret.yml - - - name: 8. Docker 빌드 및 푸시 + - name: 6. Docker 빌드 및 푸시 uses: docker/build-push-action@v3 with: context: ./backend From f26b51bcdd8df68ffecb48c2dfb0b3d9ca3536a7 Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 20:20:25 +0900 Subject: [PATCH 08/17] fix: Dockerfile --- backend/Dockerfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index c612d77..fe551b0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,17 +2,17 @@ ## 소스 코드 컴파일 #FROM openjdk:21-jdk-slim AS builder ## 컨테이너 내의 작업 디렉터리를 /app으로 설정 -#WORKDIR /app -## 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 -#COPY . . -## Gradle Wrapper 스크립트에 실행 권한을 부여 -#RUN chmod +x ./gradlew -## Gradle을 사용하여 애플리케이션을 빌드 -## - clean: 이전 빌드 결과물(build 폴더)을 모두 삭제하여 깨끗한 상태에서 빌드를 시작 -## - build: 소스 코드를 컴파일하고 실행 가능한 JAR 파일을 생성 -## - --no-daemon: CI/CD 환경에 적합하도록 Gradle 데몬을 사용하지 않고 일회성으로 빌드를 실행 -## - -x test: CI 단계에서 이미 테스트를 수행했으므로, Docker 이미지를 만드는 과정에서는 테스트를 건너뜀 -#RUN ./gradlew clean build --no-daemon -x test +WORKDIR /app +# 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 +COPY . . +# Gradle Wrapper 스크립트에 실행 권한을 부여 +RUN chmod +x ./gradlew +# Gradle을 사용하여 애플리케이션을 빌드 +# - clean: 이전 빌드 결과물(build 폴더)을 모두 삭제하여 깨끗한 상태에서 빌드를 시작 +# - build: 소스 코드를 컴파일하고 실행 가능한 JAR 파일을 생성 +# - --no-daemon: CI/CD 환경에 적합하도록 Gradle 데몬을 사용하지 않고 일회성으로 빌드를 실행 +# - -x test: CI 단계에서 이미 테스트를 수행했으므로, Docker 이미지를 만드는 과정에서는 테스트를 건너뜀 +RUN ./gradlew clean build --no-daemon -x test # --- Stage 2: 이미지 생성 --- From d059a8f77e840fcf0585935dcb471c972e5d787d Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 20:26:32 +0900 Subject: [PATCH 09/17] fix: Dockerfile --- backend/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/Dockerfile b/backend/Dockerfile index fe551b0..5c2e9a9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -18,6 +18,8 @@ RUN ./gradlew clean build --no-daemon -x test # --- Stage 2: 이미지 생성 --- # JRE만 포함된 가벼운 이미지를 사용하여, 최소한의 파일만으로 이미지를 만듬 FROM eclipse-temurin:21-jre +# 소스 코드 컴파일 +FROM openjdk:21-jdk-slim AS builder # 컨테이너 내의 작업 디렉터리 WORKDIR /app # 현재 이미지의 /app 디렉터리로 복사하고, 이름을 app.jar로 변경 From 28e9f664e6bb75e7ed63526df46d6705ce1a575f Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 20:30:28 +0900 Subject: [PATCH 10/17] fix: Dockerfile --- backend/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 5c2e9a9..3e010f8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,6 @@ ## --- Stage 1: 애플리케이션 빌드 단계 --- ## 소스 코드 컴파일 -#FROM openjdk:21-jdk-slim AS builder +FROM openjdk:21-jdk-slim AS builder ## 컨테이너 내의 작업 디렉터리를 /app으로 설정 WORKDIR /app # 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 @@ -18,8 +18,6 @@ RUN ./gradlew clean build --no-daemon -x test # --- Stage 2: 이미지 생성 --- # JRE만 포함된 가벼운 이미지를 사용하여, 최소한의 파일만으로 이미지를 만듬 FROM eclipse-temurin:21-jre -# 소스 코드 컴파일 -FROM openjdk:21-jdk-slim AS builder # 컨테이너 내의 작업 디렉터리 WORKDIR /app # 현재 이미지의 /app 디렉터리로 복사하고, 이름을 app.jar로 변경 From 48edc6f123ae7c9b62b427c413cd202b1ec2827a Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 20:46:36 +0900 Subject: [PATCH 11/17] fix: CI --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 01dfcd6..ad5f6d6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,6 +109,10 @@ jobs: name: 도커 이미지 빌드와 푸시 needs: makeTagAndRelease runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: - name: 1. 소스 코드 체크아웃 uses: actions/checkout@v4 From d1882ae247b26f212eb6f6a98e388f0dcebf8be3 Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 20:58:16 +0900 Subject: [PATCH 12/17] fix: CI --- .github/workflows/deploy.yml | 53 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ad5f6d6..0ae923a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -107,41 +107,46 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: name: 도커 이미지 빌드와 푸시 + if: github.event_name == 'push' needs: makeTagAndRelease runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: 1. 소스 코드 체크아웃 - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: 2. JDK 21 설치 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' + - name: 빌드 컨텍스트 확인 (backend 폴더 내용) + run: | + echo "--- Docker 빌드에 사용될 backend 폴더의 전체 파일 목록입니다 ---" + ls -laR ./backend - - name: 3. Gradle 실행 권한 부여 - working-directory: ./backend - run: chmod +x ./gradlew - - - name: 4. application-secret.yml 생성 (빌드 전) + - name: application-secret.yml 생성 env: APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} - run: echo "$APPLICATION_SECRET" > ./backend/src/main/resources/application-secret.yml - - - name: 5. CI 환경에서 Gradle 빌드 실행 - working-directory: ./backend - run: ./gradlew clean build -x test + run: | + mkdir -p ./backend/src/main/resources + echo "$APPLICATION_SECRET" > ./backend/src/main/resources/application-secret.yml + - name: Docker Buildx 설치 + uses: docker/setup-buildx-action@v2 + - name: 레지스트리 로그인 + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: 6. Docker 빌드 및 푸시 + - name: set lower case owner name + id: set_owner + run: | + echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV} + env: + OWNER: '${{ github.repository_owner }}' + + - name: 빌드 앤 푸시 uses: docker/build-push-action@v3 with: context: ./backend push: true + # 이 옵션을 추가하여 Docker 빌드 캐시를 사용하지 않도록 강제합니다. no-cache: true tags: | - ghcr.io/${{ github.repository_owner }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }} - ghcr.io/${{ github.repository_owner }}/catch-course:latest \ No newline at end of file + ghcr.io/${{ env.OWNER_LC }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }} + ghcr.io/${{ env.OWNER_LC }}/catch-course:latest \ No newline at end of file From db935b9f515cda9dc6cd51e00dc574dc80a2450e Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 21:03:10 +0900 Subject: [PATCH 13/17] fix: CI --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ae923a..d3e338a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,14 +29,14 @@ jobs: java-version: '21' distribution: 'temurin' - # Gradle 캐싱 제거 -> 처음부터 빌드 + # Gradle 캐싱을 추가하여 빌드 속도를 개선합니다. - name: Cache Gradle packages uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-v2 + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- @@ -107,7 +107,7 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: name: 도커 이미지 빌드와 푸시 - if: github.event_name == 'push' + # if: github.event_name == 'push' needs: makeTagAndRelease runs-on: ubuntu-latest steps: From f2fe026621b52054886935cfe1283b31a75a3800 Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Mon, 18 Aug 2025 21:09:18 +0900 Subject: [PATCH 14/17] fix: Dockerfile --- backend/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 3e010f8..a74b2a6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,7 @@ -## --- Stage 1: 애플리케이션 빌드 단계 --- -## 소스 코드 컴파일 +# --- Stage 1: 애플리케이션 빌드 단계 --- +# 소스 코드 컴파일 FROM openjdk:21-jdk-slim AS builder -## 컨테이너 내의 작업 디렉터리를 /app으로 설정 +# 컨테이너 내의 작업 디렉터리를 /app으로 설정 WORKDIR /app # 모든 소스코드를 컨테이너의 /app 디렉터리로 복사 COPY . . @@ -21,8 +21,8 @@ FROM eclipse-temurin:21-jre # 컨테이너 내의 작업 디렉터리 WORKDIR /app # 현재 이미지의 /app 디렉터리로 복사하고, 이름을 app.jar로 변경 -COPY build/libs/*-SNAPSHOT.jar app.jar +COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar # 8080 포트를 외부에 노출 EXPOSE 8080 # "java -jar app.jar" 명령어로 Spring Boot 애플리케이션을 실행 -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file From f0d1482aa92190758a0045e0792fc061417bc6ae Mon Sep 17 00:00:00 2001 From: nokkae Date: Mon, 18 Aug 2025 22:04:27 +0900 Subject: [PATCH 15/17] Update deploy.yml --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d3e338a..d74aff4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -77,7 +77,7 @@ jobs: # 2. 릴리스 : main/develop 브랜치로 Push될 때만 실행 makeTagAndRelease: name: Create Tag and Release - # if: github.event_name == 'push' + if: github.event_name == 'push' needs: backend-ci runs-on: ubuntu-latest permissions: @@ -107,7 +107,7 @@ jobs: # 3. 빌드 및 배포: main/develop 브랜치로 Push될 때만 실행 buildImageAndPush: name: 도커 이미지 빌드와 푸시 - # if: github.event_name == 'push' + if: github.event_name == 'push' needs: makeTagAndRelease runs-on: ubuntu-latest steps: @@ -149,4 +149,4 @@ jobs: no-cache: true tags: | ghcr.io/${{ env.OWNER_LC }}/catch-course:${{ needs.makeTagAndRelease.outputs.tag_name }} - ghcr.io/${{ env.OWNER_LC }}/catch-course:latest \ No newline at end of file + ghcr.io/${{ env.OWNER_LC }}/catch-course:latest From 9bb6fd146415b512c8e4e10b279910cfad532583 Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Tue, 14 Oct 2025 18:02:40 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix=EB=B2=84=EC=A0=84=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.DS_Store | Bin 0 -> 6148 bytes backend/.gitignore | 2 +- backend/build.gradle | 1 + backend/src/.DS_Store | Bin 0 -> 6148 bytes backend/src/main/.DS_Store | Bin 0 -> 6148 bytes backend/src/main/java/.DS_Store | Bin 0 -> 6148 bytes backend/src/main/java/com/.DS_Store | Bin 0 -> 6148 bytes .../src/main/java/com/Catch_Course/.DS_Store | Bin 0 -> 6148 bytes backend/src/main/resources/application.yml | 2 +- .../controller/ReservationControllerTest.java | 20 ++++++++++++++++-- 10 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 backend/.DS_Store create mode 100644 backend/src/.DS_Store create mode 100644 backend/src/main/.DS_Store create mode 100644 backend/src/main/java/.DS_Store create mode 100644 backend/src/main/java/com/.DS_Store create mode 100644 backend/src/main/java/com/Catch_Course/.DS_Store diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ab36f92165ca9685c440f8abc624b8e5c3847a74 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O({YS3VK`cS}?XnC|*LWFJMFuDm5`hgE3o@)*MP9cYPsW#OHBl zcLNr47O^w1`_1oe_JiyXV~l%?=$J8^F=jzSX*G6d@*oSE2P z2mJOX%UQ-A!s7e)CviS^oKN0pwzjt0R@>@Ycm9(s{CO~6|Q;Ud7X5 zV(*^GbRNX%G*bm}G=-Gg>o|>M;mJiB<*L@#0jpzmCicN{*&p_uo)|czRZlEOs1FAx zt5wI^J3KnQm^>#hseIFXa-dtuj=>7vK{3mD^|LgQ=_7cm>?(_p7$63S0b*dI88F9! z)!k?oXz|1VF;K?=tMu>oLT?(j6xp`u6 zT@HR>@?3+ZMqSRhni`fWCGK00Z}teHGMkfjZ>521|`N3i?$! PAYB9$A=D8Azres3Yo$rH literal 0 HcmV?d00001 diff --git a/backend/.gitignore b/backend/.gitignore index c1eeb98..91c51e5 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -43,4 +43,4 @@ db_dev.trace.db ### yml /src/main/resources/application-secret.yml apiV1.json -.env +.env \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index 5ee546c..0992cb2 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -80,6 +80,7 @@ dependencies { // 모니터링 implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'io.prometheus:prometheus-metrics-model:1.0.0' } tasks.named('test') { diff --git a/backend/src/.DS_Store b/backend/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d6da1a1f08ec6ee7f91a8e24cf2e24443dac5c70 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O0O({YS3VI88Eg0J(6fYsx7cim+m70*E!F0DYsXdfJ?)pN$h|lB9 z?nW%utB9R}-EV$(vma!C7-Kw`CC7|8j4>M;B1dJ7pnGkoW|9#(juFrENu0?T>^Bqp z>ww>Gu`!ES%EZ_2k0)807oPXdYjtaDyJ0u%mVFmK%6V8s#Vik^=?z+!Qf6_bNAXoU znh%}bGbxHF6{D#tNRtty++L?5k@G;#M3Sr8PzUU$-5fdxi$$l`@!D?J>o42xqK|s7 zd$L?M?Y+aJ(~IF#HWuEQW)4!_LWNB(LF zS;PP_@Xr|F)*u`Nuqbo3ep?=%wJx-KXegLhq5=Z?$|V2{+(-6RP{&==AS5Z<-XrW7Fu1-&hJEg0J(6fdFH7cim+m70*E(U>hw+8jzDcYPsW#OHBl zcOw?-Rm9G~?l-@?*$=Wmj4|GyMn{Y}j4>M;B1dI~pnGL#!z3ee93w2UahS*u>^Bqp z>ww>Gu}2oL84JFCe>hI!EO*^^zEQI_x9WD?ZrZp0gDm_!$fsF9m|UZEA!QO)dJta5 zqhjdnoXRv0;&e1o1#vWjl$)zKjbzc6(=^Idt*--i!)^?n{rSAzZM!YeaeIrFnDhZ$W?BEwFopDbi^~3-%u*^VBn+~4;XYk9ceB>{e zkVOm-1OJQxZVmiF9~NcK)^E$hvsOU6gNA~6B`P4GFI@t_zS5Z<-brW7Fu1-&hJEg0J(6fYsx7cim+m718M!8BW%)Er77cYPsW#OHBl zcLNr47O^w1`_1oe_JiyXV~l%?=!7wwF=jzShXm0Pctd`ZW?)_(3_<4{oGB23lqID%@5>|Q;UdNMS zZ10`RG!No*GFJt0G=Y@6n>dYR;mJiBWvbTK0jq7b$M)fJ*&FnnuIM|%RaY#Bs1N$5 zt5w_DKRQ0U9KR&fRK96GInb?S*I)(jpqS;n`tvlA=@WRW>?(_p7$63S0b*dI88F9! z)!AqkXz|1VF;K?=tMu>oLT?(j6xp`u6 zT@HR>@?3+ZMqSRhni`fWCGK00Z}t0~OS9fjZ>521|`N3i?$! PAYB9$A=D8Azres3)pSXu literal 0 HcmV?d00001 diff --git a/backend/src/main/java/com/.DS_Store b/backend/src/main/java/com/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..14f70eacaa9401b68e911a8948d79e1d52e264ac GIT binary patch literal 6148 zcmeHK%}N6?5T3NvZmB{K3VK`cTClc7C|;JfzJM!wP^r7N*u`~I_QxJdVek4vzKGA` zOp=PF_ToXL%)sQEOlCIZ%aTa|Ky%}+JyOc8Y7q;hL1*7Spw%3zU>IKm#Rtdpygdw-rK{S-puFRrgqH88AnlRo&n<^3s?AX5e=W(D`7a5;_KRjq2#YhOUn^ULqtx zo8A(Hwn4{Wt`SF2gib}&slq%lgic4lZQ>k*xkjB1Ld}fdF*6JELJ?|q^xG;NgrkvL zW`G%3W}v9MHLCyT>)-#&N!()wn1PLAK$QA!zl%#UwRL52RBLV2dsGsN%Qb$bprJ}J f#!@LhKvjZ%n+!z9V6G88D0~r6G;qTV{3!#UgPczE literal 0 HcmV?d00001 diff --git a/backend/src/main/java/com/Catch_Course/.DS_Store b/backend/src/main/java/com/Catch_Course/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b62a53cef40c3eacef800ee023b7ee1c5648da88 GIT binary patch literal 6148 zcmeHK%}T>S5T0#on^J@x6!f;>wP0JSP`rd%U%-eSRBA#>4W`-BruI+@x$6u0B0i5Z zyIbjxUPWaFX20FpnS}YW>|_8ybf#egpb7vEDq*gS!xuvPq+61+mI9*C&xjxiW|L76 zt62%WZ$G5b) zQYsFXb3eF_`m<4v4}&y4>AFNvLKWHBG{aj`hrjQ+CR|>fwCeY&F_7 z*=(OIYI5Fg)@!nL+FC5i&i>Ky*=6r39wzF=Fe&iRY1y^7gjbxbCH3r$lUOAKv{OtG z1`uP)0j3gnVfEoB#42VM4>vk|DTU)o1qgpFbA5cli oFEjWVf)ibe(U(f`0jd_XOPV0M7BhorLE#?(O#=_iz>hNU0ogr@4*&oF literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d28e72f..a864a46 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -71,4 +71,4 @@ spring: app: registration-time: start: "09:00" - duration-in-minutes: 900 + duration-in-minutes: 1080 diff --git a/backend/src/test/java/com/Catch_Course/domain/reservation/controller/ReservationControllerTest.java b/backend/src/test/java/com/Catch_Course/domain/reservation/controller/ReservationControllerTest.java index 3b99b75..db7f711 100644 --- a/backend/src/test/java/com/Catch_Course/domain/reservation/controller/ReservationControllerTest.java +++ b/backend/src/test/java/com/Catch_Course/domain/reservation/controller/ReservationControllerTest.java @@ -35,7 +35,9 @@ import java.util.List; import java.util.function.Supplier; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.*; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -229,14 +231,28 @@ void cancelReservation() throws Exception { @DisplayName("수강 취소 실패 - 이미 취소") void cancelReservation2() throws Exception { Long courseId = 1L; - cancelReservation(); // 수강 취소 - Thread.sleep(1000); + cancelReservation(); + // then 1: DB에서 방금 생성된 예약을 직접 조회하여 ID를 확보 + Course course = courseService.findById(courseId); + Reservation reservation = reservationRepository.findByStudentAndCourse(loginedMember, course) + .orElseThrow(() -> new AssertionError("테스트 예약을 찾을 수 없습니다.")); + Long reservationId = reservation.getId(); + + // then 2: Awaitility를 사용해 비동기 처리가 완료될 때까지 대기 + await().atMost(5, SECONDS).untilAsserted(() -> { + Reservation updatedReservation = reservationRepository.findById(reservationId) + .orElseThrow(() -> new AssertionError("예약을 찾을 수 없습니다.")); + assertThat(updatedReservation.getStatus()).isEqualTo(ReservationStatus.CANCELLED); + }); + + // when 2: 두 번째 수강 취소 요청 ResultActions resultActions = mvc.perform( delete("/api/reserve?courseId=%d".formatted(courseId)) .header("Authorization", "Bearer " + token) ).andDo(print()); + // then 3: 최종 결과 검증 resultActions .andExpect(status().isConflict()) .andExpect(jsonPath("$.code").value("409-5")) From 4f85e0bc75cd37a8d767db9f23befd3cd74c6c7d Mon Sep 17 00:00:00 2001 From: wonseokyoon Date: Tue, 14 Oct 2025 18:03:54 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=EB=B2=84=EC=A0=84=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/payments/entity/PaymentOutbox.java | 42 +++++++++++++++ .../repository/PaymentOutboxRepository.java | 10 ++++ .../payments/service/OutboxScheduler.java | 52 +++++++++++++++++++ .../payments/service/PaymentService.java | 26 +++++++--- 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/com/Catch_Course/domain/payments/entity/PaymentOutbox.java create mode 100644 backend/src/main/java/com/Catch_Course/domain/payments/repository/PaymentOutboxRepository.java create mode 100644 backend/src/main/java/com/Catch_Course/domain/payments/service/OutboxScheduler.java diff --git a/backend/src/main/java/com/Catch_Course/domain/payments/entity/PaymentOutbox.java b/backend/src/main/java/com/Catch_Course/domain/payments/entity/PaymentOutbox.java new file mode 100644 index 0000000..93d66a1 --- /dev/null +++ b/backend/src/main/java/com/Catch_Course/domain/payments/entity/PaymentOutbox.java @@ -0,0 +1,42 @@ +package com.Catch_Course.domain.payments.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class PaymentOutbox { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long aggregateId; + + @Column(nullable = false) + private String aggregateType; + + @Column(columnDefinition = "TEXT", nullable = false) + private String payload; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @Builder + public PaymentOutbox(Long aggregateId, String aggregateType, String payload) { + this.aggregateId = aggregateId; + this.aggregateType = aggregateType; + this.payload = payload; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/Catch_Course/domain/payments/repository/PaymentOutboxRepository.java b/backend/src/main/java/com/Catch_Course/domain/payments/repository/PaymentOutboxRepository.java new file mode 100644 index 0000000..f619c27 --- /dev/null +++ b/backend/src/main/java/com/Catch_Course/domain/payments/repository/PaymentOutboxRepository.java @@ -0,0 +1,10 @@ +package com.Catch_Course.domain.payments.repository; + +import com.Catch_Course.domain.payments.entity.PaymentOutbox; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PaymentOutboxRepository extends JpaRepository { + List findTop100ByOrderByCreatedAtAsc(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/Catch_Course/domain/payments/service/OutboxScheduler.java b/backend/src/main/java/com/Catch_Course/domain/payments/service/OutboxScheduler.java new file mode 100644 index 0000000..025c5f6 --- /dev/null +++ b/backend/src/main/java/com/Catch_Course/domain/payments/service/OutboxScheduler.java @@ -0,0 +1,52 @@ +package com.Catch_Course.domain.payments.service; + +import com.Catch_Course.domain.payments.entity.PaymentOutbox; +import com.Catch_Course.domain.payments.repository.PaymentOutboxRepository; +import com.Catch_Course.global.kafka.dto.PaymentCancelRequest; +import com.Catch_Course.global.kafka.producer.PaymentCancelProducer; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OutboxScheduler { + + private final PaymentOutboxRepository paymentOutboxRepository; + private final PaymentCancelProducer paymentCancelProducer; + private final ObjectMapper objectMapper; + + // 10초마다 실행 (fixedDelayString = "10000") + @Scheduled(fixedDelay = 10000) + @Transactional + public void pollAndPublish() { + List events = paymentOutboxRepository.findTop100ByOrderByCreatedAtAsc(); + if (events.isEmpty()) { + return; + } + + log.info("[Outbox] 미처리 이벤트 {}건을 조회했습니다. 메시지 발행을 시작합니다.", events.size()); + + for (PaymentOutbox event : events) { + try { + // 2. 페이로드를 역직렬화 + PaymentCancelRequest payload = objectMapper.readValue(event.getPayload(), PaymentCancelRequest.class); + + // 3. 메시지 발행 + paymentCancelProducer.send(payload); + + // 4. 아웃박스 테이블에서 삭제 + paymentOutboxRepository.delete(event); + + } catch (Exception e) { + log.error("[Outbox] 이벤트 처리 실패. eventId: {}. 에러: {}", event.getId(), e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/Catch_Course/domain/payments/service/PaymentService.java b/backend/src/main/java/com/Catch_Course/domain/payments/service/PaymentService.java index a191a0e..a653f38 100644 --- a/backend/src/main/java/com/Catch_Course/domain/payments/service/PaymentService.java +++ b/backend/src/main/java/com/Catch_Course/domain/payments/service/PaymentService.java @@ -3,7 +3,9 @@ import com.Catch_Course.domain.member.entity.Member; import com.Catch_Course.domain.payments.dto.PaymentDto; import com.Catch_Course.domain.payments.entity.Payment; +import com.Catch_Course.domain.payments.entity.PaymentOutbox; import com.Catch_Course.domain.payments.entity.PaymentStatus; +import com.Catch_Course.domain.payments.repository.PaymentOutboxRepository; import com.Catch_Course.domain.payments.repository.PaymentRepository; import com.Catch_Course.domain.reservation.entity.Reservation; import com.Catch_Course.domain.reservation.entity.ReservationStatus; @@ -12,9 +14,9 @@ import com.Catch_Course.global.kafka.dto.PaymentCancelRequest; import com.Catch_Course.global.kafka.producer.PaymentCancelProducer; import com.Catch_Course.global.payment.TossPaymentsService; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -35,7 +37,8 @@ public class PaymentService { private final ReservationRepository reservationRepository; private final TossPaymentsService tossPaymentsService; private final PaymentCancelProducer paymentCancelProducer; - private final ApplicationEventPublisher eventPublisher; + private final PaymentOutboxRepository paymentOutboxRepository; + private final ObjectMapper objectMapper; public PaymentDto getPayment(Member member, Long reservationId) { @@ -149,13 +152,24 @@ public PaymentDto deletePaymentRequest(Member member, Long reservationId) { throw new ServiceException("409-2", "이미 취소된 결제입니다."); } - // 취소 요청 상태 + // DB 상태 변경 payment.setStatus(PaymentStatus.CANCEL_REQUESTED); + paymentRepository.save(payment); - // 메세지 직접 발행 대신 내부 이벤트로 발행 String cancelReason = "고객 요청"; - PaymentCancelRequest request = new PaymentCancelRequest(payment, cancelReason, reservation, member); - eventPublisher.publishEvent(request); + PaymentCancelRequest requestPayload = new PaymentCancelRequest(payment, cancelReason, reservation, member); + + try { + String payloadJson = objectMapper.writeValueAsString(requestPayload); + PaymentOutbox outboxEvent = PaymentOutbox.builder() + .aggregateId(payment.getId()) + .aggregateType("PAYMENT_CANCEL") + .payload(payloadJson) + .build(); + paymentOutboxRepository.save(outboxEvent); + } catch (Exception e) { + throw new RuntimeException("Outbox payload 직렬화에 실패했습니다.", e); + } return new PaymentDto(paymentRepository.save(payment)); }