Skip to content

Conversation

@jsoonworld
Copy link
Member

@jsoonworld jsoonworld commented Jun 30, 2025

📄 Work Description

이번 작업은 반복적으로 발생하던 CI/CD 파이프라인 실패 및 배포 중 서버 다운 현상의 근본 원인을 파악하고 해결하기 위해, 프로젝트 전반의 설정 관리 방식과 배포 파이프라인을 현대적인 방식으로 전면 개선하는 것을 목표로 삼았습니다.

파편화되어 있던 설정 정보와 배포 스크립트를 중앙에서 명확하게 관리할 수 있도록 변경하여, 향후 유지보수성과 안정성을 높이는 데 초점을 맞췄습니다.

주요 변경 사항 및 기술적 근거

1. 설정 관리 구조 개선 (코드와 설정의 분리)

  • application.yml 템플릿화:

    • As-Is: 민감 정보가 포함된 yml 파일을 통째로 GitHub Secret에 저장하고, 빌드 시점에 동적으로 생성했습니다. 이는 Secret 관리의 복잡성을 높이고, 이미지 내에 민감 정보가 포함될 위험이 있었습니다.

    • To-Be: application.yml을 ${...} 플레이스홀더를 사용하는 안전한 '템플릿'으로 변경했습니다. 이제 Git에는 설정의 '구조'만 남고, 실제 '값'은 실행 시점에 주입되어 보안과 유연성이 크게 향상될 것으로 기대합니다.

  • 프로파일 기반 설정 분리 (application-*.yml):

    • As-Is: 하나의 거대한 yml 파일에 모든 환경의 설정이 혼재되어 있었습니다.

    • To-Be: dev, prod, test 등 각 환경의 고유한 동작 방식(예: ddl-auto: update vs validate)만 각자의 파일에 정의하도록 분리했습니다. 이를 통해 환경별 설정 차이를 명확하게 관리하고, 실수를 방지할 수 있을 것 같아요.

  • 안전한 Secret 주입:

    • As-Is: 빌드 시점에 Secret을 파일로 생성했습니다.

    • To-Be: GitHub Environments 기능을 활용하여, 각 환경(staging, production)에 격리된 Secret들을 런타임에 컨테이너 환경 변수로 안전하게 주입하게 됩니다.

2. 독립적인 테스트 환경 구축

  • application-test.yml 및 H2 도입:

    • As-Is: 테스트를 위한 독립적인 설정이 없어, 로컬 환경이나 외부 DB에 의존했습니다.

    • To-Be: application-test.yml에 H2 인메모리 DB 설정을 완벽하게 정의했습니다. H2를 PostgreSQL 호환 모드로 사용하여 실제 운영 환경과 유사하게 동작하도록 구성했고, 이를 통해 외부 의존성 없이 언제 어디서든 빠르고 일관된 테스트 실행을 보장할 것 같아요.

  • @ActiveProfiles("test") 적용 및 의존성 수정:

    • 모든 테스트 코드에 @ActiveProfiles("test")를 적용하여 application-test.yml을 사용하도록 명시했습니다.

    • build.gradle에서 H2 의존성을 testImplementation으로 변경하여, IDE에서도 관련 클래스를 정상적으로 인식하도록 개선했습니다.

[현황 및 후속 계획]
현재 운영 환경은 PostgreSQL, 테스트 환경은 H2를 사용하도록 구성하는 과정에서, 각기 다른 DB 의존성으로 인해 한쪽의 빌드가 성공하면 다른 쪽에서 문제가 발생하는 트레이드오프 관계가 있었습니다. 프로젝트에 테스트 코드가 아직 부재한 현 상황을 고려하여, 우선 프로덕션 코드의 안정적인 빌드와 배포를 보장하는 방향으로 의존성 구조를 구성했습니다.

향후 독립된 테스트 환경의 완전한 구현은 후속 작업으로 계획하고 있으며, 관련 내용을 추가적으로 학습하여 완성도 높게 적용할 예정입니다.

