Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e81309
🔥 remove:: WarmupModule, WarmupService 삭제
JangDongHo May 2, 2026
44c7db0
♻️ refactor: 식당 식단 정보 구조 변경 및 관련 로직 개선
JangDongHo May 2, 2026
5de2f21
🔥 remove: @nestjs/schedule 의존성 제거
JangDongHo May 2, 2026
c764807
📝 docs: 도메인별 요구사항 문서화
JangDongHo May 2, 2026
bd99b5c
Merge branch 'dev' into refactor/#78
JangDongHo May 2, 2026
ae4281d
🔥 remove: 필요없는 의존성 정리
JangDongHo May 3, 2026
d75dd69
♻️ refactor: CafeteriaDietQueryDto 필드 필수화 및 관련 로직 개선
JangDongHo May 3, 2026
b80518f
Merge pull request #79 from GNU-connect/refactor/#78
JangDongHo May 3, 2026
dfc7931
📝 docs: README.md 설계 과정과 이유 추가
JangDongHo May 4, 2026
b1beaf4
📝 docs: README.md 업데이트
JangDongHo May 4, 2026
2a2f0a9
♻️ refactor: TypeORM 파일을 도메인 모듈 안으로 이동
JangDongHo Jun 11, 2026
495c2fe
Merge pull request #82 from GNU-connect/refactor/co-locate-typeorm-files
JangDongHo Jun 11, 2026
546add3
♻️ refactor: public 모듈 레이어 구조 정리
JangDongHo Jun 11, 2026
44fb793
Merge pull request #83 from GNU-connect/refactor/public-module-layer-…
JangDongHo Jun 11, 2026
0ceb4e7
♻️ refactor: TypeORM 저장소 조회 로직 정리
JangDongHo Jun 11, 2026
3845675
Merge pull request #84 from GNU-connect/refactor/typeorm-repositories
JangDongHo Jun 11, 2026
8623b63
♻️ refactor: 카카오 표현 계층 의존성 분리
JangDongHo Jun 12, 2026
0232c29
Merge pull request #85 from GNU-connect/refactor/decouple-kakao-prese…
JangDongHo Jun 12, 2026
ce45e4c
✨ feat: 배치 모듈 생성
JangDongHo Jun 14, 2026
801511b
✨ feat: 배치 모듈 및 스케줄링 기능 추가
JangDongHo Jun 14, 2026
dacb5c5
✨ feat: 배치 작업 모듈 및 작업 추가
JangDongHo Jun 14, 2026
4128031
✨ feat: 셔틀 시간표 수집 및 파싱 구조 추가
JangDongHo Jun 14, 2026
be5736b
✨ feat: 셔틀 시간표 엔티티 및 파서 구현
JangDongHo Jun 14, 2026
e7e886f
✨ feat: 셔틀 시간표 파서 및 데이터 구조 개선
JangDongHo Jun 14, 2026
d409159
✨ feat: 배치 서비스 및 HTTP 클라이언트 개선, 셔틀 시간표 파서 테스트 추가
JangDongHo Jun 15, 2026
887a270
Merge pull request #87 from GNU-connect/feat/data-scraper
JangDongHo Jun 15, 2026
ff15806
📝 docs: API CI/CD 리팩토링 및 AWS EC2 이전 설계 문서 추가
JangDongHo Sep 24, 2026
4d5f3a9
🔧 chore: API 운영 compose 및 nginx 설정을 EC2 서버 반영본으로 동기화
JangDongHo Sep 24, 2026
cf52934
📝 docs: API CI/CD 설계 문서에 서버 반영 설정 내용 보완
JangDongHo Sep 24, 2026
b998539
📝 docs: API CI/CD 구현 계획 추가 및 배포 스크립트 설계 보완
JangDongHo Sep 24, 2026
059951a
👷 ci: API CI를 재사용 워크플로로 전환하고 빌드 검증 추가
JangDongHo Sep 24, 2026
185490b
🔧 chore: nginx 설정을 디렉터리 마운트로 변경해 배포 시 reload 반영
JangDongHo Sep 24, 2026
2932702
👷 ci: API CD에 CI 게이트 추가 및 이미지 빌드 설정 정리
JangDongHo Sep 24, 2026
1575c5f
👷 ci: EC2 배포 job에 헬스체크 대기와 nginx 설정 반영 추가
JangDongHo Sep 24, 2026
54337de
🐛 fix: nginx 설정 검사를 컨테이너 재생성 전에 수행
JangDongHo Sep 24, 2026
96aafab
🐛 fix: 루트 테스트 스크립트가 pnpm 버전과 무관하게 jest 인자를 전달하도록 수정
JangDongHo Sep 24, 2026
3f5eacc
Merge pull request #88 from GNU-connect/chore/api-cicd-aws
JangDongHo Sep 24, 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
108 changes: 85 additions & 23 deletions .github/workflows/api-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,59 @@ on:
branches:
- main
paths:
- "services/api/app/src/**"
- "services/api/app/package.json"
- "services/api/app/pnpm-lock.yaml"
- "services/api/app/Dockerfile"
- "services/api/docker-compose.yml"
- "services/api/nginx/**"
- ".github/workflows/api-cd.yml"
- 'services/api/app/src/**'
- 'services/api/app/package.json'
- 'services/api/app/pnpm-lock.yaml'
- 'services/api/app/Dockerfile'
- 'services/api/app/.dockerignore'
- 'services/api/app/tsconfig*.json'
- 'services/api/app/nest-cli.json'
- 'services/api/docker-compose.yml'
- 'services/api/nginx/**'
- '.github/workflows/api-cd.yml'

