앱 서버가 죽어도 점검 화면은 떠야 한다 - 게이트웨이 기반 서비스 점검 모드 설계
서비스를 운영하다 보면 DB 마이그레이션, 서버 교체, 대규모 배포 등의 이유로 일정 시간 동안 사용자 요청을 차단해야 할 때가 있다. 가장 먼저 떠올릴 수 있는 방법은 애플리케이…
서비스를 운영하다 보면 DB 마이그레이션, 서버 교체, 대규모 배포 등의 이유로 일정 시간 동안 사용자 요청을 차단해야 할 때가 있다.
가장 먼저 떠올릴 수 있는 방법은 애플리케이션 서버에서 점검 상태를 확인하고, 점검 중이라면 503 Service Unavailable 응답을 반환하는 것이다.
예를 들어 Spring Boot에서는 필터나 인터셉터를 사용해 다음과 같이 구현할 수 있다.
1@Component2public class MaintenanceInterceptor implements HandlerInterceptor {3
4 private final MaintenanceService maintenanceService;5
6 public MaintenanceInterceptor(7 MaintenanceService maintenanceService8 ) {9 this.maintenanceService = maintenanceService;10 }11
12 @Override13 public boolean preHandle(14 HttpServletRequest request,15 HttpServletResponse response,16 Object handler17 ) throws IOException {18
19 if (!maintenanceService.isMaintenanceMode()) {20 return true;21 }22
23 response.setStatus(24 HttpServletResponse.SC_SERVICE_UNAVAILABLE25 );26 response.setContentType("application/json");27 response.setCharacterEncoding("UTF-8");28
29 response.getWriter().write("""30 {31 "code": "MAINTENANCE",32 "message": "현재 서비스 점검 중입니다.",33 "end_at": "2026-07-25T03:00:00+09:00"34 }35 """);36
37 return false;38 }39}이 방식은 구현하기 쉽고, 관리자가 데이터베이스나 관리 API를 통해 점검 모드를 변경할 수도 있다.
하지만 여기에는 중요한 문제가 있다.
애플리케이션 서버가 죽어 있으면 점검 응답도 반환할 수 없다.
점검 중 Spring Boot 프로세스를 종료하거나 전체 컨테이너를 재배포한다면, 사용자는 점검 안내가 아니라 연결 실패나 타임아웃을 보게 된다.
따라서 서비스 전체를 차단하는 점검 기능은 애플리케이션 서버보다 앞단에 있는 게이트웨이 계층에서 처리하는 것이 더 안전하다.
1. 점검 모드를 애플리케이션에 두면 생기는 문제
애플리케이션에서 점검 모드를 처리하는 구조는 다음과 같다.
1Client2 ↓3Gateway4 ↓5Application6 ↓7MaintenanceInterceptor8 ↓9503 MAINTENANCE점검 응답을 만들기 위해 반드시 애플리케이션까지 요청이 도달해야 한다.
따라서 다음과 같은 작업이 진행되면 점검 응답 자체를 반환할 수 없다.
- 애플리케이션 프로세스 종료
- 전체 컨테이너 또는 Pod 재배포
- 애플리케이션 기동 실패
- JVM 장애
- DB 연결 실패로 인한 요청 처리 불가
- 잘못된 설정으로 인한 서버 부팅 실패
- 네트워크 정책 변경으로 인한 앱 서버 접근 실패
실제 점검은 애플리케이션이 정상적으로 살아 있는 상태에서만 진행되는 것이 아니다.
오히려 서버를 내리거나 데이터베이스 구조를 변경하기 위해 점검을 수행하는 경우가 많다.
따라서 전면 점검 기능이 애플리케이션 서버에 의존하면 점검 목적과 구현 위치가 서로 충돌한다.
2. 게이트웨이 계층에서 요청을 차단한다
권장 구조는 Nginx, Ingress Gateway, API Gateway 또는 Load Balancer처럼 애플리케이션보다 앞에 있는 계층에서 요청을 차단하는 것이다.
1Client2 ↓3Gateway4 ├─ 정상 모드 → Application5 └─ 점검 모드 → 503 MAINTENANCE점검 모드가 활성화되면 게이트웨이는 요청을 애플리케이션으로 전달하지 않고 즉시 응답한다.
1HTTP/1.1 503 Service Unavailable2Content-Type: application/json3Cache-Control: no-store4Retry-After: 18001{2 "code": "MAINTENANCE",3 "message": "서비스 안정화를 위한 점검이 진행 중입니다.",4 "start_at": "2026-07-25T01:00:00+09:00",5 "end_at": "2026-07-25T03:00:00+09:00"6}이 구조에서는 애플리케이션 서버가 종료되어 있어도 게이트웨이가 살아 있다면 점검 안내를 반환할 수 있다.
또한 요청이 애플리케이션까지 전달되지 않으므로 점검 중 데이터 변경 요청이 들어오는 것도 방지할 수 있다.
3. 왜
503 Service Unavailable
을 사용해야 할까
점검 응답을 200 OK로 반환하는 구현도 볼 수 있지만, 운영 관점에서는 적절하지 않다.
1HTTP/1.1 200 OK1{2 "code": "MAINTENANCE"3}HTTP 상태만 보면 요청이 성공한 것처럼 보이기 때문이다.
이 경우 다음과 같은 문제가 발생할 수 있다.
- 모니터링 시스템이 서비스를 정상 상태로 판단한다.
- API 성공률 통계가 왜곡된다.
- 클라이언트 성공 처리 로직이 실행될 수 있다.
- 프록시나 CDN이 정상 응답으로 캐싱할 수 있다.
- 장애 감지가 늦어질 수 있다.
점검 중에는 서버가 의도적으로 요청을 처리할 수 없는 상태이므로 503 Service Unavailable이 의미상 적절하다.
1HTTP/1.1 503 Service Unavailable2Retry-After: 1800Retry-After 헤더를 사용하면 클라이언트에게 재시도 시점을 안내할 수도 있다.
값은 초 단위로 지정할 수 있다.
1Retry-After: 1800또는 HTTP 날짜 형식으로 지정할 수도 있다.
1Retry-After: Sat, 25 Jul 2026 03:00:00 GMT다만 모바일 앱이 Retry-After 값에 맞춰 모든 기기에서 동시에 재시도하면 점검 종료 직후 트래픽이 몰릴 수 있다.
따라서 자동 재시도를 구현한다면 지수 백오프와 랜덤 지연을 함께 적용하는 것이 좋다.
4. Nginx에서 점검 응답 반환하기
가장 단순한 구현은 Nginx 설정에서 바로 503을 반환하는 것이다.
1server {2 listen 443 ssl;3 server_name api.example.com;4
5 location / {6 default_type application/json;7
8 add_header Cache-Control "no-store" always;9 add_header Retry-After "1800" always;10
11 return 503 '{12 "code": "MAINTENANCE",13 "message": "현재 서비스 점검 중입니다.",14 "start_at": "2026-07-25T01:00:00+09:00",15 "end_at": "2026-07-25T03:00:00+09:00"16 }';17 }18}점검 시작 시 설정을 변경하고 Nginx를 reload한다.
1nginx -t && nginx -s reload여기서 반드시 nginx -t로 설정 문법을 먼저 검증해야 한다.
검증하지 않고 reload하면 잘못된 설정으로 인해 게이트웨이 자체에 문제가 발생할 수 있다.
운영에서는 정상 설정과 점검 설정을 별도로 관리하는 방법이 안전하다.
1/etc/nginx/conf.d/2├── app-normal.conf3├── app-maintenance.conf4└── app-enabled.conf점검 시작 시 심볼릭 링크를 점검 설정으로 변경한다.
1ln -sfn \2 /etc/nginx/conf.d/app-maintenance.conf \3 /etc/nginx/conf.d/app-enabled.conf4
5nginx -t && nginx -s reload점검 종료 시 정상 설정으로 되돌린다.
1ln -sfn \2 /etc/nginx/conf.d/app-normal.conf \3 /etc/nginx/conf.d/app-enabled.conf4
5nginx -t && nginx -s reload이 정도 규모라면 자동화하지 않고 담당자가 점검 시작과 종료 시 직접 실행해도 된다.
다만 수동 작업이라도 명령어를 직접 타이핑하기보다는 검증이 포함된 스크립트로 관리하는 것이 안전하다.
1#!/usr/bin/env bash2
3set -euo pipefail4
5MODE="${1:-}"6
7NORMAL_CONF="/etc/nginx/conf.d/app-normal.conf"8MAINTENANCE_CONF="/etc/nginx/conf.d/app-maintenance.conf"9ENABLED_CONF="/etc/nginx/conf.d/app-enabled.conf"10
11case "$MODE" in12 on)13 TARGET="$MAINTENANCE_CONF"14 ;;15 off)16 TARGET="$NORMAL_CONF"17 ;;18 *)19 echo "Usage: $0 {on|off}"20 exit 121 ;;22esac23
24ln -sfn "$TARGET" "$ENABLED_CONF"25
26if nginx -t; then27 nginx -s reload28 echo "Maintenance mode: $MODE"29else30 echo "Nginx configuration validation failed."31 exit 132fi5. JSON을 Nginx 설정에 직접 작성해도 될까
간단한 서비스라면 설정 파일에 JSON을 직접 작성해도 된다.
하지만 다음 정보가 자주 변경된다면 별도의 정적 JSON 파일로 관리하는 것이 낫다.
- 점검 시작 시각
- 예상 종료 시각
- 점검 연장 여부
- 다국어 메시지
- 고객센터 URL
- 앱 업데이트 안내
- 점검 상세 설명
예를 들어 다음과 같은 JSON 파일을 준비할 수 있다.
1{2 "code": "MAINTENANCE",3 "message": {4 "ko": "현재 서비스 점검 중입니다.",5 "en": "The service is currently under maintenance."6 },7 "start_at": "2026-07-25T01:00:00+09:00",8 "end_at": "2026-07-25T03:00:00+09:00",9 "status_url": "https://status.example.com"10}그리고 Nginx가 이 파일을 반환하게 만든다.
1location = /maintenance.json {2 root /var/www/system;3 default_type application/json;4
5 add_header Cache-Control "no-store" always;6}7
8location / {9 error_page 503 =503 /maintenance.json;10 return 503;11}실제 운영 환경에서는 내부 리다이렉트 결과가 반드시 503으로 유지되는지 확인해야 한다.
설정 방식에 따라 정적 파일 응답이 200으로 변경될 수 있기 때문에 배포 전에 다음 항목을 검증해야 한다.
1curl -i https://api.example.com/api/users기대 결과는 다음과 같다.
1HTTP/1.1 503 Service Unavailable2Content-Type: application/json3Cache-Control: no-store6. 클라이언트가 점검 화면을 직접 그린다
모바일 앱이나 SPA에서는 게이트웨이가 HTML 점검 페이지를 내려주기보다, 상태 코드와 에러 코드를 보고 클라이언트가 내장된 화면을 그리는 방식이 좋다.
1503 + MAINTENANCE2 ↓3앱 내부 전면 점검 화면이 방식의 장점은 다음과 같다.
- 앱 디자인과 동일한 점검 화면을 제공할 수 있다.
- 다국어 처리가 쉽다.
- 앱 버전별 맞춤 안내가 가능하다.
- 네이티브 버튼과 링크를 사용할 수 있다.
- 서버가 HTML을 렌더링할 필요가 없다.
안드로이드 클라이언트에서는 다음처럼 처리할 수 있다.
1data class ApiErrorResponse(2 val code: String?,3 val message: String?,4 val startAt: String?,5 val endAt: String?,6 val statusUrl: String?7)1suspend fun <T> handleResponse(2 response: Response<T>3): T {4 if (response.isSuccessful) {5 return requireNotNull(response.body())6 }7
8 val errorBody = parseErrorBody(response)9
10 if (11 response.code() == 503 &&12 errorBody?.code == "MAINTENANCE"13 ) {14 throw MaintenanceException(15 message = errorBody.message,16 startAt = errorBody.startAt,17 endAt = errorBody.endAt18 )19 }20
21 throw ApiException(22 status = response.code(),23 code = errorBody?.code24 )25}화면 계층에서는 예외를 받아 점검 화면으로 전환한다.
1when (throwable) {2 is MaintenanceException -> {3 navigator.navigate(4 MaintenanceRoute(5 message = throwable.message,6 startAt = throwable.startAt,7 endAt = throwable.endAt8 )9 )10 }11
12 else -> {13 showTemporaryError()14 }15}7. 모든
503
을 점검으로 처리하면 안 된다
여기서 중요한 점이 있다.
503 Service Unavailable은 계획 점검에서만 발생하는 상태 코드가 아니다.
다음 상황에서도 503이 반환될 수 있다.
- 업스트림 애플리케이션 전체 장애
- 서버 과부하
- 정상 인스턴스가 하나도 없는 상태
- 배포 중 일시적인 대상 제거
- 요청 제한 또는 서킷 브레이커 동작
- 게이트웨이 내부 정책에 의한 차단
따라서 상태 코드만 보고 점검 화면을 띄우면 안 된다.
1// 잘못된 처리2if (response.code() == 503) {3 showMaintenanceScreen()4}HTTP 상태와 비즈니스 에러 코드를 함께 확인해야 한다.
1if (2 response.code() == 503 &&3 errorBody.code == "MAINTENANCE"4) {5 showMaintenanceScreen()6}오류는 다음처럼 분류하는 것이 좋다.
1503 + MAINTENANCE2→ 계획된 점검 화면3
4503 + OVER_CAPACITY5→ 서비스 혼잡 화면6
7502 / 5048→ 서버 연결 장애 화면9
10Timeout / DNS / Offline11→ 사용자 네트워크 확인 화면12
13기타 5xx14→ 일시적인 서비스 오류 화면8. 점검 화면과 일반 장애 화면은 달라야 한다
계획 점검과 비계획 장애는 사용자에게 다른 메시지를 보여줘야 한다.
계획 점검
1서비스 안정화를 위한 점검이 진행 중입니다.2
3예상 종료 시각42026년 7월 25일 오전 3시5
6완료되는 대로 서비스를 다시 이용하실 수 있습니다.서버 장애
1현재 서비스 연결이 원활하지 않습니다.2
3잠시 후 다시 시도해주세요.4문제가 계속되면 고객센터로 문의해주세요.사용자 네트워크 장애
1인터넷 연결을 확인해주세요.2
3Wi-Fi 또는 모바일 데이터 연결 상태를 확인한 후4다시 시도해주세요.서버가 점검 응답을 반환하지 못하는 최악의 상황에서도 앱은 자체적으로 일반 장애 화면을 보여줄 수 있어야 한다.
9. 게이트웨이 서버 자체가 죽으면 어떻게 할까
Nginx 한 대만 운영한다면 Nginx가 단일 장애점이 된다.
1Client2 ↓3Nginx 한 대4 ↓5Application이 구조에서 Nginx가 죽으면 점검 JSON도 반환할 수 없다.
클라이언트에는 다음과 같은 오류가 보일 수 있다.
- Connection refused
- Connection timeout
- TLS 연결 실패
- 502 Bad Gateway
- 504 Gateway Timeout
따라서 운영 서비스에서는 게이트웨이를 다중화하는 것이 일반적이다.
1 ┌─ Nginx A ─ Application2Load Balancer ───┤3 └─ Nginx B ─ Application한쪽 Nginx가 비정상이라면 로드밸런서가 해당 인스턴스를 제외한다.
더 나아가 클라우드 환경에서는 관리형 로드밸런서 자체에서 점검 응답을 반환할 수도 있다.
1Client2 ↓3Managed Load Balancer4 ├─ 정상 모드 → Nginx → Application5 └─ 점검 모드 → Fixed 503 Response이 경우 Nginx와 애플리케이션이 모두 내려가더라도 관리형 로드밸런서가 점검 응답을 반환할 수 있다.
10. CDN 또는 Edge 계층에서 처리하는 방법
더 높은 가용성이 필요하다면 CDN이나 Edge 계층에서 점검 응답을 반환할 수 있다.
1Client2 ↓3CDN / Edge4 ↓5Load Balancer6 ↓7Nginx8 ↓9Application점검 응답용 JSON이나 HTML은 애플리케이션 서버와 다른 저장소에 둬야 한다.
1서비스 Origin2- Load Balancer3- Nginx4- Spring Boot5
6점검 안내 Origin7- 별도 Object Storage8- maintenance.json9- maintenance.html점검 안내 파일이 애플리케이션과 같은 서버에 있다면 애플리케이션 서버 장애 시 해당 파일에도 접근할 수 없다.
따라서 장애 안내 리소스는 가능한 한 서비스 본체와 장애 영역을 분리하는 것이 좋다.
11. 게이트웨이까지 전부 응답하지 못하면?
완벽하게 죽지 않는 서버는 없다.
다음 계층 중 하나라도 문제가 생길 수 있다.
- DNS
- CDN
- Load Balancer
- Nginx
- TLS 인증서
- 클라우드 리전
- 통신사 네트워크
- 사용자의 Wi-Fi
- 모바일 데이터 연결
따라서 클라이언트에는 최종 fallback 화면이 필요하다.
1명시적인 MAINTENANCE 응답 수신2→ 계획 점검 화면3
4서버 응답은 있지만 502·503·5045→ 일시적인 서버 장애 화면6
7서버 응답 자체가 없음8→ 네트워크 또는 서비스 연결 장애 화면즉, 게이트웨이 점검 모드는 높은 확률로 정확한 점검 화면을 보여주기 위한 수단이지, 모든 장애 상황에서 점검 상태를 전달할 수 있다는 보장은 아니다.
12. 헬스 체크 경로는 따로 관리해야 한다
점검 모드에서 모든 요청을 차단하면 로드밸런서 헬스 체크까지 503을 받을 수 있다.
그러면 로드밸런서는 Nginx 자체가 죽었다고 판단하고 모든 게이트웨이를 대상에서 제거할 수 있다.
따라서 게이트웨이 상태 확인용 경로는 점검 중에도 열어두는 것이 좋다.
1location = /gateway-health {2 access_log off;3 default_type application/json;4
5 return 200 '{"status":"UP"}';6}사용자 요청에는 503을 반환한다.
1location / {2 default_type application/json;3
4 return 503 '{5 "code": "MAINTENANCE",6 "message": "현재 서비스 점검 중입니다."7 }';8}이렇게 하면 두 상태를 구분할 수 있다.
1/gateway-health2→ Nginx 프로세스가 살아 있는가3
4/api/**5→ 사용자가 서비스를 이용할 수 있는가운영 모니터링에서도 다음 지표를 분리하는 것이 좋다.
1Infrastructure Health2- Gateway process3- Load balancer target4- TLS certificate5- CPU and memory6
7Service Availability8- 사용자 API 정상 응답 여부9- 점검 모드 여부10- 주요 기능 정상 동작 여부13. 모든 API를 막을 필요는 없다
전면 점검 중에도 일부 경로는 허용할 수 있다.
예를 들면 다음과 같다.
- 앱 버전 확인
- 시스템 상태 조회
- 공지사항
- 고객센터 정보
- 점검 상태 조회
- 로그아웃
- 정적 리소스
1location = /api/system/status {2 proxy_pass http://application;3}4
5location = /api/app/version {6 proxy_pass http://application;7}8
9location /api/ {10 default_type application/json;11
12 return 503 '{13 "code": "MAINTENANCE",14 "message": "현재 서비스 점검 중입니다."15 }';16}하지만 애플리케이션 서버를 완전히 종료한다면 허용한 API도 사용할 수 없다.
이런 정보가 점검 중에도 반드시 필요하다면 별도의 정적 파일이나 경량 상태 서버에서 제공해야 한다.
14. 관리자와 내부 트래픽 예외 처리
점검 중에도 운영자가 실제 서버 상태를 확인해야 할 수 있다.
이 경우 내부 IP나 VPN을 통해서만 애플리케이션 접근을 허용할 수 있다.
1location /internal/ {2 allow 10.0.0.0/8;3 deny all;4
5 proxy_pass http://application;6}또는 특정 헤더를 사용하는 방법도 생각할 수 있다.
1map $http_x_maintenance_bypass $maintenance_bypass {2 default 0;3 "secret-value" 1;4}하지만 고정된 비밀 헤더는 유출 가능성이 있고 로그에 남을 수 있으므로 권장하기 어렵다.
가능하면 다음 방법을 우선하는 것이 좋다.
- VPN 내부 접근
- 관리망 IP 허용
- 별도 내부 도메인
- 인증된 운영자 전용 게이트웨이
- Bastion 또는 사내 네트워크 이용
15. 점검 응답 캐싱에 주의해야 한다
점검 중 반환한 503 응답이 CDN이나 프록시에 캐싱되면 점검이 끝난 뒤에도 사용자가 계속 점검 화면을 볼 수 있다.
따라서 점검 응답에는 캐시 방지 헤더를 넣는 것이 좋다.
1Cache-Control: no-store, no-cache, must-revalidate2Pragma: no-cache3Expires: 0Nginx에서는 다음처럼 설정할 수 있다.
1add_header Cache-Control \2 "no-store, no-cache, must-revalidate" always;3
4add_header Pragma "no-cache" always;5add_header Expires "0" always;CDN에서 오류 응답을 캐싱하도록 구성했다면 점검 종료 절차에 다음 작업을 포함해야 한다.
- Error caching TTL 확인
- 점검 JSON 캐시 무효화
- CDN invalidation
- 외부 네트워크에서 정상 응답 확인
16. 점검 종료 시각은 신중하게 제공한다
사용자에게 종료 시각을 안내하는 것은 좋지만, 해당 시각을 지키지 못하면 오히려 신뢰를 잃을 수 있다.
종료 시각이 확실하지 않다면 null로 내려주는 것도 방법이다.
1{2 "code": "MAINTENANCE",3 "message": "현재 서비스 점검 중입니다.",4 "end_at": null5}클라이언트에서는 값이 있을 때만 표시한다.
1if (maintenance.endAt != null) {2 showEstimatedEndTime(maintenance.endAt)3} else {4 hideEstimatedEndTime()5}점검이 연장되었다면 안내 메시지를 갱신할 수 있어야 한다.
1{2 "code": "MAINTENANCE",3 "message": "점검 작업이 예상보다 길어지고 있습니다.",4 "end_at": "2026-07-25T04:00:00+09:00",5 "extended": true6}17. 점검 모드와 읽기 전용 모드는 다르다
모든 점검에서 전체 서비스를 차단할 필요는 없다.
DB 마이그레이션이나 데이터 정합성 작업에 따라 읽기 요청은 허용하고 쓰기 요청만 차단할 수 있다.
1GET 요청2→ 허용3
4POST / PUT / PATCH / DELETE5→ 503 또는 423 응답Nginx에서는 요청 메서드를 기준으로 차단할 수 있다.
1location /api/ {2 if ($request_method !~ ^(GET|HEAD|OPTIONS)$) {3 return 503 '{4 "code": "READ_ONLY_MAINTENANCE",5 "message": "점검 중에는 조회만 가능합니다."6 }';7 }8
9 proxy_pass http://application;10}다만 Nginx의 if 사용에는 주의가 필요하며, 복잡한 정책은 map이나 별도의 location 구성을 활용하는 편이 좋다.
읽기 전용 점검은 사용자 경험을 개선할 수 있지만 다음 조건이 필요하다.
- 읽기 요청이 DB 변경 없이 안전해야 한다.
- 오래된 데이터를 보여줘도 문제가 없어야 한다.
- 캐시나 복제본을 활용할 수 있어야 한다.
- 조회 과정에서 쓰기 작업이 발생하지 않아야 한다.
- 로그인 세션 갱신처럼 숨겨진 쓰기가 없어야 한다.
18. 장기 작업은 점검 전에 차단해야 한다
점검 모드를 켜는 순간 신규 요청은 차단할 수 있지만, 이미 처리 중인 요청은 남아 있을 수 있다.
예를 들어 다음 작업이 진행 중일 수 있다.
- 결제 승인
- 파일 업로드
- 대용량 데이터 처리
- 예약 생성
- 배치 작업
- 메시지 큐 소비
- 외부 API 연동
따라서 점검 절차에는 요청 드레이닝이 필요하다.
11. 신규 쓰기 요청 차단22. 진행 중인 요청 완료 대기33. 메시지 소비 중단44. 배치 작업 상태 확인55. 애플리케이션 종료66. DB 또는 인프라 작업 수행쿠버네티스 환경이라면 readiness probe를 먼저 실패시키고, 기존 연결이 정리될 시간을 확보한 뒤 Pod를 종료하는 방식을 사용할 수 있다.
애플리케이션에서도 graceful shutdown 설정을 적용하는 것이 좋다.
1server:2 shutdown: graceful3
4spring:5 lifecycle:6 timeout-per-shutdown-phase: 30s게이트웨이의 점검 모드는 요청 유입을 막는 기능이고, 이미 진행 중인 트랜잭션을 안전하게 종료해주는 기능은 아니다.
19. WebSocket과 SSE도 고려해야 한다
일반 HTTP 요청만 막아도 기존 WebSocket이나 SSE 연결은 계속 유지될 수 있다.
1점검 모드 활성화2 ↓3신규 HTTP 요청 차단4 ↓5기존 WebSocket 연결은 계속 살아 있음실시간 서비스를 운영한다면 다음 정책을 별도로 정해야 한다.
- 점검 시작 전에 연결 종료 메시지 전송
- 특정 close code 사용
- 클라이언트 재연결 중단
- 점검 종료 예정 시각 전달
- SSE 재연결 간격 증가
클라이언트가 연결 종료 후 무한 재접속하면 점검 중 게이트웨이에 불필요한 트래픽이 계속 발생할 수 있다.
따라서 점검 코드가 전달되면 일정 시간 동안 자동 재연결을 중지해야 한다.
20. 구버전 앱도 고려해야 한다
새 버전 앱은 MAINTENANCE 코드를 이해하지만, 구버전 앱은 해당 코드를 모를 수 있다.
따라서 클라이언트는 알 수 없는 서버 오류도 안전하게 처리해야 한다.
1when {2 status == 503 &&3 code == "MAINTENANCE" -> {4 showMaintenanceScreen()5 }6
7 status in 500..599 -> {8 showTemporaryServiceError()9 }10
11 else -> {12 showGenericError()13 }14}서버에서도 오래된 앱을 고려해 message 필드를 함께 내려주는 것이 좋다.
1{2 "code": "MAINTENANCE",3 "message": "현재 서비스 점검 중입니다.",4 "end_at": "2026-07-25T03:00:00+09:00"5}21. 인증 실패와 점검 응답의 우선순위
점검 중 만료된 토큰으로 요청하면 어떤 응답을 반환해야 할까?
1401 Unauthorized2또는3503 Maintenance전면 점검이라면 일반적으로 인증보다 점검 응답을 먼저 반환하는 편이 사용자 경험에 좋다.
그렇지 않으면 앱이 점검 중에도 토큰 갱신을 시도하고, 로그인 화면으로 이동하거나 반복 요청을 발생시킬 수 있다.
1Client2 ↓3Gateway maintenance check4 ├─ 점검 중 → 503 MAINTENANCE5 └─ 정상 → Authentication다만 관리자 전용 API나 내부 운영 API는 별도 인증과 우회 정책을 적용해야 한다.
22. CORS 헤더도 놓치면 안 된다
웹 프론트엔드가 별도 도메인에서 API를 호출한다면 점검 응답에도 CORS 헤더가 있어야 한다.
정상 요청은 애플리케이션이 CORS 헤더를 추가하지만, 점검 응답은 Nginx가 직접 반환하기 때문에 헤더가 빠질 수 있다.
1add_header Access-Control-Allow-Origin \2 "https://app.example.com" always;3
4add_header Access-Control-Allow-Credentials \5 "true" always;6
7add_header Access-Control-Allow-Headers \8 "Authorization, Content-Type" always;9
10add_header Access-Control-Allow-Methods \11 "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;CORS 헤더가 없으면 브라우저에서 응답 본문을 읽지 못하고 단순 네트워크 오류처럼 처리될 수 있다.
특히 always 옵션을 사용하지 않으면 503 응답에 헤더가 붙지 않을 수 있으므로 주의해야 한다.
23. OPTIONS 요청 처리도 필요하다
브라우저는 실제 API 호출 전에 CORS preflight 요청을 보낼 수 있다.
1OPTIONS /api/orders점검 중 OPTIONS 요청도 503으로 막으면 브라우저가 실제 응답의 에러 코드를 읽지 못할 수 있다.
따라서 웹 클라이언트가 있다면 OPTIONS 요청은 정상 응답을 주고, 실제 요청에서 점검 응답을 반환하는 방식도 고려할 수 있다.
1if ($request_method = OPTIONS) {2 add_header Access-Control-Allow-Origin \3 "https://app.example.com" always;4
5 add_header Access-Control-Allow-Methods \6 "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;7
8 add_header Access-Control-Allow-Headers \9 "Authorization, Content-Type" always;10
11 return 204;12}모바일 앱만 사용하는 서비스라면 CORS 처리가 필요하지 않을 수 있다.
24. 점검 모드 자체를 모니터링해야 한다
점검 모드가 활성화되면 일반 장애 모니터링이 계속 울릴 수 있다.
1API 성공률 하락25xx 증가3헬스 체크 실패4사용자 요청 실패계획된 점검과 실제 장애를 구분할 수 있도록 모니터링에 점검 상태를 전달하는 것이 좋다.
예를 들면 다음과 같은 메트릭을 둘 수 있다.
1service_maintenance_mode{service="api"} 1점검 중에는 일부 알림을 억제하되, 다음 알림은 유지해야 한다.
- 게이트웨이 자체 장애
- 점검 종료 시각 초과
- 점검 모드 해제 실패
- 데이터베이스 작업 실패
- Nginx 설정 reload 실패
- CDN 또는 Load Balancer 오류
- 관리자 접근 경로 장애
점검 중이라고 모든 알림을 꺼버리면 실제 인프라 장애를 놓칠 수 있다.
25. 수동 점검 모드도 운영 절차가 필요하다
점검 시작과 종료를 수동으로 처리하는 것 자체는 문제가 아니다.
빈도가 낮고 담당자가 명확하다면 자동화보다 수동 전환이 더 단순하고 안전할 수도 있다.
다만 다음과 같은 운영 절차는 필요하다.
점검 시작 전
- 점검 응답 JSON 검토
- 시작·종료 시각 확인
- Nginx 설정 검증
- 점검 해제 명령 준비
- 관리자 접근 경로 확인
- 모바일 및 웹 화면 확인
- 모니터링 알림 정책 확인
- 진행 중 작업과 배치 상태 확인
점검 시작
- 신규 쓰기 요청 차단
- 진행 중 요청 종료 대기
- 점검 모드 활성화
- 외부 네트워크에서
503확인 - 앱의 점검 화면 확인
- 애플리케이션 요청 유입 중단 확인
점검 종료 전
- 애플리케이션 health check 확인
- 주요 API smoke test
- DB 마이그레이션 검증
- 메시지 큐와 배치 상태 확인
- 로그에서 반복 오류 확인
점검 종료
- 점검 모드 해제
- 외부 네트워크에서
200확인 - 앱 재진입 확인
- CDN 캐시 확인
- 모니터링 알림 정상화
- 점검 결과 기록
수동 작업에서 가장 흔한 문제는 점검 모드를 켜는 것이 아니라 끄는 것을 잊는 것이다.
따라서 종료 예정 시각이 지나도 점검 모드가 유지되면 알림을 보내도록 구성하는 것이 좋다.
26. 최종 권장 아키텍처
소규모 서비스라면 다음 구조로도 충분하다.
1Client2 ↓3Nginx4 ├─ 정상 모드 → Spring Boot5 └─ 점검 모드 → 503 JSON다만 Nginx 한 대가 단일 장애점이라는 한계가 있다.
일반적인 운영 환경에서는 다음 구조가 더 안전하다.
1Client2 ↓3Managed Load Balancer4 ↓5Nginx A / Nginx B6 ↓7Spring Boot Instances더 높은 가용성이 필요한 서비스라면 다음처럼 구성할 수 있다.
1Client2 ↓3CDN / Edge4 ↓5Managed Load Balancer6 ↓7Nginx or Kubernetes Ingress8 ↓9Spring Boot점검 응답을 어느 계층에서 반환할지는 서비스 규모와 장애 허용 범위에 따라 결정한다.
1Nginx2- 구현이 단순하다.3- 애플리케이션과 분리할 수 있다.4- 단일 인스턴스라면 장애점이 된다.5
6Managed Load Balancer7- 애플리케이션과 Nginx 장애에 영향을 덜 받는다.8- 관리형 고가용성을 활용할 수 있다.9- 복잡한 응답 형식에는 제약이 있을 수 있다.10
11CDN / Edge12- 가장 앞단에서 빠르게 응답할 수 있다.13- 원본 서버 전체 장애에도 대응하기 좋다.14- 캐싱과 설정 배포 정책을 신중하게 관리해야 한다.마무리
전면 서비스 점검 안내를 애플리케이션 서버에서만 처리하면 애플리케이션이 죽었을 때 점검 안내도 함께 사라진다.
따라서 계획된 전면 점검은 애플리케이션보다 앞에 있는 게이트웨이, 로드밸런서 또는 CDN 계층에서 처리하는 것이 안전하다.
핵심 원칙은 다음과 같다.
- 점검 응답은
503 Service Unavailable로 반환한다. MAINTENANCE같은 명시적인 에러 코드를 함께 제공한다.- 클라이언트는 상태 코드와 에러 코드를 함께 확인한다.
- 점검 화면은 클라이언트에 내장하고 응답 정보만 활용한다.
- 모든
503을 계획 점검으로 취급하지 않는다. - 게이트웨이는 다중화하거나 관리형 로드밸런서를 사용한다.
- 게이트웨이까지 실패하면 클라이언트의 일반 장애 화면을 사용한다.
- 점검 응답이 캐싱되지 않도록 관리한다.
- 헬스 체크와 내부 운영 경로는 별도로 설계한다.
- 진행 중 요청, WebSocket, 배치, 메시지 큐까지 함께 고려한다.
- 수동 전환이라도 검증과 복구 절차를 스크립트로 관리한다.
- 점검 모드의 종료 지연과 해제 실패도 모니터링한다.
점검 모드는 단순히 사용자에게 안내 문구를 보여주는 기능이 아니다.
점검 중 신규 트래픽을 안전하게 차단하고, 진행 중인 작업을 정리하며, 서버가 내려간 상태에서도 사용자에게 일관된 경험을 제공하기 위한 운영 장치다.
결국 중요한 것은 “점검 화면을 어디서 보여줄 것인가”보다 다음 질문에 답하는 것이다.
현재 점검 응답을 담당하는 계층이 죽더라도, 그보다 앞단에서 사용자를 보호할 수 있는가?
이 질문을 반복하면서 장애 영역을 한 계층씩 분리하는 것이 실제 운영 서비스의 점검 설계다.