3. CI/CD 파이프라인(GitHub Actions) 개편

  • 범용 Docker 이미지 빌드:

    • As-Is: 환경별로 다른 이미지를 만들거나, 빌드 시점에 환경 설정을 주입했습니다.

    • To-Be: CI 단계에서는 어떤 환경 정보도 담지 않은 '범용' 이미지를 빌드합니다. "한 번 빌드해서, 모든 환경에 배포한다(Build Once, Deploy Anywhere)"는 원칙을 따릅니다.

  • 배포 스크립트 내재화 (Infrastructure as Code):

    • As-Is: 서버에 존재하는 deploy.sh를 원격으로 호출했습니다. 배포 로직을 확인하려면 서버에 직접 접속해야 했습니다.

    • To-Be: deploy.sh의 블루/그린 배포 로직 전체를 워크플로우 script 블록 안으로 가져왔습니다. 이제 배포의 모든 과정을 코드로 명확하게 확인하고 Git으로 버전을 관리할 수 있습니다.

  • 테스트 실행 의무화:

    • DEV-CI 뿐만 아니라 DOCKER-CD-STAGING, DOCKER-CD-PRODUCTION 워크플로우의 빌드 단계에서 항상 ./gradlew build를 실행하도록 -x test 옵션을 제거했습니다. 이를 통해 모든 배포 전에 코드의 안정성을 자동으로 검증합니다. (기존에 테스트에서 컴파일 에러가 나타남에도 불구하고 머지가 되어왔습니다.)

4. 코드 및 빌드 설정 개선

  • User.java의 @Builder.Default 적용 및 와일드카드 임포트 제거 등 코드 컨벤션을 개선했습니다.

  • build.gradle의 중복된 의존성을 정리하고 기능별로 그룹화하여 가독성을 높였습니다.

💬 To Reviewers

이번 PR은 저희 프로젝트의 CI/CD 안정성을 확보하고, 반복되던 배포 실패 문제를 근본적으로 해결하기 위한 구조적인 개선 작업입니다. 관련된 설정과 파이프라인이 많아 본래는 단계별로 나누어 진행하려 했으나, 변경 사항들이 서로 강하게 맞물려 있어 전체적인 흐름을 한 번에 검토하고 빠른 피드백을 통해 안정화하는 것이 더 효율적이라 판단하여 하나의 PR로 통합하게 되었습니다.

가장 큰 수확은 이번 개선 작업을 준비하면서, 지속적으로 발생했던 배포 중 서버 다운 현상의 가장 유력한 원인을 발견한 것입니다. docker stats 로그를 통해 애플리케이션 컨테이너 하나가 평상시에도 서버 메모리의 약 43%를 점유하고 있음을 확인했습니다. 이는 다운타임 없는 배포를 위해 2개의 컨테이너를 동시에 실행하는 블루/그린 배포 과정에서 메모리 사용량이 86% 이상으로 급증하여, 리눅스의 OOM Killer에 의해 프로세스가 강제 종료되었을 것이라는 강력한 심증을 갖게 되었어요.

따라서 이번 PR이 리뷰 및 머지된 후, 다음 단계로 EC2 인스턴스 사양을 t3급으로 업그레이드하여 블루/그린 배포 시 필요한 메모리 자원을 확보하고, 안정적인 배포를 보장하고자 합니다.

새롭게 개편된 설정 관리 방식과 CI/CD 파이프라인 구조가 앞으로 프로젝트를 더 안전하고 효율적으로 운영하는 데 큰 도움이 될 것이라 생각합니다. 이번에 변경된 세부적인 환경 변수 목록이나 워크플로우 내에서 수정한 네이밍 컨벤션 등은 리뷰의 편의를 위해 별도의 팀 공유 문서로 정리하여 전달드리겠습니다. 코드와 워크플로우의 구조적인 개선점에 대해 중점적으로 리뷰해주시면 감사하겠습니다.

⚙️ ISSUE

기존에는 테스트 실행을 위한 독립적인 설정이 없어, 실제 DB나 외부 환경 변수에 의존하는 문제가 있었습니다.

이를 해결하기 위해 다음과 같이 테스트 환경을 구축하고 설정을 분리합니다.

