engineering5분 분량

Sentry가 보여 준 것: 관측 가능성, 안정성, 그리고 코드 감시 비용의 변화

프로덕션 오류에서 스키마 불일치, 트랜잭션 복구 실패, 도움이 되지 않는 로그가 드러났습니다. 소규모 팀이 Sentry로 백엔드 오류를 수정하고 브라우저 모니터링을 준비하며 알림 잡음을 조절한 과정을 살펴봅니다.

K
Ken Jo

예전에는 엔지니어링 팀이 관측 가능성에 지나치게 투자하거나(아무도 보지 않는 대시보드를 연결하는 SRE 조직을 따로 두거나), 아예 건너뛰곤 했습니다(서비스 장애를 고객의 이메일로 알게 되는 식입니다). 2026년에는 어느 쪽도 받아들이기 어렵습니다. 무엇이 고장 났는지 알아야 하지만, 인디 제품에 엔터프라이즈 수준의 비용을 내지 않고 알아야 합니다.

이 글은 Sentry 연동으로 프로덕션 오류를 발견하고, 그에 대응해 무엇을 바꿨으며, 잡음과 사용량을 조절하도록 모니터링을 구성한 과정을 살펴봅니다. 소규모 엔지니어링 팀을 위해 관측 도구가 어떻게 변하고 있는지도 다룹니다.

브라우저 오류 알림이 빠져 있던 이유

백엔드는 3월부터 Sentry에 연결되어 있었습니다. DSN은 Google Secret Manager에 있고, 시크릿은 Cloud Run에 마운트되며, sentry_sdk.init은 시작할 때마다 실행됩니다. 이메일 알림도 도착하고 있었습니다. 겉으로는 문제가 없어 보였습니다.

하지만 내부를 들여다보니 그렇지 않았습니다. 프런트엔드는 Sentry에 전혀 연결되지 않았습니다. 브라우저 예외, 하이드레이션 오류, 청크 로드 실패가 보이지 않았습니다. 게다가 백엔드의 Python 오류가 조직에 유일한 프로젝트인 javascript-nextjs 프로젝트로 들어가고 있었습니다. 스택 트레이스는 잘못된 위치에 쌓이고, 그룹화는 쓸모없었으며, 대시보드에는 서로 다른 런타임의 이벤트가 뒤섞였습니다.

개별 버그를 살피기 전에 브라우저 오류와 백엔드 오류를 분리해야 이 공백을 메울 수 있었습니다.

데이터가 보여 준 문제

최근 90일간의 오류 이벤트를 살펴보니 몇 가지 패턴이 드러났습니다.

이미 해결됐지만 기록에 남은 사고

기간 중 가장 자주 나타난 오류는 소셜 가입 엔드포인트에서 발생한 Postgres StringDataRightTruncation이었습니다. 22초 동안 16건이 발생했습니다. 원인은 전형적인 스키마 불일치였습니다. 모델 정의는 800자 자기소개를 허용했지만, 프로덕션 데이터베이스 컬럼은 이전 마이그레이션의 VARCHAR(500) 상태였습니다. 사용자가 500자를 조금 넘는 자기소개를 제출하자 Pydantic 계층은 이를 받아들였고 INSERT가 실패했습니다.

컬럼을 800자로 늘리는 마이그레이션은 이미 작성되어 다음 날 아침 배포됐습니다. 하지만 모델과 DB 스키마가 어긋나도 고객이 문제를 일으키기 전까지 아무도 알아차리지 못할 수 있다는 근본 문제는 남아 있었습니다.

두 가지를 조치했습니다. 먼저 소셜 가입 요청 스키마에 이메일, 표시 이름, 토큰의 명시적인 max_length를 지정해 비정상적으로 긴 입력이 데이터베이스까지 도달하지 않고 깔끔하게 422를 반환하도록 했습니다. 다음으로 DB 무결성 스크립트를 확장해 information_schema.columns.character_maximum_length를 애플리케이션이 기대하는 길이와 비교하게 했습니다. 앞으로 마이그레이션으로 컬럼 길이가 모델이 생성할 수 있는 값보다 짧아지면 다음 배포의 무결성 검사에서 실패합니다.

