DevInsight

개발하며 겪은 판단을 적어요

DevOps
조회 0약 4분 읽기

console.log를 놓아주는 밤, 미리 답해야 할 다섯 가지 질문

구조화 로깅은 라이브러리 교체가 아니라 결정의 연속이다. 로그 레벨을 누가 정하는지, 어떤 필드를 어떤 이름으로 담을지, 마스킹이 로그 전송 전에 걸리는지, 로그가 하루 얼마의 비용과 저장을 먹는지까지 정해두지 않으면 운영 중에 로그 스키마를 쪼개는 대가를 치른다. 실제 전환 시점에 막히는 지점들을 사전에 점검하는 체크리스트와 함께, 전환 후에도 뒤집히지 않을 최소 설계 원칙까지 담았다.

DevInsight에서 발행해요

#구조화로깅#로그레벨#마스킹#로그비용#observability#로그필드설계#로그스키마#개발운영

console.log를 놓아주는 밤은 대개 배포 후 첫 주말에 온다. 라이브러리를 갈아끼우는 일 자체는 한 시간이면 끝난다. 문제는 그다음이다. 구조화 로깅을 도입했는데도 로그 스키마가 두 갈래로 쪼개져 있고, 레벨은 누구 기준인지 알 수 없고, 수집기 어딘가에 민감정보가 통째로 박혀 있다. 라이브러리는 잘못이 없다. 전환 전에 답해두지 않은 결정이 그만큼 많았을 뿐이다.

전환은 도구의 문제가 아니라 결정의 연속이다. 문자열 한 줄이 JSON이 되면 뭘 담을지, 어느 기준으로 쌓을지, 누구에게 보여줄지를 골라야 한다. 그 다섯 가지를 지금 답해두지 않으면 나중에 로그 스키마를 뜯어고치는 대가를 치른다. 전환하는 밤에 결정하는 게 아니라, 그 전에 답을 모아두어야 할 것들이다.

레벨은 정의로 시작해 알림으로 끝난다

로그 레벨을 누가 정하는가. 대부분의 팀은 이 질문을 건너뛰고 라이브러리 기본값부터 쓴다. 그러면 debug는 되도록 적게, error는 되도록 크게라는 감각만 남는다. 기준이 없는 상태에서 세 명의 개발자가 같은 이벤트를 세 갈래로 분류하면, 레벨은 곧 아무 의미 없는 문자열이 된다. 그렇게 되면 알림 임계값도 같이 죽는다. error 한 건에 진동하는 알림이 하루 수천 건 쌓이면, 사람들은 그다음 날부터 알림 자체를 끈다.

기준은 짧을수록 오래 산다. info는 흐름이 바뀌는 지점, warn은 자동으로 복구되지만 눈여겨봐야 하는 지점, error는 이용자에게 이미 영향이 닿은 지점. 이 세 줄만 팀 전체가 함께 정해도 절반은 끝난다. 여기서 주의할 점은 이 정의를 혼자 세우지 말라는 것이다. 로그 레벨은 쓰는 사람 기준이 아니라, 그것을 소비하는 시스템 기준으로 정해져야 한다. 정의가 끝나면 검증이다. 같은 이벤트 두어 개를 뽑아 두 명이 분류해보고 일치율을 보는 일, 이것이 전환 전날의 테스트다. 전환 후 첫 주에는 알림이 살아 있는지, 죽어서 조용한 건 아닌지까지 루틴에 넣어라.

필드 이름이 어긋나면 대시보드가 멍든다

두 번째는 어떤 필드를 어떤 이름으로 담느냐다. 로그 필드 설계에서 가장 흔한 실수는 같은 의미를 두 이름으로 굴리는 것이다. orderId와 order_id, createdAt과 created_at. 처음에는 하나였는데, 어느 틈에 둘 다 존재하고, 집계 쿼리는 둘 중 하나만 잡는다. 대시보드 그래프가 느닷없이 중간에 끊기면 필드 이름부터 의심해보는 편이 이득이다.