- H2 인메모리 데이터베이스를 사용하여 테스트 환경을 구성하고, `application-test.yml`에 관련 설정을 모두 정의했습니다.
- 테스트 실행 시 `@ActiveProfiles("test")`를 통해 `test` 프로파일이 활성화되도록 수정했습니다.
- `build.gradle`의 h2 의존성을 `testRuntimeOnly`에서 `testImplementation`으로 변경하여, IDE에서도 관련 클래스를 정상적으로 인식하도록 개선했습니다.
기존에는 민감 정보가 포함된 yml 파일을 GitHub Secret으로 관리하여, 빌드 시점에 동적으로 생성하는 방식을 사용했습니다. 이는 보안상 위험하고 유연성이 떨어지는 문제가 있었습니다.

'코드와 설정의 분리' 원칙에 따라 다음과 같이 설정 구조를 개선합니다.

- `application.yml`: 모든 환경의 설정 뼈대가 되는 템플릿 파일로 변경하고, 민감 정보는 `${...}` 플레이스홀더로 대체했습니다.
- `application-dev.yml`, `application-prod.yml`: 각 환경의 고유한 동작(ddl-auto 등)만 정의하도록 분리했습니다.
- `.gitignore`: 이제 yml 템플릿 파일들은 안전하므로, Git 추적에서 제외하던 설정을 제거하여 버전 관리에 포함시킵니다.
기존에는 테스트 실행을 위한 독립적인 설정이 없어, 실제 DB나 외부 환경 변수에 의존하는 문제가 있었습니다.

이를 해결하기 위해 다음과 같이 테스트 환경을 구축하고 설정을 분리합니다.

- H2 인메모리 데이터베이스를 사용하여 테스트 환경을 구성하고, `application-test.yml`에 관련 설정을 모두 정의했습니다.
- 테스트 실행 시 `@ActiveProfiles("test")`를 통해 `test` 프로파일이 활성화되도록 수정했습니다.
- `build.gradle`의 h2 의존성을 `testRuntimeOnly`에서 `testImplementation`으로 변경하여, IDE에서도 관련 클래스를 정상적으로 인식하도록 개선했습니다.
기존 CI/CD 파이프라인은 빌드 시점에 Secret을 이용해 yml 파일을 동적으로 생성하고, 서버에서는 deploy.sh 스크립트를 호출하는 방식에 의존했습니다. 이로 인해 배포 과정의 가시성이 낮고, 서버에 배포 스크립트를 수동으로 관리해야 하는 불편함이 있었습니다.

새로운 설정 구조에 맞춰 다음과 같이 파이프라인을 전면 개편합니다.

- **yml 파일 생성 제거:** 더 이상 CI 단계에서 `yml` 파일을 생성하지 않고, Git에 포함된 템플릿을 사용하여 범용 Docker 이미지를 빌드합니다.
- **배포 스크립트 내재화:** `deploy.sh`의 모든 로직(블루/그린 배포)을 워크플로우의 `script` 블록으로 가져와, 배포의 모든 과정을 코드로 명확하게 관리(IaC)합니다.
- **안전한 Secret 주입:** GitHub Environments 기능을 사용하여 각 환경(`staging`, `production`)에 맞는 Secret들을 `docker run` 명령어의 `-e` 옵션으로 안전하게 주입합니다.
- **테스트 실행:** 모든 배포 전 빌드 단계에서 테스트가 항상 실행되도록 `-x test` 옵션을 제거하여 코드 안정성을 강화했습니다.
@jsoonworld jsoonworld self-assigned this Jun 30, 2025
@jsoonworld jsoonworld added ♻️ refactor 코드 리팩토링 ex) 형식변경 🦊장순🦊 labels Jun 30, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR modernizes configuration management, testing, and deployment pipelines to improve CI/CD stability and maintainability.

  • Centralized application settings with a template-based application.yml and separate profile files.
  • Introduced an in-memory H2 test environment and activated the test profile in integration tests.
  • Overhauled GitHub Actions workflows to build a single Docker image and execute blue/green deployments for staging and production.