롤백되지 않던 세션

목록 아래쪽에는 오류가 이어져 있었습니다. POST /v1/upload에서 OperationalError: server closed the connection unexpectedly가 한 번 발생한 뒤 같은 코드 경로에서 PendingRollbackError가 계속 발생했습니다. Postgres가 쿼리 도중 연결을 끊었고 SQLAlchemy 세션은 유효하지 않은 상태가 됐습니다. 업로드 서비스는 그 위에서 부분 결과 저장을 시도했습니다. 이후 같은 세션에서 수행한 모든 작업이 연쇄적으로 실패했습니다.

수정은 작지만 순서를 잘못 잡기 쉽습니다. 이제 업로드 파이프라인이 예외를 잡으면 가장 먼저 db.rollback()을 실행합니다. 롤백 실패가 원래 오류를 가리지 않도록 자체 try/except로 감쌌습니다. 그 뒤 깨끗한 세션에서 부분 저장 로직을 실행합니다.

스스로 내용을 지워 버리던 로그

WebSocket 종료 핸들러는 잡은 예외를 err=%s로 기록하고 있었습니다. 대부분의 예외에서는 유용한 내용이 출력됩니다. 하지만 __str__이 빈 문자열을 반환하는 일부 예외에서는 err= 뒤에 아무것도 나오지 않았습니다. 저장 실패를 알리는 Sentry 이벤트가 왜 실패했는지는 알려 주지 않았습니다.

logger.exception(...)과 err=%r로 바꾸는 데 비용은 들지 않았고 이후 이벤트에 맥락이 빠지지 않게 됐습니다.

비용을 내고 싶지 않았던 잡음

나머지 이벤트는 버그가 아니었습니다. 클라이언트가 SSL 중간에 연결을 끊을 때 uvloop가 가끔 RuntimeError: unable to perform operation on <TCPTransport closed=True>를 발생시킵니다. 이는 수정 대상이 아닌 정상적인 동작입니다. 월 5,000건 오류라는 무료 요금제 한도에서 이는 낭비되는 사용량입니다. 해당 예외와 값 패턴을 제거하는 before_send 필터를 추가했습니다. 프런트엔드에서도 ResizeObserver loop limit exceeded 등 비슷한 브라우저 잡음을 같은 방식으로 처리했습니다.

무료 요금제에 맞게 조정하기

2026년 10월 13일 확인한 Sentry의 현재 Developer 요금제는 매월 오류 5,000건, 트레이싱 스팬 500만 개, 리플레이 50개를 무료로 제공합니다. 트레이싱은 기존 성능 단위가 아니라 스팬으로 측정합니다. 샘플링 비율을 정하기 전에 현재 요금제 한도를 확인하세요. 오류 허용량이 SDK가 보낼 수 있는 모든 신호의 예산은 아닙니다.

설정은 다음과 같습니다.

  • 백엔드와 프런트엔드 모두 tracesSampleRate: 0으로 설정했습니다. 분산 추적은 정말 유용하지만 매월 성능 단위가 10K인 상황에서는 한 번의 나쁜 오후에 예산을 모두 쓸 수 있습니다. 특정 문제를 조사할 때 낮은 샘플링 비율로 켤 수 있습니다.
  • 백엔드는 profilesSampleRate: 0으로 설정했습니다. 이유는 같습니다.
  • 프런트엔드에서 Session Replay를 비활성화했습니다. 연동 코드는 두 샘플링 비율을 모두 0으로 설정한 채 남겨 두었습니다. 심각한 버그를 조사하는 데 리플레이 맥락이 필요하면 하루 동안 replaysOnErrorSampleRate를 0.05로 바꿨다가 다시 끌 수 있습니다.
  • 두 런타임 모두 before_send 필터를 사용해 이미 조치할 필요가 없다고 파악한 이벤트를 제거합니다.

백엔드 변경은 조치 가능한 오류의 잡음을 줄였고, 브라우저 SDK 설정은 활성화를 준비했습니다. 두 단계는 서로 다릅니다. 코드와 샘플링 설정만으로 브라우저 이벤트가 의도한 프로젝트에 도달한다고 확인할 수는 없습니다.

