[모니터링] Sentry 도입부터 Discord 에러 알림까지 — 서버 감시 시스템 구축기
"서버 죽었는데 아무도 몰랐다"에서 "에러나면 1분 안에 안다"까지
1. 모니터링을 시작한 계기
프론트엔드: "API 안 되는데요?"
백엔드: "엥? 언제부터요?"
프론트엔드: "...2시간 전부터요?"
백엔드: 😱
어느 날 서버가 죽어있었는데 아무도 몰랐다. 그날 이후, 모니터링 시스템 구축을 결심했다. (Issue #102)
2. 모니터링 도구 비교: 뭘 쓸까?
처음에는 여러 도구를 비교했다.
| 도구 | 무료 티어 | 장점 | 단점 |
| Sentry | 5,000 에러/월, 1유저 | 에러 추적 특화, Spring Boot 연동 쉬움 | Discord 연동은 Team 플랜($26/월) 필요 |
| Datadog | 14일 체험 | 올인원 (메트릭+로그+APM) | 무료 없음. 비쌈 ($15~/호스트/월) |
| New Relic | 100GB/월 무료 | 무료 티어 넉넉 | 설정 복잡, 학습 곡선 높음 |
| ELK Stack | 셀프호스팅 무료 | 로그 검색 강력 | 직접 운영해야 함. 리소스 많이 먹음 |
| GCP Cloud Monitoring | GCP 자원 무료 | GCP 인프라 지표 자동 수집 | 앱 레벨 에러 추적은 못 함 |
우리의 선택
앱 에러 추적 → Sentry (무료 Developer 플랜)
인프라 지표 → GCP Cloud Monitoring (무료)
실시간 알림 → Discord Webhook (직접 구현, 무료)
로그 중앙화 → GCP Cloud Logging (Docker gcplogs 드라이버)
핵심 전략: "무료로 최대한 커버하고, 부족한 건 직접 만들자"
3. Sentry 도입하기
왜 Sentry인가?
Spring Boot에서 에러 모니터링을 시작하기 가장 쉬운 도구였다.
의존성 하나 추가:
// build.gradle
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:8.28.0'
설정 추가:
# application.yml
sentry:
dsn: ${SENTRY_DSN} # Sentry 프로젝트 DSN
environment: ${SPRING_PROFILES_ACTIVE:local}
traces-sample-rate: 1.0 # 성능 추적 비율
send-default-pii: false # 개인정보 전송 안 함
in-app-includes: com.finders # 우리 코드만 하이라이트
logging:
minimum-event-level: warn # WARN 이상만 Sentry로
minimum-breadcrumb-level: info # Breadcrumb은 INFO부터
이것만으로 서버에서 발생하는 모든 Exception이 자동으로 Sentry에 기록된다.

Sentry 대시보드에서 에러 목록과 발생 빈도를 한눈에 볼 수 있다
환경별 샘플링 차이
| 환경 | 샘플링 비율 | 이유 |
| Dev | 100% | 모든 에러 다 봐야 디버깅 가능 |
| Prod | 30% | 비용 절감. 패턴만 파악하면 충분 |
무료 플랜은 월 5,000 에러까지라서, prod에서 100% 캡처하면 한도를 금방 넘긴다.
Sentry 초기 삽질: @ExceptionHandler와의 충돌
처음에 Sentry를 붙이고 나서, 에러가 캡처가 안 되는 문제가 있었다.
원인: Spring의 @ExceptionHandler가 에러를 처리해버리면, Sentry 입장에서는 "에러가 아닌 정상 응답"으로 보이는 것이었다!
# 해결: exception-resolver-order를 최우선으로 설정
sentry:
exception-resolver-order: -2147483648 # HIGHEST_PRECEDENCE
# → @ExceptionHandler보다 먼저 예외를 캡처!
이 설정 하나로 해결되었지만, 찾는 데 한참 걸렸다... (Issue #177)
4. Sentry Discord 알림 — 왜 포기했나
Sentry에서 에러가 기록되는 건 좋은데, 대시보드를 열어봐야 알 수 있다는 게 문제였다.
"에러 났을 때 바로 알림 받고 싶은데..."
Sentry의 알림 옵션
| 방법 | 비용 | 비고 |
| Email 알림 | 무료 ✅ | Developer 플랜에서 사용 가능 |
| Slack 연동 | $26/월 (Team 플랜) | Third-party integration 필요 |
| Discord 연동 | $26/월 (Team 플랜) | Third-party integration 필요 |
Discord 연동을 하려면 Team 플랜($26/월)이 필요했다! 대학생 팀에게 매달 $26은... 🥲
그래서 결론:
Sentry는 Email 알림만 사용 (무료)
Discord 알림은 직접 구현하기로!
5. Discord Webhook 직접 만들기
Discord 설정: 이게 제일 헷갈렸다
Discord에서 Webhook URL을 만드는 과정이 의외로 복잡했다.
💡 Discord 웹후크 설정 경로: 서버 설정(⚙️) → 앱 → 연동 → 웹후크 → "새 웹후크" → URL 복사
자주 헷갈리는 것들 정리:
| 이름 | 뭔가 | 어디서 찾나 |
| 서버 ID | Discord 서버의 고유 번호 | 서버 아이콘 우클릭 → "서버 ID 복사" |
| 채널 ID | 텍스트 채널의 고유 번호 | 채널 이름 우클릭 → "채널 ID 복사" |
| Webhook URL | 메시지를 보낼 수 있는 URL | 서버 설정 → 연동 → 웹후크 → URL 복사 |
주의! "채널 ID"와 "Webhook URL"은 완전히 다른 것이다.
Sentry 같은 서비스는 채널 ID를 요구하고, 직접 구현할 때는 Webhook URL을 쓴다.
Webhook URL 만드는 법
1. Discord 서버 설정 (⚙️) 클릭
2. 앱 > 연동 > 웹후크 클릭
3. "새 웹후크" 클릭
4. 이름 설정 (예: "Finders 에러 알림")
5. 채널 선택 (에러 알림을 받을 채널)
6. "웹후크 URL 복사" 클릭
⚠️ 개발자 모드가 필요할 수 있다! Discord 사용자 설정 → 고급 → 개발자 모드 켜기 이게 꺼져있으면 "ID 복사" 메뉴가 안 보인다. 찾기 어려워서 한참 헤맸다...
💡 개발자 모드 경로: Discord 사용자 설정(⚙️) → 앱 설정 → 고급 → 개발자 모드 토글 ON
6. 코드 구조: 어떻게 만들었나
전체 구조
src/main/java/com/finders/api/
├── infra/discord/
│ ├── DiscordProperties.java # 설정 (webhook-url, enabled)
│ ├── DiscordWebhookService.java # 알림 전송 로직
│ └── dto/
│ └── DiscordMessage.java # Discord Embed 메시지 포맷
└── global/exception/
└── GlobalExceptionHandler.java # 여기서 Discord 호출!
동작 흐름
1. 사용자 요청 → 서버에서 에러 발생 (500)
2. GlobalExceptionHandler가 catch
3. DiscordWebhookService.sendErrorNotification() 호출
4. Discord 채널에 Embed 메시지 전송 🔔
5. (동시에) Sentry에도 자동 기록
6. (동시에) 사용자에게는 깔끔한 에러 응답 반환
핵심 코드 1: GlobalExceptionHandler
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(
Exception e, HttpServletRequest request) {
log.error("[UnhandledException] {} - {}",
request.getRequestURI(), e.getMessage(), e);
try {
// 💡 에러 발생 즉시 Discord로 알림!
discordWebhookService.sendErrorNotification(
e, request.getMethod(), request.getRequestURI()
);
} catch (Exception ignored) {
// 알림 실패가 사용자 응답에 영향 주면 안 됨
}
return ResponseEntity.status(500)
.body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR));
}
핵심 설계:
Discord 알림이 실패해도 사용자 응답에 영향 없음 (try-catch로 감쌈)
500 에러만알림 (400대 에러는 클라이언트 실수이니 알림 불필요)
핵심 코드 2: 중복 알림 방지
같은 에러가 1초에 100번 나면 Discord도 100번 울릴까? 아니다!
private static final long DEDUPE_WINDOW_MS = 60_000; // 60초
private boolean isDuplicate(String errorKey) {
long now = System.currentTimeMillis();
Long lastSeen = recentErrors.putIfAbsent(errorKey, now);
if (lastSeen != null && now - lastSeen < DEDUPE_WINDOW_MS) {
return true; // 60초 내 같은 에러 → 스킵!
}
return false;
}
60초 내 같은 에러 (같은 Exception + 같은 URL)는 한 번만 알림
Discord Webhook Rate Limit(30요청/분)도 넘기지 않음
핵심 코드 3: 민감정보 마스킹
스택트레이스에 비밀번호나 토큰이 포함될 수 있다. 자동으로 걸러낸다:
private String filterSensitiveInfo(String stackTrace) {
return stackTrace
.replaceAll("(?i)password[=:]\\s*\\S+", "password=***")
.replaceAll("(?i)token[=:]\\s*\\S+", "token=***")
.replaceAll("(?i)secret[=:]\\s*\\S+", "secret=***")
.replaceAll("(?i)api[_-]?key[=:]\\s*\\S+", "api_key=***");
}
핵심 코드 4: Discord Embed 메시지
Discord에는 단순 텍스트보다 Embed 메시지가 훨씬 보기 좋다:
public record DiscordMessage(List<Embed> embeds) {
public record Embed(
String title, // "Server Error"
String description,
int color, // 0xE74C3C (빨간색)
List<Field> fields, // Exception, Method, URL, Message, Stack Trace
Instant timestamp
) {}
}
실제로 받는 알림은 이런 느낌이다:
🚨 Server Error
━━━━━━━━━━━━━━━━━━━
Exception NullPointerException
Method POST
URL /api/v1/reservations
Message Cannot invoke method on null
Stack Trace (첫 5줄만)
━━━━━━━━━━━━━━━━━━━
환경별 설정
# application.yml (공통 - 기본 비활성화)
discord:
webhook-url: ${DISCORD_WEBHOOK_URL:}
enabled: false
# application-prod.yml (운영에서만 활성화)
discord:
enabled: true
webhook-url: ${DISCORD_WEBHOOK_URL}
local이나 dev에서 에러 날 때마다 디코에 알림이 오면 스팸이 되니까, prod에서만 활성화했다.
7. 기술 선택 이유: 왜 직접 만들었나?
Issue #211에서 여러 방안을 검토했다:
| 방안 | 비용 | 판단 |
| Sentry Team + Discord 연동 | $26/월 | 대학생팀엔 부담 ❌ |
| discord-webhooks 라이브러리 | 무료 | OkHttp 기반, 기존 WebClient 패턴과 불일치 ❌ |
| WebClient로 직접 구현 | 무료 | 기존 프로젝트 패턴 유지, 의존성 최소화 ✅ |
직접 구현 시 고려한 것들:
| 고려사항 | 해결책 |
| 알림 실패 시 서비스 영향? | 비동기 전송 + try-catch |
| 스택트레이스에 비밀번호? | 정규식으로 자동 마스킹 |
| 같은 에러 반복 시 스팸? | 60초 중복 제거 (deduplication) |
| local에서도 알림? | enabled: false로 환경별 분리 |
8. 최종 모니터링 구조
[Spring Boot 서버]
│
├── 에러 발생 시
│ ├── Sentry ──→ 대시보드에서 상세 분석 📊
│ │ (스택트레이스, 빈도, 환경 정보)
│ │ 이메일 알림 (무료)
│ │
│ └── Discord Webhook ──→ 실시간 알림 🔔
│ (에러 요약 Embed 메시지)
│
├── 인프라 지표
│ └── GCP Cloud Monitoring ──→ 대시보드 📈
│ (CPU, 메모리, 디스크, 네트워크, 에러 로그 수)
│ Terraform으로 대시보드 코드 관리
│
└── 로그
└── Docker gcplogs ──→ GCP Cloud Logging 📝
(컨테이너 로그 자동 수집, 라벨로 필터링)
9. 마치며
비용 정리
| 도구 | 비용 | 역할 |
| Sentry Developer | 무료 | 에러 추적 + 이메일 알림 |
| Discord Webhook | 무료 (직접 구현) | 실시간 에러 알림 |
| GCP Cloud Monitoring | 무료 | 인프라 지표 대시보드 |
| GCP Cloud Logging | 무료 | 로그 중앙화 |
| 총 비용 | $0/월 |
$26/월짜리 Sentry Team 플랜 대신, 직접 구현해서 동일한 효과를 무료로 얻었다.
배운 것들
Discord Webhook URL 만들기가 의외로 어렵다 — 개발자 모드, 채널 ID vs 서버 ID vs Webhook URL 구분이 처음엔 많이 헷갈린다
Sentry의 무료 플랜은 생각보다 쓸 만하다 — 에러 추적 자체는 무료로 충분. 알림 연동만 유료
직접 만들면 우리 상황에 딱 맞출 수 있다 — 중복 제거, 민감정보 필터링, 환경별 분리 등 세밀한 제어 가능
모니터링은 빨리 도입할수록 좋다 — "나중에 하자"가 아니라, 서버가 죽은 걸 모르는 것보다 빨리 하는 게 낫다
에러가 나면:
Discord로 즉시 알림 → 1분 내 인지
Sentry에서 상세 분석 → 원인 파악
GCP Cloud Logging에서 로그 추적 → 재현 & 디버깅
"서버 죽었는데 몰랐다"는 이제 옛날 얘기가 되었다. 😄