← Back to list

Release 의 모든 것— 항공사를 멈추게 한 예외

누락된 예외처리 때문에 대형 항공사에서 장애가 발생하여 수천명의 승객의 발이 묶이고 회사는 수십만 달러의 손해를 보았다.

Taesu, lee · 2024-09-23 08:29 · 2 claps · 7.8 min read
#release-it #bugs
Open on Medium ↗

Release의 모든 것 — 항공사를 멈추게 한 예외

누락된 예외처리 때문에 대형 항공사에서 장애가 발생하여 수천명의 승객의 발이 묶이고 회사는 수십만 달러의 손해를 보았다.

멈춰버린 항공사 서비스의 고가용성 아키텍처

CF (핵심 지원 기능: Core Facility) 시스템은 늘어가는 서비스 규모에 따라 재사용성을 높이고 개발 시간, 운영 비용을 절감하기 위해 서비스 지향 아키텍처로 전환하기 위해 탄생한 것으로 셀프 체크인 키오스크, 채널 파트너, 전화 메뉴, 대화식 음성 응답(IVR) 시스템 등 많은 서비스들이 CF 시스템을 의존하고 있었다.

구조에서 알 수 있듯이 CF 시스템은 SPOF가 될 수 있다. 따라서 엔지니어들은 고가용성을 보장하기 위해 각고의 노력을 기울였다.

  • CPU, 저장장치, 네트워크 카드, 파워 서플라이, 네트워크 스위치 등 모든 하드웨어 이중화
  • 랙 손상을 방지하기 위해 서버를 여러 랙으로 분할
  • 화재, 홍수, 폭발 등을 대비해 데이터센터 이중화

무사히 완료 된 CF 시스템의 정기 유지보수 작업

인프라 운영 중 데이터베이스에 대한 정기 유지 보수 작업 수행을 아래와 같이 진행했다.

CF 시스템의 데이터베이스 이중화 구조 (Active — Standby)

CF 시스템의 데이터베이스 이중화 구조 (Active — Standby)

  • Active 데이터베이스를 1에서 2로 이전 (스위치 오버)
  • 데이터베이스 1에서 유지보수 작업을 수행
  • 잘 반영 되었는 지 확인 후 동일한 과정을 데이터베이스 2에도 반영

전체 작업은 무중단으로 진행 되었고 모니터링에서도 별다른 장애가 없었기에 작업 완료 공지 후 엔지니어들은 22시에 귀가했다.

장애 발생 및 해결

새벽 두시반 경, 전국의 모든 키오스크가 동작을 멈췄다. 몇분 후 IVR 서버도 빨간불이 되어 멈췄다. 어떤 경우에도 원인 파악보다 서비스의 복원이 최우선이기에 복구 대상을 먼저 파악해야 한다.

키오스크, IVR 모두가 공통으로 의존하는 CF 시스템인 것과 몇시간 전 CF 시스템의 정기 유지보수를 위한 데이터베이스 전환 작업을 했다는 것이 가장 큰 의심 사항이었다.

재빠르게 CF 시스템의 서버를 하나씩 재시작 하자 IVR 서버는 정상화 됐다. 그러나 키오스크는 여전히 동작을 멈췄는데, 수석 엔지니어가 직감적으로 키오스크 애플리케이션의 서버도 재시작 하도록 했고 곧 정상화 되었다.

사후분석(Postmortem)과 원인 파악

사후 분석은 미스터리 살인사건과 비슷하다. 여러 단서는 있으나 부검할 사체가 없다는 점이 파악을 어렵게 한다. 다행히 팀에선 장애 발생 당시 로그 파일, 스레드 덤프와 같은 중요한 정보를 가져오는 스크립트를 만들어서 관리하고 있었고 장애 당시 서버를 재시작 하기 전에 확보한 단서들이 많이 있었다.

키오스크 애플리케이션에 할당 된 전체 스레드 40개가 모두 CF 시스템의 서비스를 호출한 상태로 오지 않을 응답을 기다리고 있었다. (RMI로 호출하였기에 응답을 무한정 기다리는 상황이 쉽게 발생 했다.)

