개요
백엔드 서버의 안정성은 크게 세 가지 축으로 나뉩니다. 첫째는 서버 프로세스가 종료될 때 진행 중인 작업을 안전하게 마무리하는 Graceful Shutdown(우아한 종료) 전략이며, 둘째는 외부 시스템과의 연동 및 메시지 큐 발행 시 트랜잭션 범위를 최적화하여 데이터 정합성과 시스템 가용성을 확보하는 트랜잭션 관리 전략입니다. 마지막으로, 오토스케일링 환경에서 발생하는 데이터베이스 커넥션 고갈을 방지하고 리소스를 효율적으로 제어하는 아키텍처 설계가 필수적입니다. 본 문서에서는 이 세 가지 핵심 운영 전략을 종합적으로 다룹니다.
Graceful Shutdown 전략
Graceful Shutdown은 서버가 종료 신호를 받았을 때 즉시 프로세스를 중단하지 않고, 현재 처리 중인 작업을 안전하게 마무리한 뒤 종료하는 운영 전략입니다. 이를 통해 서비스 가용성을 유지하고 데이터 유실이나 비정상적인 상태를 방지합니다.
HTTP 서버의 드레인(Drain) 과정
- 새 요청 유입 차단: 서버가 종료 신호를 받으면 새로운 HTTP 연결을 거부하거나, 로드 밸런서에서 해당 서버를 제외(Readiness Probe 실패 처리)시킵니다.
- 진행 중인 요청 처리: 이미 연결된 요청들이 완료될 때까지 일정 시간(Grace Period) 동안 대기합니다.
- 연결 종료: 대기 시간이 만료되거나 모든 요청이 처리되면 서버 프로세스를 종료합니다.
장기 연결(SSE) 환경의 아키텍처 설계
- 스레드 풀 고갈 방지: 스트리밍 전용 경로에 비동기 핸들러를 적용하여 요청 수신 즉시 서블릿 스레드를 풀에 반환해야 합니다.
- 인프라 최적화: Nginx 사용 시 버퍼링 옵션을 비활성화하고, HTTP/2 멀티플렉싱을 활성화하여 연결 효율을 높입니다.
- 세션 관리: 15초 주기의 하트비트를 설정하여 유휴 커넥션 및 좀비 세션을 정리합니다.
메시지 큐(Message Queue) 처리 전략
- Kafka: 종료 신호 수신 시 새로운 메시지 폴링을 중단하고, 현재 처리 중인 메시지의 로직을 완료한 뒤 오프셋(Offset)을 커밋하고 종료합니다.
- SQS: 가시성 제한 시간(Visibility Timeout)을 고려하여 처리 로직의 멱등성을 확보하고, 처리가 완료된 메시지는 즉시 삭제합니다.
Kubernetes 환경 운영
- SIGTERM 신호: Pod 종료 시 컨테이너에 신호가 전달됩니다.
- Readiness Probe: 종료 시작과 동시에 실패 상태로 변경하여 트래픽 유입을 차단합니다.
- PreStop Hook: 애플리케이션이 종료 신호를 처리할 시간을 추가로 확보합니다.
- Grace Period:
terminationGracePeriodSeconds설정을 통해 프로세스 종료 최대 시간을 지정합니다.
트랜잭션 및 외부 연동 안정성
트랜잭션 내에서 외부 API를 호출하거나 메시지 큐로 이벤트를 발행하는 구조는 시스템의 병목과 데이터 불일치를 유발할 수 있습니다.
외부 API 및 메시지 발행의 위험성
- 커넥션 풀 고갈: 외부 API 응답이 지연되면 트랜잭션이 유지되는 동안 데이터베이스 커넥션을 점유하게 됩니다. 이로 인해 커넥션 풀이 고갈되면 외부 API와 무관한 다른 서비스 요청까지 처리되지 못하는 장애가 발생합니다.
- 데이터 정합성 및 메시지 유실 문제: 주문 서비스 등에서 데이터베이스 커밋과 카프카 발행 사이에 서버 장애가 발생하면 메시지 유실이 일어날 수 있습니다. 흔히 고려되는 해결책으로 하나의 트랜잭션 안에서 메시지 전송 메서드를 함께 호출하는 방식이 있으나, 카프카 발행이 데이터베이스 트랜잭션에 직접 참여하지 않아 원자성을 보장하지 못합니다. 커밋 직후 전송이 실패하면 메시지가 유실되며, 반대로 전송은 성공했으나 데이터베이스가 롤백될 경우에는 데이터베이스에 없는 이벤트가 카프카에 잔류하는 문제가 발생합니다.
아키텍처 대안: Outbox 패턴과 CDC
메시지 유실을 방지하고 원자성을 보장하기 위해 다음과 같은 아키텍처 대안이 활용됩니다.
아웃박스 테이블(Outbox Table) 방식
- 비즈니스 로직과 동일한 트랜잭션 내에 아웃박스 테이블을 기록하여 이벤트 발행 여부를 원자적으로 보장합니다.
- 폴링 퍼블리셔(Polling Publisher)가 주기적으로 테이블을 조회하여 메시지를 발행합니다.
- 기본적으로 '적어도 한 번(At-least-once) 전달' 방식을 따르므로, 컨슈머 측의 철저한 멱등성(Idempotency) 설계가 필수적입니다.
- 구현 시 복합 프라이머리 키를 활용하는 방식과 별도의 고유 식별자 컬럼을 두는 방식 중 시스템 상황에 적합한 구조를 선택해야 합니다.
변경 데이터 캡처(CDC, Change Data Capture) 방식
- 별도의 변경 데이터 캡처 도구를 활용하여 데이터베이스 로그 기반으로 이벤트를 캡처하고 발행함으로써 폴링 오버헤드를 줄입니다.
- 단, 별도의 인프라스트럭처가 도입되어야 하며, 해당 인프라가 단일 장애점(SPOF)이 될 수 있음을 고려하여 설계해야 합니다.
트랜잭션 범위 최적화 및 일관성 설계
- 범위 최소화: 데이터베이스 작업을 먼저 커밋하고 외부 API 호출을 커밋 전후로 분리하는 구조를 권장합니다.
- 최종적 일관성(Eventual Consistency): 분리된 시스템 간에는 원자적 처리가 불가능하므로, 아웃박스 패턴이나 재시도 큐를 활용하여 외부 호출 실패 시 데이터를 보정하는 전략을 수립해야 합니다.
- MSA 환경 고려: 분산 트랜잭션이 필요한 경우 2PC(Two-Phase Commit)나 SAGA 패턴, 혹은 아웃박스 패턴을 도입하여 격리된 시스템 간의 정합성을 보존합니다.
오토스케일링 환경의 DB 커넥션 관리
스프링 부트 애플리케이션의 HikariCP 커넥션 풀(기본 10개)은 오토스케일링 환경에서 병목의 원인이 될 수 있습니다. 파드 개수가 증가함에 따라 물리 커넥션이 급증하여 데이터베이스의 한계치에 도달하면, 커넥션 풀 대기 시간이 발생하고 톰캣 워커 스레드가 잠기게 됩니다. 이는 헬스체크 실패로 이어져 클러스터 전체의 연쇄적인 재시작을 유발할 수 있습니다.
핵심 해결 방안
- 프록시 계층 도입: RDS Proxy나 PgBouncer 등을 도입하여 물리 커넥션을 일정하게 유지하고, 트랜잭션 단위의 멀티플렉싱을 통해 커넥션 고갈을 원천 차단합니다.
- 커넥션 풀 최적화: 파드당 커넥션 풀 크기를 관행적인 10개로 두지 않고, 데이터베이스 코어 수와 파드 최대 증설치를 고려하여 3~5개 수준으로 타이트하게 제한합니다.
- 헬스체크 분리: 쿠버네티스의 라이브니스 프로브와 데이터베이스 헬스체크를 분리하여, DB 지연 발생 시 애플리케이션 파드가 강제로 종료되는 상황을 방지합니다.
종합 실무 체크리스트
- 멱등성(Idempotency) 보장: 네트워크 오류, 메시지 중복 전달, 종료 시점의 재시도에 대비하여 모든 작업과 컨슈머 로직은 여러 번 실행되어도 결과가 동일하도록 설계하십시오.
- 트랜잭션 범위 제한 및 아웃박스 패턴: 외부 API 호출은 가급적 트랜잭션 외부로 분리하고, 메시지 유실 방지가 필요한 이벤트 발행에는 아웃박스 테이블이나 CDC 패턴을 적용하십시오.
- 타임아웃 설정: 서버 종료 제한 시간(
terminationGracePeriodSeconds)은 가장 긴 작업을 처리하는 시간보다 길게 설정하십시오. - DB 커넥션 관리: 오토스케일링 환경에서는 파드당 커넥션 풀 크기를 최적화하고, 필요시 프록시 계층을 도입하여 물리 커넥션 급증을 방지하십시오.
- 모니터링 및 DLQ: 종료 과정의 로그를 추적하고, 처리에 실패한 메시지는 DLQ(Dead Letter Queue)로 격리하여 분석 및 재처리하십시오.
- 장기 연결 관리: 서버 종료 시 SSE 등 장기 연결 세션이 안전하게 종료되도록 드레인 과정을 반드시 거치십시오.