Reviewed Changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/test/java/org/terning/terningserver/service/ScrapServiceTest.java Entire test class commented out, disabling concurrency tests.
src/test/java/org/terning/terningserver/TerningserverApplicationTests.java Added @ActiveProfiles("test") annotation for test profile.
src/main/resources/application.yml Added templated config with profile imports and placeholders.
src/main/resources/application-test.yml Configured H2 in-memory database for tests.
src/main/resources/application-dev.yml Defined dev-specific JPA and batch settings.
src/main/resources/application-prod.yml Defined production JPA validation settings.
src/main/java/org/terning/terningserver/user/domain/User.java Cleaned imports and applied @Builder.Default for collections.
.github/workflows/DEV-CI.yml Renamed job and enforced full build and test.
.github/workflows/DOCKER-CD-STAGING.yml Removed yml generation, streamlined build/test, added blue/green deploy script.
.github/workflows/DOCKER-CD-PRODUCTION.yml Added production blue/green deployment workflow.
Comments suppressed due to low confidence (1)

src/test/java/org/terning/terningserver/service/ScrapServiceTest.java:1

  • The entire ScrapServiceTest class has been commented out, which disables critical concurrency tests and reduces test coverage. Please re-enable these tests or use JUnit's @Disabled annotation with a tracking issue reference if you need to temporarily disable them.
//package org.terning.terningserver.service;

Copy link
Contributor

@junggyo1020 junggyo1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작업 내용이 많았던 것 같은데, 고생하셨습니다 :)

  • 기존 파이프라인에서 application.yml을 템플릿화하고 환경 변수로 민감 정보를 주입하는 방식으로 변경한 부분이 보안과 유연성 측면에서 되게 좋은 방향의 리펙토링이라고 생각이 드네요..!
  • 다만 CI/CD 파이프라인의 변경사항이 많아 충분히 스테이징 서버에서 안정적으로 배포가 이루어지는지 테스트 한 이후 메인서버에 배포를 결정하면 좋을 것 같습니다 ㅎㅎ
  • t3로의 사양 변경은 지금 결정할 사항은 아닌 것 같고, 개선 이후에도 필요하다는 생각이 들면, 기획측과 함께 논의하고 결정하면 좋을 것 같습니다! 고생하셨어요 ㅎㅎ

Comment on lines +49 to 50
@Builder.Default
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder.Default로 NPE를 예방하는 것도 되게 좋은 방법 같아요! 👍

Comment on lines +60 to +66
script: |
# -- 변수 설정 --
APP_NAME="terning2025-prod"
IMAGE_NAME="terningpoint/terning2025"
NGINX_CONFIG_PATH="/etc/nginx"
SERVICE_URL_INC_PATH="${NGINX_CONFIG_PATH}/conf.d/service-url.inc"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 스크립트가 긴데, 중간에 하나의 명령어가 실패하더라도 다음 명령어를 계속 실행할 수 있는 상태라 set -e를 스크립트 상단에 추가하는 것은 어떨까요? 명령어가 하나라도 실패하면 스크립트 전체를 중단시키도록 설정하는게 좋을 것 같아서 제안드려봅니다 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼하게 리뷰해주셔서 감사합니다! 👍

말씀해주신 대로 set -e 옵션을 추가해서 스크립트의 안정성을 높이는 것, 정말 좋은 제안이라고 생각합니다. 중간에 명령어가 실패했을 때 즉시 파이프라인을 중단시키는 것이 더 안전한 방법이겠네요.

다만 현재 PR에서는 핵심 기능 구현에 더 집중하고 싶습니다. 제안해주신 내용은 파이프라인 안정성 강화라는 좋은 개선 과제이니, 별도 이슈로 등록해서 다음 단계에서 꼭 반영하도록 하겠습니다.

다시 한번 좋은 의견 감사드립니다! 😊

Comment on lines +34 to +35
// Database & Persistence
implementation 'org.postgresql:postgresql:42.7.3'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트를 실행할 때, postgres를 사옹하지 않는다면 runtimeOnly로 설정해도 좋을 것 같은데, 어떻게 생각하시나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

피드백 감사합니다!