workflow_dispatch:

permissions:
contents: read

concurrency:
group: api-cd-${{ github.ref }}
cancel-in-progress: false

env:
ROOT_DIR: ./connect-gnu/services/api
DEPLOY_DIR: /opt/connectgnu/api

jobs:
ci:
name: CI
uses: ./.github/workflows/api-ci.yml

build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: ci

steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Push Docker Image
uses: docker/build-push-action@v6
- name: Build and Push Docker Image
uses: docker/build-push-action@v7
with:
context: services/api/app
target: prod
push: true
pull: true
context: services/api/app
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:${{ github.sha }}
Expand All @@ -52,28 +68,74 @@ jobs:
name: Deploy to API Server
runs-on: ubuntu-latest
needs: build-and-push

steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Copy files to server
uses: appleboy/scp-action@v0.1.7
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.API_SSH_HOST }}
username: ${{ secrets.API_SSH_USERNAME }}
key: ${{ secrets.API_SSH_PRIVATE_KEY }}
source: services/api/docker-compose.yml,services/api/nginx,services/api/letsencrypt/init-letsencrypt.sh
target: ${{ env.ROOT_DIR }}
source: services/api/docker-compose.yml,services/api/nginx/prod.conf
target: ${{ env.DEPLOY_DIR }}
strip_components: 2

- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.API_SSH_HOST }}
username: ${{ secrets.API_SSH_USERNAME }}
key: ${{ secrets.API_SSH_PRIVATE_KEY }}
script: |
cd ${{ env.ROOT_DIR }}
sudo docker-compose pull app
sudo docker-compose up -d app --no-deps
sudo docker image prune -af
set -euo pipefail
cd ${{ env.DEPLOY_DIR }}

docker compose pull app
docker compose up -d --no-deps app

cid=$(docker compose ps -q app)
if [ -z "$cid" ]; then
echo "app container not found"
docker compose ps
exit 1
fi

status=none
for _ in $(seq 1 30); do
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid")
case "$status" in
healthy|unhealthy|none) break ;;
esac
sleep 3
done

if [ "$status" != healthy ]; then
echo "app health status: $status"
docker compose logs --tail 100 app
exit 1
fi

if ! docker compose run --rm --no-deps -T nginx nginx -t; then
echo "nginx config test failed"
exit 1
fi
docker compose up -d --no-deps nginx

reloaded=false
for _ in 1 2 3 4 5; do
if docker compose exec -T nginx nginx -s reload; then
reloaded=true
break
fi
sleep 2
done
if [ "$reloaded" != true ]; then
echo "nginx reload failed"
docker compose logs --tail 50 nginx
exit 1
fi

docker image prune -f
18 changes: 11 additions & 7 deletions .github/workflows/api-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ on:
branches:
- main
- dev

paths:
- 'services/api/app/**'
- '.github/workflows/api-ci.yml'

workflow_call:

workflow_dispatch:

permissions:
Expand All @@ -20,24 +21,24 @@ concurrency:
cancel-in-progress: true

jobs:
lint-and-test:
name: Lint and Test
verify:
name: Lint, Test and Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/api/app

steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 9.9.0
package_json_file: services/api/app/package.json

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
Expand All @@ -51,3 +52,6 @@ jobs:

- name: Run Tests
run: pnpm test

- name: Build
run: pnpm build
145 changes: 143 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,20 @@

---

## 설계 과정과 이유
## 🔷 사용자 요청 처리 흐름

<img width="1287" height="413" alt="image" src="https://github.com/user-attachments/assets/e866bc56-66e4-4196-bb81-4a447d2550c4" />

