드디어 12월 4일, 식후경 3.0 배포를 준비하면서 여러 인프라 문제와 개발 방향을 고민하게 되었고, 특히. 그중, 알림톡 템플릿 관리와 관련된 이야기를 정리해본다.
** 모든 코드는 예시코드입니다.
**
시작은 템플릿 관리의 어려움으로부터...
우선 템플릿 데이터를 관리하기 위해 MongoDB를 도입하고, 이를 NestJS와 연결해 관리자 권한으로 접근할 수 있는 시스템을 설계했다. MongoDB는 템플릿 구조 변경과 동적 파라미터 처리에서 유연한 스키마를 제공했기 때문에 선택하게 되었다. Atlas 무료 티어로 비용도 최소화했다.
TypeScripttsx 코드 복사 const templateSchema = new mongoose.Schema({ templateCode: { type: String, required: true }, templateName: { type: String, required: true }, content: { type: String, required: true }, parameters: [String], // 동적 파라미터 });
NestJS는 템플릿 생성, 수정, 삭제 API를 제공해 관리 편의성을 크게 높였다. 이렇게 템플릿 관리 로직을 cron.js에서 완전히 분리할 수 있었다.
템플릿 발송 로직은 Serverless Framework를 사용해 AWS Lambda에 배포했다. API Gateway를 통해 Lambda를 호출하고, MongoDB에서 템플릿 데이터를 가져오는 방식으로 설계했다. 발송 요청은 동적 파라미터를 처리한 뒤 알리고 API로 전달되는 구조였다.
TypeScripttsx 코드 복사 export const handler: APIGatewayProxyHandler = async (event) => { const { templateCode, phone, parameters } = JSON.parse(event.body || '{}'); // MongoDB에서 템플릿 조회 const template = await Template.findOne({ templateCode }); // 동적 파라미터 치환 let message = template.content; parameters.forEach(([key, value]) => { message = message.replace(`#{${key}}`, value); }); // 알림톡 발송 로직 // ... };
Lambda와 API Gateway를 사용하면 인프라 관리 부담 없이 서버리스를 통해 손쉽게 확장할 수 있을 것으로 기대했다.
하지만 예상치 못한 문제가 발생했다. 알리고 API는 최대 8개의 고정 IP만 허용했는데, 이미 Lambda에서 다른 작업들이 이 IP를 모두 사용하고 있었다. NAT Gateway를 도입하려 했지만, 운영 비용이 너무 높아 현실적이지 않았다.
결국 Lambda를 포기하고 EC2로 전환했다. EC2에서는 고정 IP를 자유롭게 설정할 수 있었고, 기존 NestJS 인스턴스에 Express를 통해 발송 로직을 간단히 통합했다.
이제 EC2에서 실행되는 Express 서버와 NestJS를 결합해 알림톡 발송 로직을 처리하게 되었다.
TypeScripttsx 코드 복사 app.post("/dev/send-alimtalk", async (req, res) => { try { const { templateCode, phone, parameters } = req.body; const template = await Template.findOne({ templateCode }); // 파라미터 치환 및 알림톡 발송 로직 const alimtalkResponse = await axios.post( "https://kakaoapi.aligo.in/akv10/alimtalk/send/", { sender: "01012345678", receiver: phone, msg: template.content, // ... 추가 데이터 } ); res.status(200).json({ success: true, data: alimtalkResponse.data }); } catch (error) { console.error("Error:", error); res.status(500).json({ success: false, error: error.message }); } });
이 방식은 안정적으로 작동했으며, 비용 효율성과 확장성을 동시에 확보할 수 있었다.
Lambda로 시작했던 발송 시스템은 IP 제한 문제로 인해 EC2로 전환되었다. 아쉬움은 있었지만, 결과적으로는 더 안정적이고 실용적인 구조를 만들 수 있었다. 특히 MongoDB와 NestJS 기반의 템플릿 관리 시스템 덕분에 cron.js에 종속되지 않는 더 유연한 환경을 구축했다.
이 여정을 통해 서버리스의 장단점을 배우는 동시에, 기술적 문제에 유연하게 대응하는 중요성을 다시금 느꼈다. 앞으로는 템플릿 관리뿐 아니라 cron.js의 기존 로직 자체를 NestJS로 완전히 분리하는 것을 목표로, 더욱 개선된 구조를 만들어갈 예정이다.
알림톡 시스템을 EC2로 이전하면서, 안정적인 서비스 운영을 위해 무중단 배포 환경을 구축하게 되었다. Blue-Green 배포 전략, Docker 컨테이너화, GitHub Actions 기반의 CI/CD 등 새로운 기술들을 도입하며 많은 시행착오를 겪었다. 이 글에서는 Docker 설정부터 GitHub Actions까지 하나하나 구체적으로 정리해 보려 한다.
NestJS 애플리케이션을 Docker로 컨테이너화하면서 다단계 빌드(Multi-stage Build)를 적용했다. 이는 빌드와 실행 단계를 분리해 이미지 크기를 줄이고 보안을 강화하는 데 중점을 뒀다.
PLAINdockerfile 코드 복사 # 빌드 스테이지 FROM node:18-alpine AS builder WORKDIR /app # 캐시 최적화를 위해 종속성 파일만 먼저 복사 COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --network-timeout 1000000 # TypeScript 빌드 설정 복사 COPY tsconfig*.json nest-cli.json ./ COPY src ./src COPY config ./config COPY .env ./ # 빌드 최적화 ENV NODE_OPTIONS="--max-old-space-size=1536" RUN yarn build # 실행 스테이지 FROM node:18-alpine WORKDIR /app # 빌드 결과만 복사 COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.env ./ COPY --from=builder /app/config ./config COPY package*.json ./ # 환경 변수 설정 ENV NODE_ENV=production ENV PORT=3000 EXPOSE 3000 CMD ["node", "dist/main.js"]
이 구조를 통해 실행 환경에 불필요한 빌드 파일을 제거하고 이미지 크기를 최소화할 수 있었다.
Blue-Green 배포는 두 개의 독립적인 애플리케이션 환경(Blue, Green)을 유지하며, 하나가 운영되는 동안 다른 하나를 배포하는 방식이다. 이를 통해 무중단 배포가 가능하며, 문제 발생 시 즉각 롤백이 가능하다.
YAMLyaml 코드 복사 version: "3.9" services: blue: build: . container_name: eatbuy_blue ports: - "8080:3000" environment: - NODE_ENV=production - PORT=3000 - CONTAINER_NAME=blue volumes: - ./logs:/app/logs - ./config:/app/config networks: - eatbuy_network deploy: resources: limits: memory: 2G reservations: memory: 1G green: build: . container_name: eatbuy_green ports: - "8081:3000" environment: - NODE_ENV=production - PORT=3000 - CONTAINER_NAME=green volumes: - ./logs:/app/logs - ./config:/app/config networks: - eatbuy_network deploy: resources: limits: memory: 2G reservations: memory: 1G networks: eatbuy_network: driver: bridge
두 컨테이너(Blue와 Green)는 동일한 설정을 공유하며, 배포 과정에서 번갈아 활성화된다. 각 컨테이너에 메모리 제한을 설정해 안정성을 확보했다.
배포 과정의 자동화를 위해 Shell Script를 작성했다. 새로운 컨테이너를 배포하고 헬스 체크 후 문제가 없으면 이전 컨테이너를 종료한다.
deploy.shBashbash 코드 복사 #!/bin/bash # 실행 중인 컨테이너 확인 CURRENT_CONTAINER=$(docker ps --filter "name=eatbuy_" --format "{{.Names}}") # 헬스 체크 함수 check_health() { local port=$1 local attempts=30 local sleep_time=1 echo "🏥 Checking health on port $port..." for ((i=1; i<=attempts; i++)); do if curl -s "http://localhost:$port/api/health" | grep -q "status.*ok"; then return 0 fi echo "Attempt $i/$attempts..." sleep $sleep_time done return 1 } # Blue가 실행 중이면 Green으로 배포 if [[ $CURRENT_CONTAINER == *"blue"* ]]; then echo "🚀 Deploying to green container..." docker compose up -d green --build --force-recreate if check_health 8081; then echo "✅ Green deployment successful, stopping blue..." docker compose stop blue else echo "❌ Green deployment failed, rolling back..." docker compose stop green docker compose start blue exit 1 fi else echo "🚀 Deploying to blue container..." docker compose up -d blue --build --force-recreate if check_health 8080; then echo "✅ Blue deployment successful, stopping green..." docker compose stop green else echo "❌ Blue deployment failed, rolling back..." docker compose stop blue docker compose start green exit 1 fi fi echo "🎉 Deployment completed successfully!"
코드가 main 브랜치에 병합되면 자동으로 배포가 이루어지도록 GitHub Actions를 구성했다.
.github/workflows/deploy.ymlYAMLyaml 코드 복사 name: Deploy to EC2 on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Configure SSH run: | mkdir -p ~/.ssh/ echo "${{ secrets.EC2_SSH_KEY }}" | base64 -d > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - name: Deploy to EC2 run: | ssh -i ~/.ssh/deploy_key ubuntu@${{ secrets.EC2_HOST }} ' cd ~/eatbuy2.0_sub_backend && git pull origin main && ./deploy.sh '
GitHub Actions는 배포 과정을 자동화하며, EC2에 SSH로 접속해 deploy.sh 스크립트를 실행한다. 이를 통해 코드 업데이트와 배포가 완전히 자동화되었다.
운영 중인 컨테이너의 상태를 실시간으로 모니터링하고 관리하기 위해 Portainer를 도입했다.
YAMLyaml 코드 복사 services: portainer: image: portainer/portainer-ce:latest container_name: portainer ports: - "9000:9000" volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data restart: always networks: - eatbuy_network networks: eatbuy_network: driver: bridge volumes: portainer_data:
Portainer는 직관적인 UI를 제공해 컨테이너 상태, 리소스 사용량, 로그를 실시간으로 확인할 수 있었다.
Docker 기반의 Blue-Green 배포와 GitHub Actions CI/CD를 성공적으로 구축하며, 무중단 배포 환경을 완성할 수 있었다. 특히 배포 자동화와 모니터링 강화 덕분에 운영 효율성이 크게 향상되었고, 서비스 안정성도 확보할 수 있었다.
앞으로는 카나리 배포와 같은 다른 배포 전략도 도입해 다양한 시나리오에 대비하고자 한다. 또한, Portainer의 대시보드를 커스터마이징하고 로그 집중화 시스템을 구축해 운영 효율성을 더욱 높일 계획이다. 이번 여정은 Docker와 CI/CD의 강력함을 체감하는 좋은 경험이었다.
4o