CF 시스템의 스레드 덤프를 보면 별도의 스레드 풀을 통해 EJB 호출과 HTTP 요청을 처리하도록 했다. 그래서 EJB 호출엔 장애가 발생해도 HTTP 요청엔 응답을 할 수 있어 모니터링 애플리케이션엔 정상 응답할 수 있었다.

EJB 스레드는 FlighSearch.lookupByCity를 처리하는데에 동원되었고 그 메서드는 아래와 같이 생겼다.

public class FlightSearch implements SessionBean {
  public List lookupByCity(. . .) throws SQLException, RemoteException {
    Connection conn = null;
    Statement stmt = null;
    try {
      conn = getConnection();
      stmt = conn.createStatement();
      // do query
      return result;
    } finally {
      if (stmt != null) {
        stmt.close();
      } 
      if (conn != null) {
        conn.close();
      }
    }
  }
}
  • java.sql.Statement.close()는 SQLException을 던지도록 되어있음
  • conn.createStatement()는 커넥션 연결 여부는 따로 확인하지 않고 객체 내부에 대해서만 확인 하도록 되어있음
  • 이 예외는 거의 만나기 힘든데, 커넥션은 생성 성공 했는데 Statement를 close하다가 예외가 발생할 케이스가 거의 없기 때문임
  • CF 시스템의 데이터베이스 전환 과정과 연관지어 보면, 데이터베이스 1을 바라보는 커넥션이 생성 되어있었고 스위치 오버되어 데이터베이스 2가 Active가 된 경우를 가정
  • 생성 된 stmt 객체로 쿼리를 날릴 때 예외가 발생할 것이고 finally에서 stmt.close() 호출 시에 한번 더 예외가 발생해서 connection은 풀로 반환되지 않게 됨 -> 결국 커넥션이 고갈 됨

외양간 고치기

작은 실수로 인해 어마어마한 비용이 발생할 때 자연스러운 반응은 ‘이런 일이 다시 일어나서는 안돼'라고 말하는 것이다.

하지만 어떻게 예방할 수 있을까? 코드리뷰로 이런 버그를 찾아낼 수 있었을까? 더 강화된 절차를 만들고 참조자가 많으면 이런 버그를 예방할 수 있었을까? 이 경우엔 코드 검토자가 JDBC 드라이버 내부를 잘 알거나 혹은 커넥션은 생성 성공했지만 Statement 종료에서 예외가 발생할 수 있을만한 상황을 인지해야 잡을 수 있다.

혹은 QA 단계에서 테스트 플랜을 더 강화 했어야 했을까? 어쩌면 가능 할수도 있다. 문제를 한번 식별하면 즉, 어디를 봐야할 지 알면 그것을 찾는 테스트를 만드는 것은 쉽다.

결국 이런 버그 하나하나가 모두 사전에 제거되기를 기대하는 것은 환상에 가깝다. 언제든 예기치 못한 상황에서 튀어나올수 있다. 버그는 발생한다. 말살되기는 커녕 반드시 살아남는다.

이 지점에서 가장 심각한 문제는 한 시스템의 버그가 관련이 있는 다른 모든 시스템으로 전파됐다는 점이다. 버그를 예방할 방법을 찾는 것보다 더 나은 질문은 “한 시스템의 버그가 다른 시스템에 영향을 미치지 않게 하는 방법은 무엇인가?”이다.

오늘날 기업의 내부엔 상호 연결되고 의존하는 많은 서비스들이 있다. 이런 환경에서 버그가 연쇄적으로 장애를 일으키지 않도록 여러 설계 패턴을 잘 활용해야 할 것이다.

마무리 및 생각 정리

새벽 두시반 경, 전국의 모든 키오스크가 동작을 멈췄다. 몇분 후 IVR 서버도 빨간불이 되어 멈췄다. 어떤 경우에도 원인 파악보다 서비스의 복원이 최우선이기에 복구 대상을 먼저 파악해야 한다.

장애 발생 시 “원인 파악보다 서비스의 복원이 최우선”이라는 구문이 인상깊었다. 이따금 장애 상황을 마주하다 보면 서버 재시작으로 해결될 것 같은 직감이 어느정도 있기는 하다.

