NCP · 3-Tier Architecture
NAVER TENNIS CLUB — 시스템 구성도
NCP 에 구성한 웹·앱·DB 3계층 아키텍처.
로드밸런서가 트래픽을 분산하고, 웹 계층(nginx)이 앱 계층(Flask)으로 프록시하며,
모든 데이터는 관리형 Cloud DB에 저장됩니다.
도메인 https://navertennis.duckdns.org 🔒
VPC 10.0.0.0/16
리전 KR (한국)
앱 계층 이중화 ×2
① 사용자
웹 브라우저 · 모바일
👥 클럽 회원 (197명)
HTTP 요청 → navertennis.duckdns.org
랭킹 · 경기입력 · 대진편성 · 클럽현황 조회
🎨 외부 CDN
DiceBear · Twemoji
아바타 SVG를 브라우저가 직접 로드
HTTPS :443 (HTTP는 301 자동 이동)
② 로드밸런서
Network LB · Public · TCP 통과
⚖️ lab-nlb
Public IP 101.79.18.101
TCP:443 · TCP:80 그대로 통과 — TLS는 서버의 nginx가 종료
클라이언트 실제 IP 보존 · 라운드로빈 · 헬스체크 TCP (10초 주기)
Target Group → lnxsvr1 · lnxsvr2
분산
☁️ VPC · 10.0.0.0/16
web-subnet · 10.0.1.0/24
🛡️ NACL (서브넷 방화벽) + ACG (서버 방화벽)
③ 웹 계층 — nginx (🔒 TLS 종료 + 리버스 프록시)
🖥️ lnxsvr1 WEB+APP
nginx :443(TLS)·:80 · 사설 10.0.1.101
공인 101.79.26.198
proxy_pass → 127.0.0.1:5000
🖥️ lnxsvr2 WEB+APP
nginx :443(TLS)·:80 · 사설 10.0.1.102
공인 101.79.27.121
proxy_pass → 127.0.0.1:5000
↓ 각 서버의 nginx가 자기 서버의 Flask :5000으로 프록시 — 한 대가 죽어도 서비스 유지 ↓
④ 앱 계층 — Flask 애플리케이션
🎾 guestbook (Flask)
이중화 ×2 (Active-Active)
lnxsvr1 · lnxsvr2 각각 :5000 · systemd (Restart=always, enabled)
/opt/guestbook/app.py — 라우트·NTR 알고리즘·HTML 렌더링 일체형
📷 프로필 사진 업로드 → Object Storage ntc-uploads (public-read)
MySQL :3306
db-subnet · VPC Cloud DB
🛡️ ACG cloud-mysql-2dfqr5
⑤ 데이터 계층 — 관리형 DB
🗄️ Cloud DB for MySQL
db-48rail.vpc-cdb.ntruss.com : 3306
DB guestbook · 계정 appuser
테이블: players · matches · entries · settings · attendance
backup.sh
⑥ 백업
Object Storage (S3 호환)
💾 s3://ntc-backup
kr.object.ncloudstorage.com
DB 덤프(.sql.gz) + 앱 소스(.tar.gz)
🖼️ s3://ntc-uploads
선수 프로필 사진 (public-read)
앱이 boto3로 업로드 · 브라우저가 직접 로드
요청 처리 흐름
회원이 https://navertennis.duckdns.org 접속 → DuckDNS가 Network LB 공인 IP로 연결
아바타 이미지(DiceBear·Twemoji)는 서버를 거치지 않고 브라우저가 CDN에서 직접 로드
Network LB가 TCP를 그대로 통과시키며 정상 서버로 라운드로빈 분산 — 클라이언트 IP가 서버까지 보존됨
선택된 서버의 nginx(:443)가 TLS를 종료(암호화 해제)하고 proxy_pass로 앱 계층에 전달
HTTP(:80)로 오면 nginx가 301로 HTTPS 이동 · 각 서버가 자기 Flask(127.0.0.1:5000)로 프록시 — Active-Active 이중화
Flask(:5000)가 라우팅·NTR 계산 후 서버사이드 HTML 렌더링, 프로필 사진은 Object Storage에서 직접 로드
데이터 읽기/쓰기는 Cloud DB for MySQL(:3306)에 질의 → 결과를 페이지에 반영해 응답
backup.sh가 DB 덤프와 파일을 Object Storage에 주기 백업
Flask 앱 내부 구조 — 요청 한 건이 처리되는 과정
🚦
① URL 라우팅
주소를 보고 실행할 기능 연결
랭킹·경기입력·선수관리 등 12개 경로
→
🧮
② 비즈니스 로직
NTR 레이팅 계산 (Elo 방식)
대진 편성 · 궁합/천적 · 어워드 집계
→
🗄️
③ 데이터 처리
Cloud DB 읽기/쓰기 (테이블 5개)
사진은 Object Storage 저장
→
🖼️
④ 화면 생성
서버에서 HTML 완성 후 응답
(서버사이드 렌더링 · 프론트 분리 없음)
✏️ / 게시판
🏆 /tennis 2026 랭킹
📝 /tennis/games 경기입력·대진편성
👥 /tennis/players 선수관리
🎾 /tennis/matches 2025 기록
📊 /tennis/dashboard 클럽현황
👑 /tennis/awards 월간성적
📈 /tennis/p/<id> 선수상세
⚔️ /tennis/vs 선수비교
🏗️ /architecture 구성도
💓 /health LB 헬스체크
2단계 방화벽 (트래픽이 지나는 순서)
1. NACL — 서브넷 방화벽
서브넷 경계에서 먼저 걸러냅니다. 규칙에 우선순위가 있어 낮은 번호가 먼저 적용되며,
ACG보다 앞단에서 차단합니다. (실습 중 22번 SSH가 여기서 막혀 특정 IP만 허용 규칙을 추가한 이력)
2. ACG — 서버 방화벽
서버(NIC) 단위 보안그룹. 웹 서버는 lab1-web-acg,
Cloud DB는 cloud-mysql-2dfqr5가 적용되어
앱 계층(10.0.1.0/24)에서 오는 3306 포트만 DB로 통과시킵니다.
리소스 명세
| 구성요소 | 역할 | 사설 IP | 공인 IP / 엔드포인트 | 포트·서비스 |
| lab-nlb | Network LB (Public · TCP 통과) | — | 101.79.18.101 | TCP:443 · TCP:80 · 라운드로빈 |
| lnxsvr1 | 웹 + 앱 (nginx·Flask) | 10.0.1.101 | 101.79.26.198 | nginx :80 · Flask :5000 |
| lnxsvr2 | 웹 + 앱 (nginx·Flask) | 10.0.1.102 | 101.79.27.121 | nginx :80 · Flask :5000 |
| Cloud DB | MySQL (관리형) | VPC 내부 | db-48rail.vpc-cdb.ntruss.com | :3306 |
| Object Storage | 백업 + 사진 저장소 | — | kr.object.ncloudstorage.com | s3://ntc-backup · s3://ntc-uploads |
| Auto Scaling | 미사용 잔여 서버 | 10.0.2.6 | 없음 | 반납 시 AS그룹 먼저 제거 |
구조상 유의점 & 개선 여지
① 앱 계층 이중화 완료 (2026-07-26). lnxsvr1에도 Flask를 배치해 웹·앱 계층 모두
Active-Active 2중화. 업로드 사진도 Object Storage로 이전해 서버 간 파일 불일치 없음.
한 서버 장애 시 LB 헬스체크(30초×2)가 감지 후 나머지 서버로만 서빙합니다.
② HTTPS + 인증서 완전 자동 갱신 (2026-07-26). Network LB(TCP 통과) 전환으로 TLS를
서버 nginx에서 종료. Let's Encrypt 인증서는 lnxsvr2의 acme.sh가 HTTP 인증으로 60일마다 자동
재발급하고, 갱신 즉시 lnxsvr1로 자동 복사 + 양쪽 nginx 재적용 — 사람 개입 0. HTTP 접속은
nginx가 301로 HTTPS 이동.
무중단 장애조치 테스트 — 2026-07-26 실측
앱 계층 이중화 직후, lnxsvr2의 Flask를 실제로 중지시켜 LB가 장애를 감지하고
lnxsvr1 단독으로 서비스를 유지하는지 검증한 기록입니다.
| 시각 | 이벤트 | 결과 |
| 11:09:56 | 기준 확인 (3회) | 모두 200 |
| 11:09:56 | 🔥 lnxsvr2 Flask 중지 (장애 주입) | inactive |
| 11:10:02 | LB 감지 창 | 502 단 1회 |
| 11:10:07 ~ 11:12:24 | lnxsvr1 단독 서빙 (140초 관찰) | 연속 200 · 에러 0 |
| 장애 중 | 랭킹(DB 조회) · 게시판 실기능 확인 | 모두 200 — 완전한 서비스 |
| 11:13:40 | ✅ lnxsvr2 재기동 | active · health OK |
| ~11:15:10 | 재편입 관찰 (90초) | LB·직접 모두 200 · 무결점 복귀 |
결론: 장애 영향은 감지 창의 502 응답 1건뿐.
한쪽 서버 전체가 죽어도 반대편이 웹+앱 풀스택으로 서비스를 유지하며,
복구 시 LB가 헬스체크 2회 통과 후 자동으로 트래픽을 재분배한다 (사용자 영향 0건).