관측 가능성 시장은 어떻게 바뀌고 있나요?

Sentry의 무료 요금제가 호스팅 제품 중 넉넉한 편이라 이에 맞춰 설정했지만, 더 큰 흐름도 살펴볼 필요가 있습니다. 이 분야의 경제성이 바뀌고 있기 때문입니다.

지난 12~18개월 사이 몇 가지 변화가 있었습니다.

  • 개방형 표준은 이전 작업을 줄여도 모든 작업을 없애지는 않습니다. OTLP 호환 계측은 모니터링 백엔드와의 결합을 낮출 수 있습니다. 하지만 이전할 때 공급자별 쿼리, 대시보드, 경보 규칙, SDK 기능은 여전히 계획하고 테스트해야 합니다.
  • 자체 호스팅은 비용 계산을 바꿉니다. GlitchTip이나 OTLP 기반 스택 같은 선택지도 모니터링을 위한 다른 경로입니다. 가상 머신 비용만 구독료와 비교하지 말고 저장 공간, 백업, 업그레이드, 엔지니어링 시간을 함께 비교해야 합니다.
  • AI 디버깅 지원은 업무 흐름을 확장합니다. Sentry의 Seer는 근본 원인과 패치를 제안하고, 승인된 MCP 연동은 코딩 어시스턴트에서 오류 맥락을 확인할 수 있게 합니다. 제안된 수정도 릴리스 전에 재현과 회귀 테스트를 거쳐야 합니다.
  • 요금제 한도도 기능만큼 주의해야 합니다. 오류량, 보존 기간, 샘플링, 팀 접근 권한이 비용에 영향을 줄 수 있습니다. 필요한 신호와 측정된 이벤트 예산부터 정한 다음 그 사용량에 맞춰 요금제를 비교하세요.

이 릴리스 스냅샷에서 Sentry는 당장의 오류 모니터링 요구를 충족했습니다. 개방형 인터페이스를 사용할 수 있는 곳에서 활용하면 나중에 선택지가 늘어나지만, 공급자를 바꿀 때는 프로젝트 라우팅, 알림, 보존 데이터, 공급자별 기능을 여전히 확인해야 합니다.

배포 확인까지 해야 모니터링이 완성됩니다

이 글의 2026년 5월 17일 기준으로 배포 작업 두 가지가 남아 있었습니다.

  1. 백엔드 오류가 javascript-nextjs에 기록되지 않도록 전용 Python Sentry 프로젝트를 만듭니다. DSN은 백엔드가 이미 읽고 있는 동일한 Secret Manager 항목에서 바꿉니다.
  2. 클라이언트 오류가 전송되도록 프런트엔드 DSN을 Vercel 프로젝트 설정에 넣습니다. 코드는 이미 있고 조건부로 초기화되며, 빠진 것은 환경 변수뿐입니다.

모니터링을 도입할 때는 각 런타임에서 통제된 테스트 이벤트를 보내 프로젝트, 환경, 그룹화, 맥락을 확인한 뒤 연동이 완료됐다고 판단하세요. 프로젝트 분리와 환경 변수 설정은 준비 단계이지 이벤트가 도착했다는 증거가 아닙니다.

왜 중요한가요?

무엇이 고장 나는지 보이지 않으면 방어 코딩에 지나치게 투자하거나, 충분히 투자하지 않은 채 버그를 배포하게 됩니다. 두 경우 모두 비용이 큽니다. 저렴하고 신뢰할 수 있는 관측 가능성이 있으면 작은 팀도 더 큰 팀처럼 움직일 수 있습니다. 고객이 알아차리기 전에 회귀를 발견하고, 추측 없이 수정하며, 엔지니어링 시간을 새 기능 개발에 쓸 수 있습니다.

중요한 교훈은 양쪽을 모두 확인하는 것입니다. 애플리케이션의 수정 사항뿐 아니라 다음 회귀를 보여 줘야 하는 모니터링 경로도 검증해야 합니다.


← 블로그로 돌아가기