근본적인 원인 해결은 못하고 시한폭탄을 안고가는 느낌이라 원인 파악을 주력했는데 이 글에서처럼 일단 단서를 남겨두고 서비스의 복원을 취우선으로 생각하는것이 좋을것 같다는 생각이 든다.

ECS Fargate에서 서비스 하다보니 로그는 잘 보존 되는데 스레드 덤프를 잘 확보해둘 수 있는 방안을 고려해봐야 할 것 같다.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

결국 이런 버그 하나하나가 모두 사전에 제거되기를 기대하는 것은 환상에 가깝다. 언제든 예기치 못한 상황에서 튀어나올수 있다. 버그는 발생한다. 말살되기는 커녕 반드시 살아남는다.

이 지점에서 가장 심각한 문제는 한 시스템의 버그가 관련이 있는 다른 모든 시스템으로 전파됐다는 점이다. 버그를 예방할 방법을 찾는 것보다 더 나은 질문은 “한 시스템의 버그가 다른 시스템에 영향을 미치지 않게 하는 방법은 무엇인가?”이다.

기업에게 버그는 언제나 민감한 주제이다. 특히나 매출과 직결되는 장애를 마주하다보면 버그를 예방하고 사전에 없애려는 방향으로 많이들 자세를 취하는 것 같다.

그러다 보니 몇가지 최악의 수를 두기도 하는데 아래의 것들이다.

  • 장애 발생/처리 내용을 매주 전사에 공유
  • 코드리뷰 강화. 2차 3차로 부족하면 4차까지!
  • 코드리뷰 체크리스트 도입. 작업 유형별로 확인 완료했다는 증거도 남기기.
  • 철저한 기능 분석 후 매주 리뷰. 모든 것을 다이어그램으로 정리.

대부분 시도들은 더 악효과를 내서 결국 생산성 저하까지 이어지지 않나 싶다.

이 단원에서 예시로 든 아주 사소한 실수 사례가 너무나도 현실적으로 느껴진다. 지금 어느 회사든 가지고 있는 레거시 코드 중 하나를 까보면 저런 형태의 finally 구문이 많을 것으로 생각한다.

Connection conn = null;
Statement stmt = null;
try {
  conn = getConnection();
  stmt = conn.createStatement();
  // do query
  return result;
} finally {
  if (stmt != null) {
    stmt.close();
  } 
  if (conn != null) {
    conn.close();
  }
}

그럼에도 장애가 발생하지 않은건 이 코드로 인해서 발생할 수 있는 오류 자체가 극히 드문 케이스이기 때문이기도 하다.

위 사레에서 CF 시스템은 Connection timeout은 무한대로 잡아뒀을까? 적절히 걸어뒀다면 커넥션 풀이 고갈 되어도 요청을 처리하는 스레드가 응답은 할 수 있었을 텐데 말이다.

책에서 말하는 것처럼 무언가 체계를 도입하고 프로세스를 통해 버그를 사전에 없애는 것은 환상에 가깝다고 생각한다. 모든 버그들은 사소한 실수들이 대부분이다. 그래서 더욱 사전에 발견이 어렵다.

요즘은 CI/CD를 통해 하루에도 수십번씩 배포할 수 있는 환경이 준비된 만큼 버그가 발생한 것을 곧장 인지하고 신속하게 대응하도록 하고 버그로 인한 장애가 다른 시스템으로 전파되지 않도록 체계를 만들어가는게 더 좋지 않을까?


메타데이터
post_id
b10940752bf0
slug
release-의-모든-것-항공사를-멈추게-한-예외-b10940752bf0
url
https://medium.com/@taesulee93/release-%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%ED%95%AD%EA%B3%B5%EC%82%AC%EB%A5%BC-%EB%A9%88%EC%B6%94%EA%B2%8C-%ED%95%9C-%EC%98%88%EC%99%B8-b10940752bf0
canonical_url
https://medium.com/@taesulee93/release-%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%ED%95%AD%EA%B3%B5%EC%82%AC%EB%A5%BC-%EB%A9%88%EC%B6%94%EA%B2%8C-%ED%95%9C-%EC%98%88%EC%99%B8-b10940752bf0
author_url
https://medium.com/@taesulee93
status
ok
fetched_at
2026-07-18 20:46:04