말씀해주신 대로, 프로덕션에서만 사용하는 PostgreSQL 드라이버는 runtimeOnly로 선언하는 것이 의존성 관리 측면에서 훨씬 깔끔하고 이상적인 방향이 좋다고 생각합니다.

사실 이번 PR을 진행하며, 테스트 환경은 H2로, 프로덕션은 PostgreSQL로 완벽하게 분리하고자 testImplementation 'com.h2database:h2'와 runtimeOnly 'org.postgresql:postgresql' 설정을 시도했습니다. 하지만 이 과정에서 두 DB 드라이버 의존성이 서로 영향을 주어, 한쪽 환경의 빌드가 성공하면 다른 쪽에서 클래스를 찾지 못하는 문제가 발생했습니다.

PR 설명(Description)에 남겼듯이, 현재 프로젝트에 테스트 코드가 아직 없는 상황을 고려하여 우선 프로덕션 코드의 안정적인 빌드와 배포를 보장하는 것을 최우선 목표로 삼았습니다. 그래서 임시적으로 PostgreSQL 의존성을 implementation으로 유지해 빌드를 성공시키는 방향으로 결정했습니다.

이 의존성 분리 문제는 꼭 해결해야 할 기술 부채라고 생각합니다. 제안해주신 내용을 바탕으로 "테스트/프로덕션 DB 의존성 분리" 라는 이름의 이슈를 등록해서 다음 단계에서 반드시 개선하도록 하겠습니다.

정확한 포인트를 짚어주셔서 다시 한번 감사합니다!

@JungYoonShin
Copy link
Member

JungYoonShin commented Jun 30, 2025

deploy.sh의 블루/그린 배포 로직 전체를 워크플로우 script 블록 안으로 가져왔습니다. 이제 배포의 모든 과정을 코드로 명확하게 확인하고 Git으로 버전을 관리할 수 있습니다.

요 의견 자체는 좋은 것 같아요!! 근데 yml에 deploy.sh 내용이 다 담기고 있다보니 조금 복잡하게 느껴지고 관리가 어려운 부분이 있을 것 같아서, scripts 같은 폴더에 deploy.sh를 두고 따로 관리하는건 어떨까요?!

deploy.sh 자체는 깃허브에서 관리하고, GitHub에서 EC2로 deploy.sh를 복사하고, 그 후에 EC2 내에서 실행하는 방식으로요!!

이렇게 되면 git으로 관리도 가능하면서 워크플로우도 더 깔끔히 관리할 수 있을 것 같습니당 ㅎㅎ

@jsoonworld
Copy link
Member Author

@junggyo1020

다만 CI/CD 파이프라인의 변경사항이 많아 충분히 스테이징 서버에서 안정적으로 배포가 이루어지는지 테스트 한 이후 메인서버에 배포를 결정하면 좋을 것 같습니다 ㅎㅎ

네, 충분히 공감합니다! 이번 CI/CD 파이프라인 변경사항이 많기 때문에, 스테이징 서버에서 충분히 안정적으로 배포가 이루어지는지 꼼꼼하게 테스트하는 과정이 정말 중요하다고 생각해요. 말씀해주신 대로 스테이징에서 완벽하게 안정성을 확인한 후 메인 서버에 배포를 진행하도록 하겠습니다. 소중한 피드백 감사드립니다!

t3로의 사양 변경은 지금 결정할 사항은 아닌 것 같고, 개선 이후에도 필요하다는 생각이 들면, 기획측과 함께 논의하고 결정하면 좋을 것 같습니다! 고생하셨어요 ㅎㅎ

t3 사양 변경에 대해서는 조금 더 자세히 제 생각을 말씀드리고 싶습니다.

현재 저희 EC2 인스턴스 메모리 사용량이 평상시에도 43%에 육박하고 있습니다. 그리고 지금까지 CI/CD를 진행할 때마다 서버가 불안정해지고 다운되는 현상을 반복적으로 겪어왔는데, 이게 바로 메모리 부족 때문이라는 강력한 심증을 가지고 있습니다. 블루/그린 배포 방식은 잠재적으로 2개의 컨테이너가 동시에 실행될 수 있어, 이 과정에서 메모리 사용량이 급증하면 OOM Killer에 의해 프로세스가 강제 종료될 가능성이 매우 높습니다. (로그에서 확인이 되었습니다.)

