목차
서비스가 에러를 내는 것보다 더 곤란한 일은, 에러를 모으는 도구가 사용자의 비밀을 같이 모아 버리는 일이에요. 이 글은 에러 추적 도구를 붙이면서 무엇을 보내고 무엇을 막을지 정하는 방법을 정리해요.
혹시 이런 고민 있으신가요?
- 에러 추적 도구를 붙이고 싶은데, 사용자가 올린 파일이나 입력값이 같이 저장될까 봐 걱정돼요.
- 서버를 직접 운영하는 작은 팀이라 무거운 상용 도구보다 가벼운 자체 호스팅 도구가 필요해요.
- 에러 알림이 너무 많아져서 정작 중요한 에러를 못 보고 있어요.
이 글을 읽으면
- Sentry 호환 도구를 내 서버에 두고 앱마다 따로 붙이는 순서를 알 수 있어요.
- 요청 본문, 예외 메시지처럼 새기 쉬운 값을 막고 보낼 값을 직접 정하는 규칙을 만들 수 있어요.
- 성능 수집과 로그의 양을 정해 도구의 디스크가 가득 차는 일을 막을 수 있어요.
1단계. 에러 추적 도구를 고르고 내 서버에 올리기
에러 추적 도구는 앱에서 난 예외를 한곳에 모아 "어떤 에러가, 몇 번, 어디서" 났는지 보여 주는 도구예요. 가장 널리 쓰이는 Sentry 는 앱에 넣는 라이브러리(SDK, 소프트웨어 개발 키트)가 에러를 모아 서버로 보내요. GlitchTip 은 이 SDK 가 보내는 형식을 그대로 받아 주는 가벼운 대체품이에요. 그래서 앱 코드는 Sentry SDK 를 쓰고, 받는 쪽만 내 서버의 GlitchTip 으로 바꿀 수 있어요.
자체 호스팅을 고르는 이유는 단순해요. 에러에는 사용자가 쓴 값이 섞여 들어올 수 있는데, 그 데이터가 남의 서버에 있는 것보다 내 서버에 있는 편이 설명하기 쉬워요. 저희는 서비스 여러 개를 한 서버에 올려 두었고, 이 도구도 같은 서버에 한 개 더 얹었어요.
| 기준 | 상용 서비스 | 자체 호스팅 GlitchTip |
|---|---|---|
| 데이터 위치 | 업체 서버 | 내 서버 |
| 운영 부담 | 거의 없음 | 디스크, 업데이트를 내가 챙김 |
| 기능 | 세션 추적, 성능 분석이 풍부함 | 에러 모으기가 중심, 세션은 받지 않음 |
| 비용 | 에러 수에 따라 늘어남 | 서버 비용에 포함 |
참고
상용 서비스의 요금과 기능은 시기마다 달라서 이 표에는 숫자를 넣지 않았어요. 고르기 전에 각 서비스의 현재 가격표를 확인하세요.
도구를 올린 다음에는 앱마다 프로젝트를 따로 만드세요. 프로젝트마다 접속 주소(DSN)가 하나씩 나오는데, 이 주소가 "에러를 어디로 보낼지"를 정해요. 프로젝트를 나누면 어느 앱의 에러인지 한눈에 보이고, 한 앱의 기록만 지워야 할 때도 그 프로젝트만 비우면 돼요.
2단계. 앱마다 SDK 를 붙이되 기본값을 의심하기
SDK 는 설치만 해도 에러를 잘 모아요. 문제는 기본값이 "많이 모으는 쪽"이라는 점이에요. 저희가 쓴 NestJS 용 SDK 11 버전은 기본으로 요청 본문, 헤더, 쿠키, 주소의 질의 문자열까지 같이 실었어요. 로그인 정보나 사용자가 올린 계약서가 요청 본문에 있으면, 그대로 에러 기록에 남을 수 있다는 뜻이에요.
그래서 SDK 를 붙인 첫날에 하는 일은 기능 켜기가 아니라 끄기예요. 아래는 이 설정을 일반화한 예시예요. 옵션 이름은 SDK 버전마다 다를 수 있으니, 내가 쓰는 버전의 문서에서 "요청 데이터를 얼마나 모을지" 정하는 옵션을 찾아 맞추세요.
import * as Sentry from "@sentry/nestjs" Sentry.init({ dsn: process.env.SENTRY_DSN, release: process.env.RELEASE, // 배포한 커밋을 적어요 dataCollection: { // 버전 11 기준, 모두 끄기 requestBody: false, requestHeaders: false, cookies: false, queryString: false, }, tracesSampleRate: 0.01, // 성능 수집은 1%})기본값을 끈 다음에는 실제로 에러를 일부러 한 번 내 보세요. GlitchTip 화면에서 그 기록을 열어, 본문이나 쿠키가 비어 있는지 눈으로 확인해요. 설정을 믿지 말고 결과를 보는 것이 이 단계의 전부예요.
앱이 NestJS 처럼 예외를 자기 필터 안에서 처리하는 프레임워크라면 한 가지가 더 있어요. 프레임워크가 예외를 삼키면 일반적인 오류 처리기에는 500 에러가 닿지 않아요. 저희는 프레임워크 전용 SDK 가 제공하는 전역 필터를 붙였고, 이 필터는 일반적인 HTTP 예외(없는 페이지, 잘못된 요청 같은 것)는 건너뛰고 예상 못 한 예외만 보내요. 이 덕에 사용자의 실수가 에러 목록을 덮지 않아요.
TIP
DSN 은 비밀번호가 아니라 "보낼 곳의 주소"예요. 브라우저 코드에 들어가는 값이라 숨길 수 없어요. 대신 DSN 이 비어 있으면 SDK 를 초기화하지 않게 짜 두세요. 개발 환경에서 에러가 운영 프로젝트로 새는 일을 막을 수 있어요.
3단계. 보낼 값을 허용 목록으로 정하기
기본값을 껐어도 새는 구멍이 하나 남아요. 예외 객체의 메시지예요. 라이브러리가 낸 예외 메시지는 문서 안에서 읽은 값을 그대로 물고 나오는 일이 흔해요. 파일 이름도 마찬가지예요. "OO 주식회사 근로계약서" 같은 이름 한 줄이면 회사와 문서 종류가 드러나요. 한번 보내면 도구에 보관 기간이 따로 생기고, 서비스가 사용자에게 한 약속과 어긋날 수 있어요.
저희는 이때 막을 것을 나열하는 대신 보낼 것을 나열하는 쪽을 골랐어요. 막을 목록은 라이브러리가 어떤 문자열을 물고 나올지 다 알 수 없어서 늘 구멍이 생겨요. 보낼 목록은 내가 만든 값만 들어가서 구멍이 없어요.
| 보내요 | 보내지 않아요 |
|---|---|
| 우리가 정한 실패 이유 코드 | 예외의 메시지 |
| 예외의 클래스 이름 | 스택 안의 값 |
| 쪽수, 바이트 크기, 개수 같은 숫자 | 파일 이름 |
| 어느 단계에서 났는지 | 문서나 입력에서 읽은 글자 |
| 배포한 커밋 | 이메일, 주소 |
예외를 그대로 넘기지 않고, 보낼 값을 만들어 넘겨요
구현은 간단해요. SDK 를 코드 여러 곳에서 직접 부르지 말고, 보낼 값을 만드는 함수를 한곳에 두세요. 예외는 그 함수가 받아서 위 표의 왼쪽 값만 뽑아 넘겨요.
type Report = { reason: "encrypted" | "library-failed" | "too-large" // 우리가 정한 코드 step: "upload" | "compose" | "save" pages?: number bytes?: number} export function reportFailure(error: unknown, info: Report) { Sentry.captureMessage(info.reason, { level: "error", tags: { step: info.step, errorClass: errorClassOf(error) }, extra: { pages: info.pages, bytes: info.bytes }, })} function errorClassOf(error: unknown): string { return error instanceof Error ? error.constructor.name : "unknown"}이렇게 하면 디버깅이 조금 답답해져요. 클래스 이름만으로는 왜 났는지 바로 알기 어려워요. 이것이 이 방식의 대가예요. 줄이는 방법은 메시지를 보내는 것이 아니라 내가 만드는 값을 늘리는 거예요. 어느 단계인지, 쪽수가 얼마인지, 몇 바이트인지, 어느 배포인지를 함께 보내면 클래스 이름 하나보다 훨씬 좁혀져요.
TIP
정말 재현이 안 되는 실패가 쌓이면 메시지를 통째로 열지 말고, 그 실패 하나에 한해 내가 허용 목록을 만든 값만 더 보내세요. 먼저 내 컴퓨터에서 같은 실패를 일으켜 메시지에 사용자 값이 없는지 읽어 보고요.
4단계. 성능과 로그의 양을 정하기
에러만 모으던 도구에 성능 수집과 로그를 켜면 양이 확 늘어요. 자체 호스팅에서는 이것이 곧 디스크예요. 저희가 정한 기준은 이래요.
- 성능 추적은 요청의 1%만 표본으로 모아요. 느려지는 경향을 보기에는 충분하고, 양은 백 배 줄어요.
- GlitchTip 은 사용자 세션 정보를 받지 않아서, 브라우저와 서버 SDK 에서 세션 기능을 뺐어요. 보내도 버려질 데이터를 만들 이유가 없어요.
- 로그는 자동으로 모으지 않고, 중요한 자리에서만 코드로 직접 보내요. 이미지 정리 작업의 실패, 요청 한도 초과, 결제나 신청 같은 핵심 사건의 성공이 그 자리예요.
- 로그에도 이메일, 주소, 파일 이름, 예외 메시지는 싣지 않아요. 3단계의 허용 목록이 로그에도 그대로 적용돼요.
반복되는 실패는 특히 조심하세요. 저희의 이벤트 적재 작업은 실패하면 1초 뒤에 다시 시도해요. 실패할 때마다 로그를 보내면 장애가 한 시간 이어질 때 3,600건이 쌓여요. 그래서 처음 실패한 한 번과 복구된 한 번만 보내요. 이렇게 하면 "언제 시작해서 언제 끝났는지"가 두 줄로 남아요.
TIP
로그 수준을 나누면 나중에 줄이기 쉬워요. 실패는 error, 한도 초과는 warn, 일어난 사건은 info 로 두면 디스크가 모자랄 때 info 부터 끌 수 있어요.
5단계. 이미지에 DSN 을 구워 넣고 배포 단위로 묶기
DSN 은 환경변수로 서버에 직접 넣을 수도 있지만, 저희는 이미지를 만들 때 빌드 인자로 넣었어요. 서버에서 손으로 키를 넣는 일이 없어져서, 새 서비스를 올릴 때 빠뜨릴 걱정이 줄어요. 브라우저에서 쓰는 DSN 은 어차피 코드에 실려 나가니 이 방식이 오히려 자연스러워요.
배포 식별자(release)에는 커밋 해시를 넣으세요. GlitchTip 은 같은 에러를 배포별로 묶어 보여 줘서, "이 에러가 어느 배포부터 생겼는지"를 바로 알 수 있어요. 배포할 때마다 같은 에러가 한 번씩 쌓이는 문제는 종료 처리를 고쳐야 하는 경우가 많은데, 그 판단도 배포 식별자와 에러 시각을 나란히 놓으면 쉬워요.
마지막으로 사용자에게 알리세요. 에러 기록에 무엇이 남는지는 개인정보 처리방침에 적는 편이 안전해요. 저희 한 서비스에서는 오류 이름과 메시지, 코드 위치, 페이지 주소, 브라우저와 기기 종류를 1년 남긴다고 적었어요. 보내는 항목이 바뀌면 방침을 먼저 고치고, 이미 쌓인 기록은 프로젝트째 지울 수 있게 해 두세요.
자주 묻는 질문
Q1. 요청 본문을 다 껐는데 정말 에러 원인을 찾을 수 있나요?
대부분은 찾을 수 있어요. 에러가 난 코드 위치와 클래스 이름, 단계, 크기 같은 숫자가 있으면 범위가 꽤 좁혀져요. 그래도 안 풀리는 경우에만 내 컴퓨터에서 재현해 보세요. 사용자의 실제 값을 서버로 가져오는 것보다 안전해요.
Q2. 허용 목록 대신 SDK 의 전송 직전 훅으로 민감한 글자만 지우면 안 되나요?
가능은 하지만 권하지 않아요. 지울 패턴을 미리 알아야 하는데, 라이브러리가 어떤 문자열을 물고 나올지 다 알 수 없어요. 놓친 패턴이 하나만 있어도 새요. 보낼 값을 직접 만드는 쪽이 구멍이 없어요.
Q3. 상용 서비스와 비교하면 무엇을 잃나요?
세션 단위의 사용자 흐름 같은 풍부한 분석을 잃어요. 대신 에러가 어디서 났는지 알려 주는 일에는 충분했어요. 사용자 행동은 방문 분석 도구에서 따로 보면 돼요.
정리
- Sentry SDK 를 쓰되 받는 쪽은 내 서버의 GlitchTip 으로 두면 데이터 위치를 설명하기 쉬워요.
- SDK 를 붙인 첫날 기본 수집을 끄고, 일부러 낸 에러로 결과를 확인해요.
- 막을 것을 나열하지 말고 보낼 것을 나열해요. 실패 코드, 클래스 이름, 내가 만든 숫자만 보내요.
- 성능은 1% 표본, 로그는 정한 자리에서만, 반복 실패는 시작과 복구 두 번만 보내요.
- 에러 기록에 무엇이 남는지는 처리방침에 적고, 지울 방법도 준비해요.
에러를 모으는 일과 사용자를 지키는 일은 부딪히지 않아요. 무엇을 보낼지 내가 정하기만 하면 둘 다 할 수 있어요.