관측 가능성의 시작: Sentry로 서버 모니터링 체계 구축하기
알파 테스트를 앞두고 디버그 로그 정리와 함께 Sentry를 도입하여 실시간 에러 추적 시스템을 구축한 과정
이제 드디어 안드로이드 14일 비공개 테스트가 끝이 났다.
이제 운영 릴리즈 & 알파 테스트를 앞두고, 마지막 마무리로 디버그용 로그를 정리하고, “관측 가능성”을 적용해서 백단 서비스나 DB가 문제가 생겼을 때 유저가 느끼는 순간 바로 나도 동시에(혹은 내가 먼저) 보고 받을 수 있는 체계를 만들면 진짜 알파 준비 완료! 이다.
오늘은 내가 이 “관측 가능성”을 적용하기 위해 사용한 sentry 에 대해서 적어보려고 한다.
왜 Sentry인가?
사실 개발하면서 console.log나 winston 같은 로깅 라이브러리를 써서 로그를 파일로 남기는 건 당연하게 해왔다.
그런데 문제는, 서버가 죽었을 때 내가 그걸 어떻게 알 수 있는가? 였다.
로그 파일을 매일 아침 SSH 접속해서 확인할 순 없는 노릇이고, 유저가 “앱이 안 돼요”라고 제보하고 나서야 문제를 알게 되는 건 너무 늦다.
특히 내 경우는 혼자서 백엔드부터 앱까지 다 만들고 있는 상황이라, 24시간 서버를 모니터링할 여유가 없었다.
그래서 선택한 것이 Sentry다.
Sentry의 핵심은 단순하다:
- 에러가 발생하면 실시간으로 알림을 준다.
- 스택 트레이스와 요청 정보를 자동으로 수집해준다.
- 성능 병목 지점을 추적할 수 있다.
무료 플랜에서도 월 5,000개 이벤트를 쓸 수 있으니, 초기 단계에서는 충분했다.
설치 및 기본 설정
1단계: Sentry 계정 생성 및 DSN 발급
설치보다 먼저 해야 할 건 Sentry 계정을 만들고 프로젝트를 생성하는 것이다.
- https://sentry.io에 접속해서 회원가입
- Create Project 클릭
- 플랫폼은 Express.js 선택
- 프로젝트 이름은
babple-backend로 설정
생성하면 **DSN (Data Source Name)**이라는 걸 준다.
형식은 대충 이런 식이다:
https://abc123def456@o123456.ingest.sentry.io/7890123
이게 내 서버와 Sentry를 연결하는 열쇠다. 잘 복사해둔다.
2단계: 패키지 설치
Node.js 백엔드에서 Sentry를 쓰려면 두 개 패키지가 필요하다.
npm install @sentry/node @sentry/profiling-node
@sentry/node: 기본 Sentry SDK@sentry/profiling-node: 성능 프로파일링 기능
Sentry 초기화 코드 작성
Sentry 설정 파일 (src/config/sentry.ts)
Sentry 초기화 로직을 별도 파일로 분리했다. 이유는 간단하다. 서버 시작 코드(server.ts)를 깔끔하게 유지하고 싶었기 때문이다.
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
export function initializeSentry() {
// DSN이 없으면 Sentry 비활성화
const dsn = process.env.SENTRY_DSN;
if (!dsn) {
console.log('⚠️ [Sentry] SENTRY_DSN이 설정되지 않았습니다. Sentry를 비활성화합니다.');
return;
}
// 환경별 샘플링 비율 설정
const isDev = process.env.NODE_ENV !== 'production';
const tracesSampleRate = isDev ? 1.0 : 0.1; // dev: 100%, prod: 10%
const profilesSampleRate = isDev ? 1.0 : 0.1;
Sentry.init({
dsn,
environment: process.env.NODE_ENV || 'development',
// 릴리스 버전 추적
release: `babple-backend@${process.env.npm_package_version}`,
// 성능 모니터링
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate,
profilesSampleRate,
// 민감한 데이터 필터링
beforeSend(event, hint) {
// 요청 데이터에서 민감 정보 제거
if (event.request?.data) {
const sensitiveFields = [
'password', 'newPassword', 'currentPassword',
'token', 'accessToken', 'refreshToken',
'fcmToken', 'socialToken', 'idToken'
];
sensitiveFields.forEach(field => {
if (event.request.data[field]) {
event.request.data[field] = '[FILTERED]';
}
});
}
// Authorization 헤더 제거
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['Authorization'];
}
return event;
},
});
console.log(`[Sentry] Sentry 초기화 완료 (환경: ${process.env.NODE_ENV})`);
console.log(`[Sentry] 성능 샘플링 비율: ${tracesSampleRate * 100}%`);
}
핵심 포인트
1. 환경별 샘플링 비율
개발 환경에서는 모든 이벤트를 추적하지만(100%), 운영 환경에서는 비용 절감을 위해 10%만 샘플링하도록 설정했다.
이유는 간단하다. Sentry 무료 플랜은 월 5,000개 이벤트까지만 무료다. 운영 환경에서 트래픽이 늘어나면 금방 한도를 초과할 수 있기 때문이다.
2. 민감한 데이터 필터링
beforeSend 함수를 사용해서 에러 정보가 Sentry로 전송되기 전에 비밀번호나 토큰 같은 민감한 정보를 자동으로 제거했다.
특히 Authorization 헤더는 JWT 토큰이 그대로 노출될 수 있으니 반드시 필터링해야 한다.
3. 릴리스 추적
package.json의 버전 정보를 릴리스 태그로 사용했다. 이렇게 하면 Sentry 대시보드에서 “어느 버전에서 에러가 많이 나는가?”를 추적할 수 있다.
서버에 Sentry 통합하기
server.ts 수정
Express 서버 코드에 Sentry를 통합했다. 중요한 건 순서다. Sentry는 반드시 모든 미들웨어보다 먼저 초기화되어야 한다.
import express from 'express';
import * as Sentry from '@sentry/node';
import { initializeSentry } from './config/sentry';
const app = express();
// 1. Sentry 초기화 (가장 먼저!)
initializeSentry();
// 2. Sentry 요청 핸들러 등록 (Express 미들웨어보다 먼저)
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// 3. 일반 미들웨어 (body-parser, cors 등)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 4. 라우터 등록
app.use('/api/auth', authRouter);
app.use('/api/recipes', recipeRouter);
// 5. Sentry 에러 핸들러 (모든 라우터 뒤에!)
app.use(Sentry.Handlers.errorHandler());
// 6. 커스텀 에러 핸들러
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
success: false,
message: '서버 내부 오류가 발생했습니다.',
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
미들웨어 순서가 중요한 이유
Sentry의 requestHandler는 요청이 들어올 때 트랜잭션을 시작하고, errorHandler는 에러가 발생했을 때 Sentry로 전송한다.
만약 순서가 잘못되면 에러를 제대로 캡처하지 못한다.
테스트: Sentry가 정말 작동하는가?
설정을 다 했으면 이제 테스트를 해야 한다.
나는 테스트용 엔드포인트를 하나 만들었다. 보안을 위해 토큰 인증도 추가했다.
테스트 엔드포인트 (server.ts에 추가)
app.get('/debug-sentry', (req, res) => {
// 보안을 위한 토큰 검증
const token = req.query.token || req.headers['x-test-token'];
const expectedToken = process.env.SENTRY_TEST_TOKEN;
if (!expectedToken || token !== expectedToken) {
return res.status(403).json({
success: false,
message: '권한이 없습니다. 테스트 토큰이 필요합니다.',
});
}
// 의도적으로 에러 발생
throw new Error('Sentry 테스트 에러입니다! 이 에러는 Sentry 대시보드에 기록됩니다.');
});
환경 변수 설정 (.env)
# Sentry 설정
SENTRY_DSN=https://your-actual-dsn@sentry.io/project-id
SENTRY_TEST_TOKEN=your_secure_random_token_here
테스트 실행
# 방법 1: 쿼리 파라미터
curl "http://localhost:3000/debug-sentry?token=your_secure_random_token_here"
# 방법 2: 헤더
curl -H "x-test-token: your_secure_random_token_here" http://localhost:3000/debug-sentry
서버에서 에러가 발생하고, 몇 초 후에 Sentry 대시보드(https://sentry.io)의 Issues 탭에 에러가 나타났다.
에러 상세 정보에서 다음을 확인할 수 있었다:
- 스택 트레이스 (어느 파일 몇 번째 줄에서 에러가 났는지)
- 요청 URL과 메서드
- 서버 환경 정보
- 발생 시간
드디어 제대로 작동하는 것을 확인했다.
Sentry에서 모니터링할 수 있는 것들
1. 에러 추적
모든 런타임 에러가 자동으로 캡처된다. try-catch로 잡지 못한 에러도 Sentry가 알아서 잡아준다.
2. 성능 모니터링
API 응답 시간, 데이터베이스 쿼리 성능 등을 추적할 수 있다.
특히 어느 엔드포인트가 느린지, 어느 DB 쿼리가 병목인지 한눈에 볼 수 있어서 최적화할 때 유용하다.
3. 알림 설정
Sentry 대시보드에서 알림 규칙을 설정할 수 있다:
- 새로운 에러 발생 시 즉시 알림
- 에러 빈도가 급증할 때 알림
- 특정 에러 패턴 발견 시 알림
나는 이메일 알림을 설정해뒀다. (1인 개발자가 뭔 슬랙이냐..) 이제 서버에 문제가 생기면 내가 먼저 알 수 있다.
운영 환경 배포 시 주의사항
1. DSN 보안
.env 파일은 절대 Git에 커밋하면 안 된다. .gitignore에 .env가 포함되어 있는지 반드시 확인해야 한다.
2. 샘플링 비율 조정
무료 플랜에서는 월 5,000개 이벤트까지만 무료다.
만약 트래픽이 많아지면 샘플링 비율을 더 낮춰야 한다. 또는 유료 플랜으로 업그레이드하거나.
3. 민감한 데이터 필터링
beforeSend 함수에서 추가로 필터링해야 할 데이터가 있는지 꼭 확인해야 한다.
특히 사용자 개인정보나 결제 정보 같은 게 노출되면 큰일이다.
결론: 이제 안심하고 잘 수 있다
Sentry를 도입하기 전에는 항상 불안했다. “지금 서버가 잘 돌아가고 있나? 에러는 안 나고 있나?”
이제는 다르다. 에러가 발생하면 유저보다 내가 먼저 알림을 받는다.
물론 Sentry가 모든 문제를 해결해주는 건 아니다. 하지만 적어도 문제가 생겼다는 것을 빠르게 알 수 있다는 점만으로도 엄청난 가치가 있다.
이제 마지막으로 디버그 로그를 정리하고, 알파 테스트 준비가 완료됐다.
드디어 진짜 유저를 맞이할 준비가 끝났다.
마침.
다른 글 보기
출근은 없는데, 퇴근도 없다: 1인 프리랜서로의 일주일을 지내고...
자율근무의 자유를 얻었지만, 시간의 밀도는 더 가혹해졌다. 1인 개발자로 살며 깨달은 시간의 무게와 자율의 공포
1일 1포스팅 실패, 그리고 Rive라는 40시간의 늪 (feat. 외주비 400만원 아끼기)
개발자가 디자인 영역을 넘보다가 영혼까지 털린 기록. 8만원짜리 외주가 아까워 결국 내가 깎았다.
표정이 없으니 좀 무서운데... (Rive 표정 구현기)
그림 한 장으로 희로애락 구현하기: 개발자스러운 표정 연출법