이번 PR을 통해 설정 관리와 배포 파이프라인의 안정성을 크게 개선하는 것은 물론 중요하지만, 근본적인 메모리 자원 부족 문제는 하드웨어 사양 업그레이드 없이는 해결하기 어렵다고 판단하고 있습니다. 이미 여러 차례 서버 다운을 겪으며 시간과 노력을 소모했기에, 더 이상 같은 문제를 반복하는 것보다는 지금 시점에 t3로의 업그레이드를 통해 안정적인 배포 환경을 확보하는 것이 장기적으로 팀의 생산성과 서비스 안정성에 훨씬 긍정적인 영향을 미칠 것이라고 생각합니다.

서버가 다운될 때마다 직접 재실행하는 방식은 단기적인 해결책일 뿐, 결국 반복적인 운영 부담과 서비스 중단으로 이어질 수 있다고 우려됩니다. 따라서 기획팀과의 논의를 거쳐서라도 t3로의 사양 업그레이드를 조속히 진행하는 것이 지금 시점에서는 필요하다고 조심스럽게 제안드립니다.

이 부분에 대해 다시 한번 함께 논의해볼 수 있을까요?

@jsoonworld
Copy link
Member Author

@JungYoonShin

요 의견 자체는 좋은 것 같아요!! 근데 yml에 deploy.sh 내용이 다 담기고 있다보니 조금 복잡하게 느껴지고 관리가 어려운 부분이 있을 것 같아서, scripts 같은 폴더에 deploy.sh를 두고 따로 관리하는건 어떨까요?!
deploy.sh 자체는 깃허브에서 관리하고, GitHub에서 EC2로 deploy.sh를 복사하고, 그 후에 EC2 내에서 실행하는 방식으로요!!
이렇게 되면 git으로 관리도 가능하면서 워크플로우도 더 깔끔히 관리할 수 있을 것 같습니당 ㅎㅎ

피드백 감사해요! 워크플로우의 가독성과 관리 포인트를 넓게 봐주셔서 감사합니다!. 😊

말씀해주신 대로, 배포 스크립트 전체가 yml 파일에 들어있어 복잡하고 길어 보일 수 있다는 점에 전적으로 공감합니다.

제가 이번에 배포 로직을 워크플로우 안으로 가져온 핵심적인 이유는, EC2 서버에 직접 접속하거나 서버 내의 파일(deploy.sh) 상태에 의존하지 않고, 배포의 모든 과정을 GitHub Actions 환경 내에서만 통제하고 싶었기 때문입니다. 제안해주신 scp 등으로 스크립트를 EC2에 복사 후 실행하는 방식도 좋은 방법이지만, 배포 로직의 실행 주체가 다시 서버 내부가 되어 당초의 목표와는 조금 멀어질 수 있다고 생각했습니다.

하지만 '하나의 yml 파일이 너무 복잡해진다'는 지점은 정말 중요한 포인트라고 생각합니다.

따라서 이 부분은 더 나은 구조를 찾아볼 개선 과제로 가져가면 좋을 것 같습니다. 우선 이번 PR은 현재 구조로 머지하여 파이프라인의 큰 흐름을 완성하고, 제안해주신 내용을 바탕으로 "배포 스크립트 관리 방식 개선" 이라는 이슈를 등록하여 논의를 이어가면 어떨까요?

예를 들어, 스크립트를 .sh 파일로 분리하되 EC2에 복사하지 않고 GitHub Actions에서 바로 실행하는 방법 등 여러 대안을 찾아볼 수 있을 것 같습니다.

다시 한번 깊이 있는 리뷰에 감사드립니다!

@jsoonworld jsoonworld merged commit f3439e5 into develop Jul 1, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ refactor 코드 리팩토링 ex) 형식변경 size/XL 🦊장순🦊

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[EPIC] 개발-배포-운영 파이프라인 안정성 강화

4 participants