웹 개발 데일리 브리핑 — 2026-10-19
Soshy··조회수 —

기준 시점: 2026-10-19 06:00 KST
조사 범위: 2026-10-18 06:00 ~ 2026-10-19 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro와 주요 프론트엔드 프레임워크의 공식 릴리스 채널을 확인했지만, 이번 24시간에 공개된 주요 정식 릴리스나 새로운 렌더링 기능은 확인하지 못했다.
React의 최근 공식 릴리스는 10월 14일에 공개된 19.3.1을 비롯한 유지보수 업데이트다. Vue는 10월 16일 3.6.0-rc.12를 공개했지만 이번 조사 범위에 포함되지 않는다. Next.js, SvelteKit, Astro에서도 이번 조사 기간에 해당하는 중요한 신규 발표는 확인하지 못했다.
JavaScript·TypeScript 및 Vite, Bun, Deno 등의 공식 릴리스 채널에서도 이번 조사 범위에 포함할 만한 주요 신규 버전은 확인하지 못했다.
개발 버전의 개별 Commit이나 소규모 유지보수 수정은 별도의 주요 발표로 포함하지 않았다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari의 공식 발표를 확인했지만, 이번 24시간에 해당하는 주요 웹 API의 정식 출시나 CSS 및 JavaScript 표준의 중요한 변경은 확인하지 못했다.
Chrome의 최근 개발자 발표와 WebKit의 Safari Technology Preview 릴리스는 이번 조사 기간 이전에 공개된 내용이다.
브라우저 엔진의 개별 소스코드 변경과 개발 빌드의 생성 여부도 검토했지만, 이를 새로운 웹 표준의 안정화나 정식 기능 출시와 동일하게 취급하지 않았다.
3️⃣ 백엔드·인프라
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
Cloudflare, Vercel, AWS, Google Cloud, Microsoft Azure와 주요 서버리스·엣지 플랫폼의 공식 발표를 확인했다.
Cloudflare의 공식 Changelog에서 이번 조사 기간 이전의 마지막 주요 업데이트는 10월 17일에 공개된 Local Explorer의 Cron Trigger 테스트 지원이다. 해당 소식은 이전 브리핑에서 이미 다뤘으므로 반복하지 않았다.
Vercel에서는 AI Gateway의 세션별 요청 분석 및 Session Affinity 관련 발표를 확인했지만, 이 역시 이전 브리핑에 포함된 내용이므로 제외했다.
Vercel AI Gateway에 새로운 AI 모델을 추가하는 소식도 확인했지만, 기존 웹 개발 플랫폼의 구조나 API를 실질적으로 변경하는 주요 업데이트보다는 모델 선택지 확대에 해당하므로 별도 항목으로 포함하지 않았다.
Node.js의 최근 주요 릴리스인 24.22.0 LTS와 26.12.0 Current는 10월 14일에 공개됐다. 이번 기간에 새롭게 발표된 주요 Node.js 릴리스는 확인하지 못했다.
AWS Lambda Web Functions 역시 10월 12일에 이미 공개된 기능이므로 이번 조사 범위의 신규 소식으로 포함하지 않았다.
4️⃣ 개발 도구 및 보안
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
GitHub, GitLab과 주요 개발 도구 및 보안 프로젝트의 공식 발표를 확인했지만, 이번 기간에 해당하는 중요한 신규 보안 권고나 개발 도구의 정식 출시 발표는 확인하지 못했다.
GitHub의 10월 공식 Changelog에서 최근 확인되는 주요 업데이트는 오픈소스 라이선스 준수 정책의 정식 출시 등 10월 16일까지의 발표다.
해당 기능은 이전 브리핑에서 이미 다뤘다.
React, Next.js, Vue, Angular 등 주요 프레임워크의 새로운 고위험 보안 권고도 이번 조사 기간에 해당하는 공식 발표를 확인하지 못했다.
다만 공식 보안 권고가 없다는 사실이 새로운 취약점이나 공격이 전혀 존재하지 않는다는 의미는 아니다. 이번 브리핑은 지정된 기간에 공개된 주요 신규 발표를 기준으로 한다.
작은 패치와 개발 버전의 개별 변경, 기존 보안 취약점의 재소개는 포함하지 않았다.
📚 추천 글
Reporting UI Errors Without Blowing Up Prometheus
- 저자: Jo4 Team
- 발행 매체: Jo4 Engineering Blog
- 게시일: 2026-10-16
- 원문: https://jo4.io/blog/ui-error-reporting-cardinality-safe/
React 애플리케이션에서 발생한 오류를 백엔드로 전송하고 Prometheus와 Grafana에서 관찰할 때 발생할 수 있는 메트릭 카디널리티 폭증, 로그 인젝션, 공개 API 남용 문제를 실제 구현 사례로 설명한 글이다.
웹 애플리케이션에서는 프론트엔드와 백엔드의 오류가 서로 다른 위치에서 발생한다.
예를 들어 서버 API가 HTTP 500 응답을 반환하면 백엔드의 로그와 모니터링 시스템에서 오류를 확인할 수 있다.
하지만 React 컴포넌트의 렌더링 과정에서 오류가 발생하는 경우는 다르다.
React Error Boundary가 오류를 처리하면 사용자에게 대체 UI가 표시될 수 있다.
이때 서버는 해당 오류가 발생했다는 사실을 자동으로 알지 못한다.
브라우저의 Console에 오류가 기록되더라도 개발자가 직접 사용자의 브라우저를 확인할 수 있는 것은 아니다.
따라서 실제 서비스에서는 프론트엔드에서 발생한 오류를 별도의 서버로 전달하는 구조가 필요할 수 있다.
저자는 이러한 목적으로 간단한 오류 수집 API를 구현했다.
하지만 오류 수집 API를 외부에 공개하면 새로운 보안 및 운영 문제가 발생한다.
React Error Boundary와 오류 수집 API
저자의 애플리케이션에서는 React Error Boundary가 포착한 오류를 백엔드로 전달한다.
오류 데이터에는 다음과 같은 정보가 포함된다.
- 오류 이름
- 오류 메시지
- 오류가 발생한 URL
- Stack Trace
백엔드는 해당 정보를 받아 로그와 메트릭으로 기록한다.
여기서 중요한 설계는 오류 전송을 일반적인 서비스 트랜잭션과 분리했다는 점이다.
오류 수집 요청이 실패했다고 해서 사용자가 수행하던 작업까지 실패하도록 만들 필요는 없다.
따라서 프론트엔드는 오류를 전송한 뒤 서버의 상세한 응답을 기다리지 않는다.
백엔드 역시 별도의 응답 데이터를 반환하지 않는다.
성공적으로 요청을 처리하면 다음 상태 코드를 반환한다.
204 No Content
HTTP 204는 요청은 처리됐지만 반환할 응답 본문이 없다는 의미다.
오류 수집처럼 클라이언트가 결과 데이터를 사용할 필요가 없는 작업에 적합하다.
그러나 이러한 구조에서는 새로운 문제가 발생한다.
오류 수집 API가 인증 없이 접근할 수 있는 공개 엔드포인트라면 누구나 임의의 데이터를 전송할 수 있다.
프론트엔드 오류 데이터는 신뢰할 수 없다
저자는 가장 먼저 클라이언트가 전달한 모든 필드를 신뢰할 수 없는 입력으로 취급해야 한다고 설명한다.
일반적인 사용자가 정상적인 React 애플리케이션에서 오류를 전송하는 경우에는 실제 JavaScript 오류가 전달된다.
하지만 공격자는 React 애플리케이션을 실행할 필요 없이 HTTP 요청을 직접 구성할 수 있다.
예를 들어 오류 이름에 매우 긴 문자열을 포함하거나, 오류 메시지에 특수문자를 넣거나, 임의의 URL과 Stack Trace를 전달할 수 있다.
이러한 데이터를 그대로 로그와 메트릭에 저장하면 문제가 발생할 수 있다.
특히 Prometheus에서는 더욱 주의해야 한다.
Prometheus의 Cardinality 문제
Prometheus는 애플리케이션의 메트릭을 수집하고 저장하는 시스템이다.
예를 들어 다음과 같은 정보를 측정할 수 있다.
- HTTP 요청 수
- API 오류 수
- 응답 시간
- 메모리 사용량
- 프론트엔드 오류 수
Prometheus에서는 메트릭에 Label을 추가할 수 있다.
Label은 같은 메트릭을 여러 조건으로 구분하기 위한 정보다.
예를 들어 HTTP 요청 수를 API 경로와 응답 상태 코드별로 구분할 수 있다.
하지만 Label의 값이 지나치게 다양해지면 문제가 발생한다.
Prometheus에서는 메트릭 이름과 Label 조합마다 별도의 Time Series가 생성되기 때문이다.
이러한 서로 다른 시계열의 개수를 Cardinality라고 한다.
예를 들어 오류 이름을 그대로 Label에 사용한다고 가정해 보자.
처음에는 다음과 같은 값이 들어올 수 있다.
- TypeError
- ReferenceError
- RangeError
이 정도라면 Label의 종류가 많지 않다.
하지만 공격자가 오류 이름으로 임의의 문자열을 보낼 수 있다면 상황이 달라진다.
공격자는 다음과 같이 계속 다른 값을 전달할 수 있다.
- error-1
- error-2
- error-3
- error-1000000
Prometheus는 각 값을 서로 다른 Label 조합으로 취급할 수 있다.
그 결과 매우 많은 시계열이 생성된다.
이는 메모리 사용량과 저장 공간을 증가시키고 메트릭 조회 성능을 저하시킬 수 있다.
심각한 경우 모니터링 시스템 자체에 장애가 발생할 수도 있다.
저자는 이러한 문제를 Metric Denial of Service로 설명한다.
원본 오류 메시지를 Label에 넣지 않는 이유
저자의 첫 번째 해결 방법은 전체 오류 수를 기록하는 Counter에서 Label을 제거하는 것이다.
전체 오류 수는 다음과 같은 하나의 Counter로 기록한다.
ui.errors
이 Counter는 오류가 발생할 때마다 증가한다.
오류 이름이나 URL을 Label로 사용하지 않는다.
따라서 공격자가 서로 다른 오류 메시지를 반복해서 전송하더라도 전체 오류 수 메트릭의 시계열 개수가 무제한으로 증가하지 않는다.
하지만 전체 오류 수만 기록하면 어떤 종류의 오류가 새롭게 발생했는지 확인하기 어렵다.
이를 해결하기 위해 저자는 별도의 신규 오류 탐지 메트릭을 사용했다.
이 메트릭에서는 Label을 허용하되 Label에 사용할 수 있는 값을 제한된 범위로 정규화한다.
예를 들어 오류 이름은 미리 정의한 Allowlist를 기준으로 분류한다.
알 수 없는 오류 이름은 별도의 공통 범주로 묶을 수 있다.
URL도 원본 문자열을 그대로 사용하지 않는다.
동적 URL을 정규화해야 하는 이유
다음 두 URL을 생각해 보자.
/users/123/profile
/users/456/profile
두 URL은 서로 다른 사용자 ID를 포함한다.
하지만 애플리케이션의 관점에서는 같은 페이지 유형일 수 있다.
사용자 프로필 화면에서 발생한 오류를 분석하려면 실제 사용자 ID보다 어떤 Route에서 오류가 발생했는지가 더 중요할 수 있다.
따라서 저자는 동적인 URL 구간을 하나의 공통 값으로 변경한다.
예를 들어 다음과 같은 형태다.
/users/:id/profile
이렇게 하면 서로 다른 사용자의 프로필 페이지에서 발생한 오류를 동일한 Route로 집계할 수 있다.
동시에 사용자 ID에 따라 Prometheus Label의 종류가 계속 증가하는 문제를 줄일 수 있다.
다만 정규화 과정에서 지나치게 많은 정보를 제거하면 실제로 서로 다른 오류를 같은 유형으로 취급할 수 있다.
따라서 어떤 URL 구간을 동적인 값으로 처리할지는 애플리케이션의 Route 구조에 맞춰 결정해야 한다.
저자는 추가적인 보호 장치로 Prometheus의 MeterFilter를 이용해 허용되는 Label 조합의 수를 제한했다.
즉 정규화 로직에 실수가 있더라도 시계열 수가 무제한으로 증가하지 않도록 방어 계층을 하나 더 추가한 것이다.
로그 인젝션 문제
두 번째 문제는 로그다.
실제 오류를 분석하려면 원본 오류 메시지와 Stack Trace가 필요할 수 있다.
하지만 공격자가 전달한 문자열을 그대로 로그에 기록하면 로그 인젝션(Log Injection)이 발생할 수 있다.
예를 들어 오류 메시지에 줄바꿈 문자가 포함돼 있다고 가정해 보자.
로그 시스템에서는 하나의 오류 메시지가 여러 줄의 로그로 분리될 수 있다.
공격자는 이를 악용해 실제로 발생하지 않은 오류 메시지나 관리자 작업 기록처럼 보이는 문자열을 삽입할 수 있다.
이 경우 개발자는 어떤 로그가 실제 서버에서 생성됐고 어떤 로그가 외부 입력을 통해 삽입됐는지 구분하기 어려워질 수 있다.
또한 지나치게 긴 오류 메시지는 로그 저장 공간을 불필요하게 소비할 수 있다.
저자는 이를 해결하기 위해 로그에 기록하기 전에 모든 입력 필드를 정리한다.
주요 처리 과정은 다음과 같다.
- 줄바꿈 문자 제거
- 제어 문자 제거
- Unicode 구분 문자 처리
- 문자열 길이 제한
- 정리된 값을 구조화된 로그에 기록
특히 단순한 제어 문자 정규식으로 처리되지 않을 수 있는 Unicode NEL(U+0085)도 고려한다.
이는 개발자가 흔히 사용하는 문자열 정리 방식이 모든 줄 구분 문자를 처리하지 못할 수 있기 때문이다.
로그 길이 제한
저자의 구현에서는 필드의 성격에 따라 최대 길이를 다르게 설정한다.
| 필드 유형 | 최대 길이 |
|---|---|
| 오류 이름 등 짧은 필드 | 512자 |
| 오류 메시지 및 Stack Trace | 4,000자 |
이러한 제한은 하나의 요청이 지나치게 많은 로그 데이터를 생성하는 문제를 줄이기 위한 것이다.
다만 길이를 제한하면 실제 오류 분석에 필요한 Stack Trace의 일부가 잘릴 수 있다.
따라서 모든 애플리케이션에서 동일한 길이가 적절한 것은 아니다.
실제 서비스의 오류 메시지와 Stack Trace 길이, 로그 저장 비용을 고려해 결정해야 한다.
또한 길이 제한이 개인정보 보호를 자동으로 보장하는 것은 아니다.
오류 메시지에는 사용자 입력이나 인증 정보가 포함될 수 있기 때문에 민감한 정보의 제거 및 마스킹 정책은 별도로 필요하다.
공개 오류 수집 API의 Rate Limiting
세 번째 문제는 요청량이다.
프론트엔드의 오류 수집 API는 일반적으로 사용자 인증 없이 호출될 수 있다.
따라서 공격자가 같은 엔드포인트에 많은 요청을 전송할 수 있다.
하지만 반드시 악의적인 공격자만 문제가 되는 것은 아니다.
애플리케이션 자체의 버그로 인해 오류가 반복적으로 발생할 수도 있다.
예를 들어 React 컴포넌트가 렌더링될 때마다 오류가 발생하고, Error Boundary가 다시 렌더링되면서 또 다른 오류가 발생하는 상황이다.
이 경우 브라우저 하나가 짧은 시간 동안 매우 많은 오류를 서버로 전송할 수 있다.
저자는 이를 방지하기 위해 IP별 Rate Limiting을 적용했다.
Rate Limiting은 일정 기간 동안 허용할 요청 수를 제한하는 기능이다.
한도를 초과한 요청은 로그와 메트릭에 기록하지 않고 버린다.
여기서 중요한 설계 선택은 오류 수집 API가 일반적인 비즈니스 API와 다른 응답 정책을 사용한다는 점이다.
일반적인 API에서는 Rate Limit을 초과했을 때 HTTP 429를 반환할 수 있다.
하지만 이번 오류 수집 API에서는 클라이언트가 결과를 기다리지 않는다.
따라서 Rate Limit을 초과한 요청에도 상세한 오류 응답을 반환할 필요가 없다.
저자는 이러한 경우에도 일관된 빈 응답을 사용하는 방식을 선택했다.
다만 이 방식은 수집되지 않은 오류가 있다는 사실을 클라이언트가 직접 확인하기 어렵게 만든다.
저자는 오류 수집을 사용자 트랜잭션이 아닌 보조적인 Telemetry로 취급하기 때문에 이러한 트레이드오프를 받아들였다.
실제 서비스에 적용할 때의 제한사항
이 글에서 제시한 구조는 일반적인 React Error Boundary와 백엔드 오류 수집 API를 연결하는 사례다.
하지만 모든 JavaScript 오류가 Error Boundary에서 포착되는 것은 아니다.
예를 들어 일부 비동기 작업과 이벤트 핸들러에서 발생한 오류는 별도의 처리 경로가 필요할 수 있다.
따라서 실제 서비스에서는 Error Boundary뿐 아니라 처리되지 않은 Promise Rejection이나 전역 오류 이벤트도 고려해야 한다.
또한 브라우저에서 서버로 전송하는 오류 수집 요청 자체가 네트워크 문제나 광고 차단 프로그램 등에 의해 차단될 수 있다.
따라서 이 방식으로 모든 프론트엔드 오류를 완벽하게 수집할 수 있다고 보장할 수는 없다.
이번 글은 실제 구현에서 발생할 수 있는 세 가지 문제를 구체적으로 설명한다.
- 메트릭 Label의 Cardinality 증가
- 사용자 입력을 통한 로그 인젝션
- 공개 Telemetry API의 요청량 폭증
특히 프론트엔드 오류를 수집하는 기능 자체가 백엔드의 새로운 공격 표면이 될 수 있다는 점을 실제 코드와 운영 설계로 보여준다.
React 애플리케이션에서 Sentry 같은 외부 오류 수집 서비스를 사용하지 않고 자체적인 Observability 시스템을 구축하려는 경우 참고할 만하다.
Staying Evergreen: How we automated secure access and repo hygiene in a shared org
- 저자: Greg Mohler
- 발행 매체: GitHub Engineering
- 게시일: 2026-10-15
- 원문: https://github.blog/engineering/platform-security/staying-evergreen-how-we-automated-secure-access-and-repo-hygiene-in-a-shared-org/
GitHub가 외부 협업자의 저장소 접근 권한을 주기적으로 검토하고, 더 이상 사용하지 않는 저장소를 자동으로 보관하기 위해 만든 Evergreen 시스템의 구조를 설명한 기술 글이다.
기업에서는 여러 팀과 외부 협력사가 GitHub Repository를 함께 사용하는 경우가 많다.
예를 들어 고객사와 공동 프로젝트를 진행하거나 외부 개발자가 특정 기간 동안 저장소에 접근해야 할 수 있다.
문제는 프로젝트가 종료된 이후에도 접근 권한이 유지될 수 있다는 점이다.
또한 오랫동안 사용하지 않은 Repository가 계속 남아 있을 수 있다.
이러한 저장소에는 오래된 코드와 의존성, 설정 파일 등이 포함돼 있을 수 있다.
따라서 사용하지 않는 저장소와 외부 접근 권한을 계속 유지하는 것은 보안 및 운영 관리 측면에서 부담이 될 수 있다.
GitHub는 이 문제를 해결하기 위해 Evergreen이라는 GitHub App을 만들었다.
이 시스템은 GitHub가 이미 제공하는 기능들을 조합해 구현했다.
새로운 관리용 웹 애플리케이션이나 별도의 대규모 데이터베이스를 구축하지 않은 것이 특징이다.
두 가지 자동화 작업
Evergreen은 크게 두 가지 작업을 수행한다.
첫 번째는 외부 협업자의 접근 권한 검토다.
각 Repository에 접근할 수 있는 외부 협업자를 주기적으로 확인한다.
이후 Repository의 담당자에게 현재 접근 권한이 필요한 사용자를 선택하도록 요청한다.
응답이 없거나 필요하지 않은 사용자라면 접근 권한을 제거한다.
두 번째는 오래된 Repository의 탐지와 보관이다.
사람이 일정 기간 동안 활동하지 않은 Repository를 찾아 담당자에게 알린다.
경고 이후에도 활동이 없다면 해당 Repository를 Archive 상태로 변경한다.
Archive된 Repository는 읽기 전용 상태가 된다.
하지만 저장소 자체가 삭제되는 것은 아니다.
GitHub는 이러한 구조를 설계하면서 두 가지 원칙을 정했다.
- 안전한 상태를 기본값으로 선택한다.
- 저장소와 데이터를 파괴하지 않는다.
예를 들어 접근 권한 검토에 아무도 응답하지 않았다면 기존 권한을 무기한 유지하지 않는다.
반대로 사용하지 않는 저장소는 완전히 삭제하지 않고 복구할 수 있는 Archive 상태로 변경한다.
GitHub Actions를 스케줄러로 사용
Evergreen의 실행 구조는 비교적 단순하다.
중앙에는 Go로 작성한 작은 Webhook 서버가 있다.
이 서버는 Container에서 실행된다.
그리고 별도의 Controller Repository가 존재한다.
Controller Repository는 Evergreen의 일정과 상태를 관리한다.
실제 작업은 GitHub Actions의 예약 실행 기능을 이용해 시작한다.
GitHub는 분기마다 다음과 같은 네 가지 작업을 수행하도록 구성했다.
| 시점 | 작업 | 동작 |
|---|---|---|
| 1일 | 오래된 저장소 검토 | 활동이 없는 Repository를 찾고 경고 Issue 생성 |
| 8일 | 접근 권한 검토 | 외부 협업자 목록을 확인하고 검토 Issue 생성 |
| 16일 | 오래된 저장소 정리 | 경고 이후에도 사용하지 않은 Repository Archive |
| 23일 | 접근 권한 정리 | 검토가 완료되지 않은 외부 접근 권한 제거 |
각 GitHub Actions Workflow는 repository_dispatch 이벤트를 전송한다.
Evergreen App은 해당 이벤트를 받아 실제 작업을 수행한다.
또한 관리자가 특정 작업을 직접 실행할 수 있도록 workflow_dispatch도 지원한다.
하지만 수동 실행 기능이 존재한다고 해서 정리 작업을 즉시 실행할 수 있도록 하지는 않았다.
GitHub는 작업 실행 시점과 실제 권한 제거 시점 사이에 별도의 유예 기간을 적용했다.
따라서 관리자가 실수로 Workflow를 실행하더라도 기존 협업자의 접근 권한이 즉시 제거되지 않도록 했다.
Git을 상태 저장소로 사용
Evergreen의 흥미로운 설계 가운데 하나는 상태 관리 방식이다.
일반적으로 자동화 시스템은 작업 상태를 저장하기 위해 PostgreSQL이나 Redis 같은 데이터베이스를 사용한다.
하지만 GitHub는 별도의 데이터베이스 대신 Controller Repository 안에 JSON 파일을 저장하는 방식을 선택했다.
각 Repository의 상태는 다음 경로에 기록된다.
state/repos/<repo_id>.json
여기에는 다음 정보가 포함된다.
- Repository ID
- Repository 이름
- 마지막 활동 시각
- 접근 권한 검토 시작 시각
- 검토 마감일
- 검토 대상 협업자
- 유지하기로 결정한 사용자
- 결정을 내린 담당자
- 권한 변경이 이루어진 시각
- 관련 GitHub Issue 번호
이렇게 하면 Git이 상태 변경 이력을 관리한다.
예를 들어 특정 사용자의 접근 권한이 지난 분기에 제거된 이유를 확인해야 한다고 가정해 보자.
관리자는 해당 Repository의 상태 파일에 대한 Git Commit 이력을 확인할 수 있다.
git log를 이용하면 언제 상태가 변경됐는지 확인할 수 있다.
git blame을 이용하면 특정 상태 정보가 어떤 Commit에서 변경됐는지 확인할 수 있다.
따라서 Git 자체가 자동화 시스템의 감사 로그 역할을 수행한다.
이 방식은 별도의 데이터베이스나 감사 로그 시스템을 만들지 않아도 된다는 장점이 있다.
하지만 모든 종류의 상태 관리에 적합한 것은 아니다.
상태 변경이 매우 자주 발생하거나 여러 프로세스가 동시에 같은 데이터를 수정해야 한다면 Git 기반 상태 관리에서 충돌과 동시성 문제가 발생할 수 있다.
Evergreen은 분기별로 실행되는 관리 작업이기 때문에 이러한 구조를 선택할 수 있었다.
GitHub Issue를 관리 UI로 사용
Evergreen은 별도의 관리자 Dashboard를 만들지 않았다.
대신 GitHub Issue를 사용자 인터페이스로 사용한다.
외부 협업자의 접근 권한을 검토해야 한다면 해당 Repository에 Issue를 생성한다.
Issue에는 접근 권한을 가진 사용자 목록이 Checklist 형태로 표시된다.
담당자는 계속 접근을 허용할 사용자를 선택한다.
이후 Issue를 닫으면 Evergreen이 issues.closed Webhook을 수신한다.
Evergreen은 Checklist를 분석해 선택된 사용자에게는 접근 권한을 유지하고, 선택되지 않은 사용자의 권한은 제거한다.
마지막으로 어떤 권한을 유지하고 제거했는지 Issue에 댓글로 기록한다.
이렇게 하면 사용자는 익숙한 GitHub 인터페이스에서 작업할 수 있다.
별도의 관리 시스템에 로그인하거나 새로운 도구를 학습할 필요가 없다.
또한 접근 권한을 검토한 이유와 최종 결정이 동일한 Issue에 기록된다.
Issue를 닫을 수 있는 사용자 제한
이러한 구조에는 보안 문제가 존재한다.
만약 누구나 접근 권한 검토 Issue를 닫을 수 있다면 외부 협업자가 자신의 접근 권한을 계속 유지하도록 선택할 가능성이 있기 때문이다.
GitHub는 이를 방지하기 위해 검토를 완료할 수 있는 사용자의 권한을 제한했다.
Evergreen은 Issue를 닫은 사용자가 승인된 GitHub 내부 구성원인지 확인한다.
허용되지 않은 사용자가 Issue를 닫으면 Evergreen이 해당 Issue를 다시 연다.
그리고 권한이 없는 사용자가 검토를 완료할 수 없다는 설명을 댓글로 남긴다.
즉 Issue를 닫는 행동 자체를 권한 변경의 최종 승인으로 인정하지 않는다.
해당 행동을 수행한 사용자의 권한도 함께 확인한다.
이는 일반적인 Webhook 자동화에서도 중요한 설계다.
Webhook 이벤트가 정상적으로 전달됐다는 사실과 이벤트를 발생시킨 사용자가 해당 작업을 수행할 권한을 가졌다는 사실은 서로 다르기 때문이다.
응답이 없으면 접근 권한 제거
Evergreen은 접근 권한 검토에 아무도 응답하지 않았을 때의 동작도 정의했다.
일반적으로 이러한 자동화에서는 담당자가 응답하지 않으면 기존 상태를 유지하도록 구현하기 쉽다.
하지만 GitHub는 반대 정책을 선택했다.
검토 기한까지 응답이 없다면 검토 대상자의 접근 권한을 제거한다.
이를 Fail Secure 또는 Fail Closed 방식이라고 할 수 있다.
불확실한 상황에서 보안상 더 안전한 상태를 선택하는 것이다.
그러나 이러한 정책은 실제 업무를 방해할 수도 있다.
담당자가 휴가 중이거나 알림을 확인하지 못했는데 접근 권한이 제거될 수 있기 때문이다.
GitHub는 이러한 문제를 줄이기 위해 사전 안내와 Reminder를 제공한다.
또한 담당자가 명시적으로 예외를 설정할 수 있도록 했다.
일부 권한 제거만 성공한 경우
GitHub가 특별히 주의한 부분은 작업이 중간에 실패하는 경우다.
예를 들어 외부 협업자 세 명의 권한을 제거해야 한다고 가정해 보자.
첫 번째 사용자의 권한 제거는 성공했다.
두 번째 사용자의 권한 제거도 성공했다.
하지만 세 번째 사용자에 대한 GitHub API 호출이 일시적으로 실패했다.
이 상황에서 전체 작업을 완료 상태로 기록하면 문제가 발생한다.
실제로는 세 번째 사용자의 권한이 여전히 유지되고 있기 때문이다.
Evergreen은 이러한 상황에서 작업을 완료 상태로 변경하지 않는다.
성공하지 못한 권한 제거 작업이 남아 있다면 검토 상태를 Pending으로 유지한다.
관련 Issue도 열린 상태로 유지된다.
이후 다음 실행에서 실패한 작업을 다시 시도한다.
모든 권한 제거가 정상적으로 완료된 이후에만 최종 상태를 기록한다.
이는 분산 시스템에서 일부 작업만 성공하는 Partial Failure를 처리하는 방식이다.
API 호출이 여러 개로 구성된 작업에서는 전체 작업을 하나의 데이터베이스 트랜잭션처럼 처리할 수 없는 경우가 많다.
따라서 중간 상태와 재시도 정책을 명확하게 정의해야 한다.
GitHub API Rate Limit 문제
Evergreen은 여러 Repository를 한꺼번에 관리한다.
따라서 API 요청량도 중요한 문제가 된다.
예를 들어 수백 개의 Repository에 접근 권한 검토 Issue를 생성한다고 가정해 보자.
각 Repository마다 Issue를 생성하고 댓글을 추가하면 짧은 시간에 많은 API 요청이 발생한다.
GitHub는 일반적인 API Rate Limit 외에도 콘텐츠 생성 요청에 대한 Secondary Rate Limit을 적용한다.
공식 글에서 언급한 제한은 다음과 같다.
- 분당 콘텐츠 생성 요청 80회
- 시간당 콘텐츠 생성 요청 500회
GitHub는 이러한 제한에 도달한 뒤 재시도하는 방식만 사용하지 않았다.
대신 처음부터 요청 속도를 제한하는 방식을 선택했다.
Evergreen은 Repository를 여러 Batch로 나누어 처리한다.
콘텐츠를 생성하는 API 호출 사이에 짧은 대기 시간을 둔다.
또한 Batch 사이에도 일정한 간격을 둔다.
이렇게 하면 대규모 작업이 동시에 실행되면서 API Rate Limit에 도달할 가능성을 줄일 수 있다.
실제로 콘텐츠를 생성한 Repository만 관련 요청량 계산에 포함하도록 구성했다.
따라서 변경할 대상이 거의 없는 상황에서는 불필요하게 오래 대기하지 않도록 했다.
실제 운영 환경에서의 검증
GitHub는 Evergreen을 적용하기 전에 Dry Run을 수행했다.
Dry Run은 실제 데이터를 변경하지 않고 어떤 작업이 실행될지 미리 확인하는 방식이다.
Evergreen은 DRY_RUN 모드를 제공한다.
이 설정을 활성화하면 접근 권한을 제거하거나 Repository를 Archive하는 대신 수행할 작업을 로그에 기록한다.
GitHub는 실제 조직을 대상으로 Dry Run을 실행하고 결과를 검토했다.
또한 Splunk를 이용해 구조화된 로그를 수집했다.
이 과정에서 다음과 같은 예외 상황을 발견했다.
- 외부 협업자가 없는 Repository
- 일반적인 구성과 다른 접근 권한
- Issue 기능이 비활성화된 Repository
- 예상과 다른 Repository 소유권 정보
GitHub는 이러한 사례를 실제 권한 제거 작업에 적용하기 전에 수정했다.
공식 글에 따르면 해당 조직에는 1,400명 이상의 GitHub 내부 구성원이 존재하며, 이외에도 많은 고객과 외부 협력사가 함께 작업한다.
따라서 자동화 시스템의 작은 오류도 여러 사용자에게 영향을 줄 수 있는 환경이었다.
저자는 이러한 작업에서는 기능 구현보다 변경 범위를 제한하고 복구 가능한 상태를 유지하는 것이 중요하다고 설명한다.
설계의 한계
Evergreen은 GitHub의 기본 기능을 적극적으로 재사용한다.
이 방식은 새로운 관리자 UI와 데이터베이스를 구축할 필요가 없다는 장점이 있다.
그러나 GitHub API와 Actions, Webhook의 가용성에 의존한다.
또한 JSON 파일을 Git Repository에 저장하는 구조는 상태 변경이 매우 많은 실시간 시스템에는 적합하지 않을 수 있다.
접근 권한 검토에 응답하지 않으면 자동으로 권한을 제거하는 정책도 모든 조직에 적합한 것은 아니다.
조직의 업무 방식과 보안 정책에 따라 더 긴 검토 기간이나 별도의 승인 절차가 필요할 수 있다.
그럼에도 이 글은 GitHub Actions, Webhook, Issue, Custom Property, Git Commit History를 조합해 별도의 복잡한 관리 시스템 없이 운영 자동화를 구현하는 방법을 구체적으로 보여준다.
특히 API Rate Limit, Partial Failure, 권한 검증, 감사 로그, Dry Run과 같은 운영상의 문제를 실제 시스템 설계에 어떻게 반영했는지 살펴볼 수 있다.
Alert on Brand-New Errors, Not Thresholds, with Redis
- 저자: Jo4 Team
- 발행 매체: Jo4 Engineering Blog
- 게시일: 2026-10-14
- 원문: https://jo4.io/blog/novelty-detector-first-seen-alerting/
웹서비스의 오류를 모니터링할 때 일정 횟수 이상의 오류가 발생했는지만 검사하면 새로운 유형의 오류를 놓칠 수 있다는 문제를 설명하고, Redis를 이용해 처음 등장한 오류를 감지하는 시스템을 구현한 글이다.
일반적인 운영 모니터링에서는 오류 발생 횟수를 기준으로 알림을 생성한다.
예를 들어 다음과 같은 규칙을 사용할 수 있다.
- 5분 동안 HTTP 500 오류가 100회 이상 발생하면 알림
- 1분 동안 로그인 실패가 50회 이상 발생하면 알림
- API 오류율이 5%를 초과하면 알림
이러한 방식은 대규모 장애를 발견하는 데 적합하다.
하지만 모든 중요한 오류가 높은 빈도로 발생하는 것은 아니다.
예를 들어 새로운 버전을 배포한 직후 특정 사용자에게만 JavaScript 오류가 발생했다고 가정해 보자.
오류는 단 한 번 발생했다.
기존 오류 알림 규칙은 이를 정상적인 노이즈로 판단할 수 있다.
하지만 실제로는 지금까지 발생한 적이 없는 새로운 버그일 수 있다.
저자는 이러한 문제를 해결하기 위해 오류의 발생 횟수와 별개로 새로운 유형의 오류가 처음 등장했는지 확인하는 Novelty Detection을 구현했다.
기존 Threshold 기반 알림의 한계
Threshold 기반 알림은 특정 수치가 기준을 초과했는지 확인한다.
예를 들어 다음과 같은 Prometheus 쿼리를 사용할 수 있다.
increase(errors_total[5m]) > 100
이 쿼리는 최근 5분 동안 오류 수가 100회 이상 증가했는지 확인한다.
하지만 새로운 오류가 단 한 번 발생했다면 해당 조건을 만족하지 않는다.
그렇다고 기준을 1로 낮추면 다른 문제가 발생한다.
기존에도 자주 발생하는 사소한 오류까지 모두 알림 대상이 될 수 있기 때문이다.
따라서 저자는 두 가지 질문을 구분해야 한다고 설명한다.
첫 번째 질문은 다음과 같다.
오류가 얼마나 많이 발생했는가?
두 번째 질문은 다르다.
지금까지 발생하지 않았던 새로운 오류가 등장했는가?
두 질문은 서로 다른 모니터링 방법이 필요하다.
저자는 두 번째 질문을 해결하기 위해 Redis를 사용했다.
Redis의 SET NX EX
Redis는 메모리 기반 데이터 저장소다.
Key-Value 구조로 데이터를 저장하며, 캐시와 세션, 메시지 처리 등에 널리 사용된다.
저자가 사용한 핵심 명령은 다음과 같다.
SET key value NX EX ttl
각 옵션의 의미는 다음과 같다.
| 옵션 | 의미 |
|---|---|
key | 저장할 데이터의 식별자 |
value | 저장할 값 |
NX | 해당 Key가 존재하지 않을 때만 저장 |
EX | 일정 시간이 지나면 Key 자동 만료 |
ttl | Key를 유지할 시간 |
여기서 가장 중요한 옵션은 NX다.
예를 들어 특정 오류 유형을 나타내는 Key가 Redis에 없다고 가정해 보자.
첫 번째 오류가 발생하면 Redis는 해당 Key를 생성한다.
이후 같은 오류가 다시 발생하면 이미 Key가 존재하므로 새로운 Key를 생성하지 않는다.
따라서 Redis가 Key를 새롭게 생성했는지 여부로 오류의 최초 등장 여부를 판단할 수 있다.
또한 이 명령은 원자적으로 실행된다.
원자적 실행은 여러 작업이 동시에 접근하더라도 하나의 명령이 중간 상태 없이 처리된다는 의미다.
예를 들어 같은 오류가 거의 동시에 열 번 발생했다고 가정해 보자.
애플리케이션에서 단순히 Key 존재 여부를 확인한 뒤 Key를 생성하는 방식이라면 여러 요청이 동시에 Key가 없다는 결과를 확인할 수 있다.
그 결과 같은 오류에 대해 여러 개의 최초 등장 알림이 발생할 가능성이 있다.
반면 Redis의 SET NX는 존재 여부 확인과 생성을 하나의 명령으로 처리한다.
따라서 동일한 Key에 대해 최초 생성에 성공한 요청만 새로운 오류로 판단할 수 있다.
TTL로 탐지 기간 관리
저자는 오류 유형을 영구적으로 저장하지 않는다.
대신 TTL(Time To Live)을 설정한다.
TTL은 데이터가 유지되는 시간을 의미한다.
예를 들어 TTL이 24시간이라고 가정해 보자.
특정 오류가 처음 발생하면 Redis에 Key가 생성된다.
이후 24시간 동안 같은 오류가 발생해도 새로운 오류로 판단하지 않는다.
24시간이 지나면 해당 Key는 만료된다.
그 이후 같은 오류가 다시 발생하면 새로운 최초 등장 이벤트로 감지될 수 있다.
즉 이번 구현의 의미는 서비스 역사상 최초로 발생한 오류를 영구적으로 판별하는 것이 아니다.
최근 일정 기간 동안 등장하지 않았던 오류가 다시 발생했는지 감지하는 방식이다.
이러한 구조는 오래된 오류가 다시 등장하는 회귀 문제를 발견하는 데도 활용할 수 있다.
다만 TTL이 너무 짧다면 같은 오류에 대해 반복적인 알림이 발생할 수 있다.
반대로 TTL이 지나치게 길면 동일한 유형의 새로운 문제가 발생했더라도 기존 오류로 분류될 가능성이 있다.
따라서 TTL은 오류의 종류와 서비스 운영 특성에 맞춰 결정해야 한다.
오류를 식별하는 Signature 생성
이번 구현에서 중요한 문제는 서로 다른 오류 메시지를 어떻게 같은 오류 유형으로 분류할 것인가이다.
예를 들어 다음 두 오류를 생각해 보자.
NullPointerException at line 42
NullPointerException at line 87
두 메시지는 서로 다른 문자열이다.
하지만 실제로는 같은 원인으로 발생한 오류일 수 있다.
반대로 오류 메시지 전체를 Redis Key로 사용하면 같은 버그가 여러 개의 새로운 오류로 분류될 수 있다.
따라서 저자는 오류 정보를 정규화한 뒤 Signature를 생성한다.
정규화 과정에서는 다음과 같은 동적인 데이터를 제거하거나 공통 값으로 변경한다.
- URL
- UUID
- 메모리 주소
- 숫자
- 연속된 공백
- 대문자와 소문자의 차이
예를 들어 다음 두 메시지를 생각할 수 있다.
Failed to load user 123
Failed to load user 456
정규화 이후에는 다음과 같은 형태로 처리할 수 있다.
failed to load user #
이렇게 하면 서로 다른 사용자 ID 때문에 같은 오류가 별도의 유형으로 분류되는 문제를 줄일 수 있다.
Hash를 이용한 오류 식별
정규화된 오류 문자열은 SHA-256으로 Hash 처리한다.
Hash는 입력 데이터를 일정한 길이의 값으로 변환하는 방식이다.
예를 들어 다음 정보가 정규화돼 있다고 가정해 보자.
- 오류 이름
- 오류가 발생한 위치
- 오류 유형
저자는 이러한 필드를 연결한 뒤 Hash를 생성한다.
Redis Key에는 원본 오류 메시지 전체 대신 Hash를 사용한다.
이 방식에는 여러 장점이 있다.
첫 번째는 Redis Key의 길이를 일정하게 유지할 수 있다는 점이다.
두 번째는 원본 오류 메시지를 Key에 직접 포함하지 않아도 된다는 점이다.
세 번째는 같은 입력에 대해 같은 Key를 생성할 수 있다는 점이다.
하지만 Hash를 사용했다고 해서 오류 유형이 자동으로 정확하게 분류되는 것은 아니다.
입력 문자열을 정규화하는 규칙에 따라 결과가 달라진다.
예를 들어 숫자를 모두 제거하는 방식은 사용자 ID가 다른 같은 오류를 묶는 데 유리하다.
반면 오류 코드의 숫자가 실제로 중요한 의미를 가진다면 서로 다른 오류가 같은 Signature로 묶일 수 있다.
따라서 어떤 값을 제거할지에 대한 판단이 중요하다.
Signature의 Granularity
저자는 Signature의 Granularity를 명시적으로 설계해야 한다고 설명한다.
Granularity는 데이터를 얼마나 세밀하게 구분할 것인지를 의미한다.
예를 들어 Java Exception 이름만 사용해 오류 유형을 구분한다고 가정해 보자.
NullPointerException
이 경우 서비스의 전혀 다른 두 위치에서 발생한 오류도 같은 유형으로 분류된다.
반대로 전체 Stack Trace와 오류 메시지를 모두 사용한다면 아주 작은 차이 때문에 같은 원인의 오류가 여러 유형으로 분리될 수 있다.
따라서 적절한 중간 수준을 선택해야 한다.
저자는 일반적으로 다음과 같은 정보를 조합할 수 있다고 설명한다.
- 오류 유형
- 오류가 발생한 위치
예를 들어 오류 이름과 정규화된 Route를 함께 사용하면 어떤 화면에서 새로운 오류가 발생했는지 구분할 수 있다.
이는 앞서 소개한 프론트엔드 오류 수집 구조와도 연결된다.
하지만 어떤 필드 조합이 가장 적합한지는 실제 애플리케이션의 요구사항에 따라 달라진다.
Prometheus에서 새로운 오류 알림 생성
Redis에서 새로운 오류가 발견되면 Prometheus Counter를 증가시킨다.
저자는 다음과 같은 메트릭을 사용한다.
jo4.novel.errors
이 메트릭은 새로운 오류 유형이 감지될 때 증가한다.
Grafana에서는 Prometheus의 increase()를 이용해 일정 기간 동안 새로운 오류가 발견됐는지 확인할 수 있다.
예를 들어 최근 5분 동안 새로운 오류가 한 번이라도 발생했다면 알림을 전송하도록 구성할 수 있다.
여기서 중요한 차이는 전체 오류 수가 아니라 새로운 Signature가 발견된 횟수를 측정한다는 점이다.
같은 오류가 수천 번 반복되더라도 새로운 유형이 아니라면 이 Counter는 증가하지 않는다.
반대로 새로운 오류가 한 번만 발생했더라도 Counter가 증가한다.
따라서 기존 Threshold 기반 오류 모니터링에서 놓치던 낮은 빈도의 신규 오류를 감지할 수 있다.
Counter를 0으로 미리 등록해야 하는 이유
저자가 실제 구현 과정에서 발견한 문제 가운데 하나는 Prometheus Counter가 처음 생성되는 시점이었다.
Prometheus의 increase()는 Counter의 값이 이전 측정 시점보다 얼마나 증가했는지 계산한다.
그런데 Counter가 처음 등장했을 때 이미 값이 1이라면 문제가 발생할 수 있다.
이전에 값이 0이었다는 측정 기록이 없기 때문이다.
예를 들어 다음과 같은 상황이다.
첫 번째 Prometheus 수집 시점에 메트릭이 존재하지 않는다.
이후 새로운 오류가 발생하면서 Counter가 생성된다.
다음 수집 시점에는 Counter의 값이 1이다.
하지만 Prometheus는 이전에 해당 Counter의 값이 0이었다는 데이터를 가지고 있지 않다.
따라서 처음 발생한 오류를 increase()가 기대한 방식으로 계산하지 못할 수 있다.
저자는 이를 해결하기 위해 애플리케이션 시작 시점에 모든 오류 유형의 Counter를 0으로 미리 등록했다.
이후 새로운 오류가 발생하면 0에서 1로 증가하는 변화가 기록된다.
이렇게 하면 신규 오류가 단 한 번 발생했더라도 알림 시스템이 이를 감지할 수 있다.
다만 Prometheus가 실제로 해당 증가를 관찰하려면 수집 주기와 데이터 보존 상태도 함께 고려해야 한다.
Redis 장애 시의 처리
이번 시스템에서는 Redis 자체가 장애를 일으킬 수도 있다.
예를 들어 Redis에 접근할 수 없다면 특정 오류가 처음 발생한 것인지 확인할 수 없다.
이때 애플리케이션이 어떤 행동을 해야 할지 결정해야 한다.
저자는 Redis 오류가 발생하면 해당 오류를 새로운 오류로 판단하지 않는 방식을 선택했다.
이는 Fail Closed 정책이다.
즉 탐지 시스템에 문제가 발생했을 때 불확실한 이벤트를 새로운 오류로 판단하지 않는다.
이렇게 하면 Redis 장애 중에 모든 오류가 새로운 오류로 분류되는 문제를 방지할 수 있다.
하지만 단점도 있다.
Redis가 사용할 수 없는 동안 실제로 새로운 오류가 발생해도 알림을 보내지 못할 수 있다.
저자는 이러한 트레이드오프를 받아들였다.
이번 시스템은 모든 오류를 영구적으로 기록하는 감사 로그가 아니라 운영자가 새로운 오류를 빠르게 발견하도록 돕는 보조적인 알림 시스템이기 때문이다.
따라서 중요한 오류를 반드시 기록해야 하는 시스템에서는 별도의 지속 저장소가 필요할 수 있다.
Redis Key가 예상보다 빨리 사라지는 경우
TTL을 설정했다고 해서 Key가 반드시 해당 시간 동안 유지되는 것은 아니다.
Redis의 메모리가 부족하거나 Eviction 정책이 활성화돼 있다면 일부 Key가 TTL 만료 전에 제거될 수 있다.
이 경우 이전에 발견했던 오류가 다시 새로운 오류로 분류될 수 있다.
따라서 이 구조는 정확히 한 번만 이벤트를 처리하는 Exactly-once 시스템이 아니다.
저자는 이를 명확하게 인정한다.
이번 구현은 새로운 오류를 빠르게 발견하기 위한 Best-effort Detection이다.
중복 알림이 발생할 가능성을 완전히 제거하는 대신 구현의 단순성과 운영 비용을 우선했다.
Cardinality와 신규 오류 탐지의 관계
이번 글은 새로운 오류 탐지 기능을 구현하면서도 Prometheus의 Cardinality 문제를 함께 고려한다.
Redis에는 정규화된 Signature의 Hash를 저장한다.
반면 Prometheus의 Label에는 제한된 범위의 값만 사용한다.
이렇게 하면 오류마다 새로운 Prometheus Time Series를 생성하지 않고도 새로운 오류가 등장했는지 기록할 수 있다.
즉 세밀한 오류 구분은 Redis가 담당하고, 집계와 알림은 Prometheus가 담당하도록 역할을 분리한 것이다.
이러한 역할 분리는 단순히 데이터를 여러 저장소에 나누어 저장하는 것과 다르다.
각 시스템이 잘 처리하는 작업을 구분한 것이다.
Redis는 원자적인 Key 생성과 TTL 관리에 적합하다.
Prometheus는 시계열 데이터의 집계와 알림 조건 평가에 적합하다.
두 기능을 조합해 새로운 오류 탐지 시스템을 구성했다.
적용 범위와 한계
이번 글에서 소개한 구현은 Java와 Redis, Prometheus, Grafana를 이용한 사례다.
하지만 핵심 원리는 특정 백엔드 언어에 종속되지 않는다.
Node.js나 다른 서버 환경에서도 동일한 개념을 적용할 수 있다.
다만 Signature 정규화 규칙과 TTL, Redis의 메모리 정책, Prometheus의 수집 주기는 실제 서비스에 맞춰 설계해야 한다.
또한 Redis 기반 Novelty Detection이 기존 오류 모니터링을 완전히 대체하는 것은 아니다.
새로운 오류가 한 번 발생했는지 확인하는 것과 기존 오류가 갑자기 수천 번 발생했는지 확인하는 것은 서로 다른 문제다.
따라서 실제 운영 환경에서는 두 종류의 알림을 함께 사용하는 편이 적합할 수 있다.
이 글은 발생 횟수가 많은 오류를 탐지하는 기존 모니터링 방식에서 벗어나, 한 번만 발생한 새로운 오류도 발견할 수 있도록 Redis와 Prometheus의 역할을 분리한 설계를 보여준다.
특히 Redis의 원자적 연산, 메트릭 Cardinality, Counter 초기화, 오류 Signature 정규화, 장애 발생 시의 Fallback 정책을 함께 이해할 수 있는 실무 사례다.