- 카카오 챗봇은 사용자의 발화를 미리 정의된 의도와 매칭하고, 각 의도에 연결된 API를 호출하는 구조입니다.
- 예를 들어 "내일 기숙사 식당 점심 알려줘" 와 같은 자연어 요청이 들어오면, NLU가 식단 조회 의도로 분류 후 연결된 식단 API를 호출해 결과를 반환합니다.

---

## 🔷 설계 과정과 이유

제한된 기간과 팀 상황 안에서 왜 그렇게 설계했는지를 남기기 위해 작성했습니다.

각 의사결정은 `문제 → 선택 → 근거 → 결과/트레이드오프 → 느낀 점` 순서로 정리했습니다.
각 의사결정은 `문제 → 원인 → 해결 → 결과/트레이드오프 → 느낀 점` 순서로 정리했습니다.

### 1. 데이터 모델링

Expand Down Expand Up @@ -136,6 +145,138 @@
- 특정 학과나 사용자보다 학교 전체에서 공통으로 사용하는 정보에 가까워 독립 테이블로 관리했습니다.
- 일정 유형을 저장해 특정 유형의 일정 필터링(예: 대학생, 대학원생 등)이 가능하도록 했습니다.

<br><br>

### 2. 제한된 리소스 환경에서의 인스턴스 분리

**문제**

- 서비스 초기에 GCP 프리티어 단일 인스턴스에서 API 서버, 스크래핑 작업, 모니터링을 함께 운영했습니다.
- 스크래핑 작업이나 메트릭 수집 시 API 응답이 눈에 띄게 느려지는 문제가 발생했습니다.

**원인**

- 배치 작업이 CPU와 메모리를 많이 사용하면서 API 서버가 사용할 자원이 부족해졌습니다.
- 메모리가 부족해지면서 스왑 메모리를 사용하게 되었고, 이로 인해 디스크 I/O가 증가했습니다.
- 여러 작업이 동시에 실행되면서 컨텍스트 스위칭이 증가해 전체적인 성능이 저하되었습니다.

<img width="60%" alt="image" src="https://github.com/user-attachments/assets/a6b9531e-9ee7-43c0-9303-7ff5072c34bf" />

**해결**

- 문제를 해결하기 위해 역할에 따라 인스턴스를 분리하는 구조로 개선했습니다.
1. API 서버 전용 인스턴스
2. 스크래핑 작업 전용 인스턴스
3. 모니터링 전용 인스턴스
- 구글 계정 2개를 사용해 프리티어 인스턴스를 추가로 확보하여, 비용을 늘리지 않으면서도 작업을 나눠서 실행할 수 있었습니다.
- 스크래핑 작업은 Github Actions Runner를 활용하여 주기적으로 실행되도록 구성했습니다.

<img width="625" height="456" alt="image" src="https://github.com/user-attachments/assets/443aad76-76d1-4287-88e4-c9169a1aba75" />

**결과**

- 리소스 경쟁이 줄어들면서 전체 시스템의 성능이 개선되었습니다.
- 서비스 간 장애 전파 가능성을 줄였습니다.
- 수직 확장(서버 스펙 증가)이나 수평 확장(인스턴스 추가)을 하지 않고도 문제를 해결할 수 있었습니다.
- 결과적으로 성능 개선과 비용 절감을 동시에 달성할 수 있었습니다.

**트레이드오프**

- 배포 대상과 운영 지점이 늘어나 환경 변수, 네트워크, 모니터링 설정을 더 꼼꼼히 관리해야 했습니다.

**느낀 점**

- 인프라 자원은 정말 비싸다는 것을 느꼈습니다.
- 프리티어 자원을 최대한으로 활용하면 비용 지출 없이도 충분히 서비스를 운영할 수 있다는 것을 배웠습니다.

<br><br>

### 3. 교내 데이터 수집 방식 분리

**문제**

- 교내 페이지를 HTTP 요청 기반으로 수집했을 때 응답은 정상이어도 실제 데이터가 비어 있는 경우가 있었습니다.
- 동일한 페이지를 브라우저에서는 정상적으로 확인할 수 있었지만, 코드로 수집할 경우 데이터가 누락되는 문제가 발생했습니다.

**원인**

- 일부 페이지는 서버에서 완성된 HTML을 반환하는 SSR 방식이 아니라, JavaScript 실행 이후 데이터가 렌더링되는 CSR 구조였습니다.
- 따라서 단순 HTTP 요청으로는 초기 HTML만 받아오게 되어 실제 데이터가 포함되지 않았습니다.
- 브라우저에서는 JavaScript가 실행되면서 추가 API 요청을 통해 데이터를 가져오고 화면에 렌더링되기 때문에 차이가 발생했습니다.