운영에서 뽑을 필드는 처음에 아껴 넣고, 추가는 심사하는 쪽이 낫다. request_id, trace_id, 서비스명, 버전. 네 개로 시작해도 충분하다. 카디널리티가 무한히 뻗는 값, 이를테면 타임스탬프나 인스턴스명이나 개별 식별자를 라벨로 넣는 순간 그 라벨 수만큼 저장과 인덱스가 비싸진다. 필드는 도입일에 열 개를 넣는 것보다, 오늘 네 개를 넣고 필요할 때 하나씩 추가하는 쪽이 스키마를 오래 살린다. 다만 이때 필드 이름 규칙만은 문서로 남겨야 한다. 네이밍 컨벤션은 코드 리뷰로는 못 막는다.

마스킹은 전송 직전에만 통한다

다섯 질문 가운데 유일하게 늦으면 되돌릴 수 없는 것이 마스킹이다. 필드 이름은 고치면 되고, 레벨 기준은 다시 세우면 된다. 이미 수집기로 나간 로그는 그렇지 않다. 저장 스토리지, 백업, 파이프라인 어딘가에 원문이 남는 순간 끝이다. 그래서 전환 스펙에는 한 줄이 반드시 들어가야 한다. 마스킹은 로거가 로그를 내보내는 그 지점, 내보내기 직전에 건다.

const safe = maskFields(record, ["authorization", "cardNumber", "token"]); logger.info("order.created", safe);

maskFields는 로거를 통과하기 직전에 돈다. 이 줄이 전송 라이브러리 뒤쪽, 아니면 다른 파이프라인에 있다면 이미 늦은 것이다. 늦은 마스킹은 어차피 쓸 수 없다.

실패 모드는 대부분 패턴 기반 마스킹에서 나온다. 이메일이나 카드번호처럼 정형화된 값은 정규식이 잘 잡는다. 구조화 로깅의 위험은 정형 값이 아니라 커스텀 페이로드 안에 숨은 토큰이다. HTTP 응답 바디를 통째로 로그에 담았다면, JWT도, 표준 헤더 형식도 아닌 토큰이 그 안에 들어 있을 수 있다. 그건 어떤 패턴으로도 잡을 수 없다. 그래서 마스킹은 패턴과 필드 블랙리스트가 함께 가야 한다. 블랙리스트는 이름으로, 패턴은 형식 없는 값을 위한 보조망으로. 전환 PR에는 운영 로그를 복사한 세트에 마스킹 룰을 돌려 민감값 잔존을 스캔하는 회귀 테스트를 통과 조건으로 걸어라. 스테이징 데이터에서 정규식이 통과했다고 운영에서도 통과하는 게 아니다. 운영 데이터의 형식은 스테이징보다 한참 지저분하다.

하루 로그가 얼마를 먹는지 모르고 샀다

네 번째 질문, 하루 로그가 얼마의 비용과 저장을 먹는가. 전환 전에 계산해본 팀이 드물다. 어림은 간단하다. 하루 호출량에 줄 수, 평균 필드 수를 곱한다. 호출당 로그가 평균 500바이트고 하루 1천만 건이면 5GB다. 월간으로는 전송, 저장, 인덱싱, 조회 네 갈래의 지출이 쌓인다. 로그 비용은 결국 볼륨과 카디널리티와 보존 기간의 곱이다.

보존 기간은 규제와 조회 빈도에 따라 다르다. 여기 정답은 없다. 다만 무한히 늘어나는 값을 필드로 넣는 순간 보존 기간을 줄여도 예산이 따라오지 않는 상황이 된다는 것, 이건 상황을 가리지 않고 성립한다. 볼륨을 줄이고 싶다면 샘플링부터 시작하라. error는 전부, debug는 1%, 나머지는 그 사이. 샘플링은 전송 후 수집기에서 줄이는 것보다, 내보내는 지점에서 줄이는 쪽이 관리가 쉽다. 줄이는 규칙이 코드와 함께 리뷰에 오른다.