<table>
<tr>
<td align="center">
<img src="https://github.com/user-attachments/assets/a0d2b371-f61a-4b64-bf09-b387eed6ae6c" width="250"/><br/>
<sub>SSR</sub>
</td>
<td align="center">
<img src="https://github.com/user-attachments/assets/80b859da-e6ca-4c63-b4e9-e0885c676e85" width="250"/><br/>
<sub>CSR</sub>
</td>
</tr>
</table>

**해결**

- 페이지 렌더링 방식에 따라 수집 방식을 분리했습니다.
1. SSR 페이지 → HTTP 요청 기반 수집 유지
2. CSR 페이지 → Selenium 기반 브라우저 렌더링 수집 적용
- 모든 페이지를 Selenium으로 처리하는 대신, 필요한 경우에만 선택적으로 적용하도록 설계했습니다.
- 이를 위해 페이지 구조를 분석하여 CSR 여부를 판단하는 기준을 정리하고, 수집 로직을 분기 처리했습니다.

<img width="789" height="460" alt="image" src="https://github.com/user-attachments/assets/d3de9343-feba-4ee5-a7b1-7226ec88d0c6" />

<br>

**결과**

- 데이터가 비어 오는 문제를 해결하고 수집 정확도를 크게 개선할 수 있었습니다.
- 필요한 페이지에만 Selenium을 적용하여 실행 시간과 리소스 사용량을 최소화할 수 있었습니다.

**느낀 점**

- 데이터를 잘 수집하기 위해서는 웹 통신 과정에 대한 이해가 꼭 필요하다는 것을 느꼈습니다.
- 특히 네트워크 탭을 통해 실제 API 요청 흐름을 분석하면서 문제를 해결할 수 있었던 경험이 인상 깊었습니다.
- 이 경험을 통해 기능 구현 전에 동작 원리를 먼저 이해하려는 습관이 중요하다는 것을 배웠습니다.

<br><br>

### 4. 수집 지연 감지를 위한 모니터링

**문제**

- 학식, 셔틀버스, 공지사항과 같은 데이터 수집이 지연되더라도 즉시 인지하기 어려웠습니다.
- 사용자가 오래된 데이터를 확인하기 전까지는 문제가 드러나지 않아 대응이 늦어지는 상황이 발생했습니다.

**원인**

- 기존에는 서버가 살아 있는지만 확인하고 있었습니다.
- 하지만 실제로 중요한 "데이터가 언제 마지막으로 갱신되었는지"는 확인하지 않고 있었습니다.
- 이로 인해 수집이 실패하거나 지연되더라도 바로 알 수 없는 구조였습니다.

**해결**

- Prometheus, Grafana, Sentry를 활용해 모니터링 환경을 구성했습니다.
- 데이터 수집 지연 감지는 PostgreSQL에 저장된 마지막 수집 시간과 현재 시간의 차이를 확인하는 쿼리로 체크했습니다.
- 해당 값을 Grafana 대시보드로 시각화하여, 데이터가 얼마나 지연되고 있는지 한눈에 확인할 수 있도록 했습니다.

<img width="70%" alt="image" src="https://github.com/user-attachments/assets/9819c68d-6664-4a7f-969f-e70ee26edceb" />

**결과**

- 실제로 셔틀버스 데이터가 약 156분 지연된 문제를 대시보드를 통해 빠르게 확인할 수 있었습니다.
- 원인을 확인한 결과 Github Actions의 cron 설정 문제였고, 실행 시간을 조정하여 해결했습니다.
- 이후에는 데이터 수집 지연을 빠르게 인지하고 대응할 수 있게 되었습니다.
- 다만 데이터 구조가 바뀔 때마다 모니터링 쿼리와 대시보드를 함께 수정해야 하는 번거로움이 있었습니다.

<img width="1301" height="224" alt="image" src="https://github.com/user-attachments/assets/2f066c7c-960c-483d-ba5a-313f9dd31a3f" />

**느낀 점**

- [모니터링을 공부](https://dongho-blog.kro.kr/posts/3145ca9b-265e-8028-9953-f659ea1a9c67)하면서 사용자가 알기 전에 문제를 발견할 수 있도록 하는 것이 서비스 운영에서 매우 중요하다고 느꼈습니다.
- 특히 p95 응답 속도, 에러율 같은 지표를 통해 사용자 경험에 직접적인 영향을 주는 요소를 기준으로 서비스를 바라봐야 한다는 것을 배웠습니다.

---

## 🔷 팀원 소개
Expand Down
Loading
Loading