이 로그를 읽는 사람이 누구인가

마지막 질문은 소비자다. 이 로그를 읽는 게 사람인지, 알림을 울리는 시스템인지. 개발자가 로컬에서 보고 싶은 필드와 운영이 알림에 쓸 필드는 같지 않다. 둘을 한 스트림에 섞으면 레벨 필터만으로는 정리되지 않는 시점이 온다. 카디널리티가 높은 디버그 필드를 운영 필드로 착각해 집계에 태우는 사고가 바로 그 자리에서 나온다. 분리 기준은 한 줄이다. 이 로그에 시스템이 의존하는가. 알림이 이 로그로 갈라진다면 운영 스트림, 사람이 짜증 나서 뒤적이는 수준이면 디버그 스트림이다.

observability의 관점에서 보면 로그는 목적지에서 거꾸로 설계하는 편이 정답이다. 이 이벤트가 어떤 알림을 낳고, 어떤 대시보드를 채우고, 어떤 조회를 받을지 먼저 그리고 필드를 고른다. 그 과정을 건너뛰면 조회수만 늘고 답을 주지 못하는 로그가 쌓인다. 그건 다시 1번 질문, 레벨의 혼란으로 되돌아온다.

결국 개발운영에서 로그를 건드리는 일은 도구 교체가 아니라 결정이다. 전환 PR 하나에 규칙을 걸어두면 된다. 구조화 로깅 도입 PR에는 다섯 질문의 답이 리뷰 코멘트로 달려 있어야 한다는 것. 레벨 정의표, 필드 명세와 이름 근거, 마스킹 회귀 테스트 결과, 하루 볼륨 산정, 스트림 분리 기준. 다섯 답이 다 모이는 밤에만 console.log를 놓아줘라. 못 쓰는 답이 하나라도 있으면 그 밤은 아직 아니다. 어차피 두 달 뒤에 후회하니까.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

DevOps 전체 보기
DevOps

INFO, WARN, ERROR만으로는 부족하다

console.log에서 JSON 로그로 가는 건 시작일 뿐이다. 로그 레벨을 모호하게 정의하면 알람이 무의미해지고, 스키마 없이 쌓은 로그는 검색조차 불가능하다. 마스킹을 미루면 개인정보가 로그 플랫폼에 그대로 노출된다. 급증하는 로그 비용도 간과할 수 없다. 이 글은 로그 레벨 기준, 공통 필드, 마스킹, 비용 거버넌스 등 구조화 로깅 도입 전에 반드시 정해야 할 결정들을 기록한다.

#구조화로깅#로그레벨#observability#DevOps
DevOps

삽질 없이 CI를 줄이는 캐시 3종 세트

GitHub Actions에서 의존성 캐시와 빌드 캐시를 도입했는데도 정작 빌드가 느리거나, 캐시가 오히려 잘못된 결과를 재사용하며 깨지는 경험을 해봤다면 이 글이 답이다. cache와 setup-*의 동작 차이, 매트릭스 분할 전략, 캐시 무효화 판단 기준을 함정과 함께 정리해 실패 없이 CI 시간을 단축하는 법을 다룬다.

#GitHub Actions#CI#캐시#빌드 최적화
DevOps

프론트엔드 개발자가 배포에서 벗어나는 순간

Vercel이 'Develop. Preview. Ship.'으로 압축한 것은 단순한 마케팅 문구가 아니다. 로컬 개발부터 프로덕션 배포까지 원클릭으로 연결하는 경험은 프론트엔드 개발 문화를 재정의하고 있다. 이 글에서는 Vercel이 만들어낸 배포의 투명화와 그 이면에 있는 기술적 트레이드오프, 그리고 팀이 겪는 현실적인 도전을 짚어본다.

#Vercel#프론트엔드#배포#CI/CD

이전 글

잘게 썰수록 놓치는 문맥, 크게 썰수록 잡히는 노이즈

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독