자체 제작 logger를 피할 수 없을 때, 정말 필요한 최소 요건은 무엇인가: 실무 요건과 통합 테스트 관점

· · Windows 개발, 로깅, 통합 테스트, 테스트 설계, 신뢰성

기성 logging framework를 쓸 수 있다면 그쪽이 안전합니다. 그래도 애플리케이션 쪽 제약이나 운영상의 사정으로 자체 제작 logger를 피할 수 없는 장면이 있습니다. 거기서 처음에 고민하기 쉬운 점은, 어디까지 구현하면 「너무 거칠지도 않고, 너무 무겁지도 않은」 설계가 되는가 하는 점입니다.

이 글에서는 대상을 장애 조사용 애플리케이션 로그에 한정합니다. 감사 증적, 분산 트레이싱, 메트릭 기반, 클라우드 집약까지 한 번에 떠안지 않고, 우선 현장에서 도움이 되는 최소 구성을 정의한 뒤, 그 구성을 정말로 신뢰할 수 있게 하기 위한 통합 테스트 관점을 정리합니다.

먼저 결론

첫 버전에서 잡고 싶은 요점은 다음과 같습니다.

  • 형식은 UTF-8JSON Lines로 한다
  • 1 레코드 1 행을 무너뜨리지 않는다
  • 필수 항목은 시각, 레벨, 카테고리, 메시지, 구조화 fields, sessionId, processId
  • 기본은 1 프로세스 1 파일
  • 부하가 낮으면 동기 쓰기, 많이 나오면 single writer + bounded queue
  • Error / Critical과 세션 시작·종료는 동기 flush한다
  • 순환과 보관은 v1부터 넣는다
  • 저장 위치를 쓸 수 없을 때 몰래 다른 곳으로 도망가지 않는다

이 정도까지 좁히면 구현과 운영 양쪽에서 파탄이 잘 나지 않습니다.

우선 대상 범위를 좁힌다

자체 제작 logger가 어려워지기 쉬운 이유는, 처음부터 뭐든지 다루려고 하기 때문입니다. 진단 로그, 감사 로그, 성능 측정, 분산 트레이스, 사용자 행동 분석을 하나의 구조로 묶으려 하면 요건이 단번에 늘어납니다.

이번 대상은 애플리케이션 장애를 구분하기 위한 진단 로그입니다. 즉, 「언제」, 「어느 처리에서」, 「무엇이 일어났고」, 「그때 어떤 문맥이었는지」를 나중에 따라갈 수 있는 것을 우선합니다. 여기로 좁히는 것만으로도 첫 설계 판단이 꽤 편해집니다.

최소한으로 필요한 요건

1. 형식은 UTF-8 JSON Lines

평문 텍스트를 이어 붙이는 식으로도 로그를 남길 수는 있지만, 나중에 기계적으로 다루기가 어려워집니다. 반대로 처음부터 무거운 독자 바이너리 형식으로 만들면 운영 시 가시성이 떨어집니다.

그 중간으로 다루기 편한 것이 UTF-8JSON Lines입니다. 1 행이 1 레코드이면 텍스트로서도 읽기 쉽고, 나중에 스크립트나 도구로 분석하기도 쉬워집니다. 중간에 쓰기가 끊겨도 어느 행이 깨졌는지 구분하기 쉬운 점도 실무에 맞습니다.

2. 필수 항목을 처음에 고정한다

최소한 갖춰 두고 싶은 항목은 다음 7개입니다.

  • timestamp
  • level
  • category
  • message
  • fields
  • sessionId
  • processId

message만의 문자열 로그로 만들면, 나중에 검색 조건이 늘어났을 때 곤란해집니다. 반대로 항목을 너무 늘리면 호출 쪽의 부담이 급격히 올라갑니다. 처음에는 이 정도로 고정하고, 추가는 정말로 필요해진 다음에 검토하는 것이 안전합니다.

3. 1 프로세스 1 파일을 기본으로 한다

여러 프로세스에서 같은 파일에 추가 쓰기를 하게 하는 설계는 겉보기 이상으로 사고 요인이 많아집니다. 배타 제어, 부분 쓰기, 순환 타이밍, 비정상 종료 시 처리가 단번에 어려워지기 때문입니다.

우선은 1 프로세스 1 파일을 기본으로 하세요. 여러 프로세스를 묶고 싶다면 후단에서 집약하거나, 전용 집약 프로세스를 명시적으로 세우는 편이 안전합니다.

4. 쓰기 전략은 부하로 나눈다

로그 양이 적은 단계에서는 동기 쓰기 쪽이 알기 쉽고, 장애 조사도 편합니다. 억지로 비동기화하면 종료 직전의 로그를 잃거나, 예외 시 flush 조건이 애매해지기 쉽습니다.

한편으로 로그 양이 많아져 동기 I/O가 병목이 되면 single writer + bounded queue를 채택합니다. 이때 중요한 것은 큐가 넘쳤을 때의 방침을 먼저 정해 두는 것입니다. 오래된 로그를 버릴지, 새로운 로그를 떨굴지, 경고를 낼지를 애매하게 두지 마세요.

5. flush 조건을 정한다

ErrorCritical, 그리고 세션 시작·종료 로그는 동기 flush해 두면 장애 조사에 도움이 됩니다. 평소의 Info까지 전부 flush하면 느려지므로, 전부를 같은 취급으로 두지 않는 것이 현실적입니다.

6. 순환과 보관은 v1부터 넣는다

순환은 「나중에 넣으면 된다」고 생각되기 쉽지만, 운영에 들어가면 갑자기 곤란해지는 기능입니다. 크기, 일일, 기동마다 등 방식은 뭐든 좋으니, 적어도 「무한히 늘어나지 않는 것」과 「몇 개를 남길 것인가」가 정해진 상태로 두어야 합니다.

7. 저장 실패 시에 멋대로 대체 저장을 하지 않는다

로그 저장 위치를 쓸 수 없을 때, 몰래 다른 곳에 쓰는 설계는 나중에 조사를 어렵게 합니다. 운영 담당이 「있어야 할 곳」에 로그가 없다는 것만으로 장애 대응의 초동이 늦어집니다.

저장할 수 없다면 앱 쪽 알림, 이벤트 로그, 표준 오류 등 명시적으로 알 수 있는 수단으로 실패를 드러내세요. 적어도 「어디로 갔는지 모르는」 상태는 피해야 합니다.

v1의 최소 구성 이미지

첫 버전에서는 다음 정도로 충분한 경우가 많습니다.

  • UTF-8 JSON Lines
  • 1 프로세스 1 파일
  • 세션 단위의 파일명
  • 크기 기반 또는 기동 단위의 순환
  • 보관 개수의 상한
  • Error / Critical의 동기 flush
  • 구조화된 fields를 받을 수 있는 API

이 이상의 기능은 실제 운영에서 「정말로 곤란했던 일」이 보인 다음에 붙이는 편이, 결과적으로 유지보수하기 쉬워집니다.

흔한 NG

피하고 싶은 전형적인 예도 들어 둡니다.

  • message 문자열에 전부 욱여넣는다
  • 여러 프로세스에서 같은 파일을 공유한다
  • flush 조건을 정하지 않고 전면 비동기화한다
  • 순환과 보관을 뒤로 미룬다
  • 저장 실패 시 몰래 다른 폴더로 도망간다
  • 네트워크 전송이나 로컬 DB 저장까지 v1에 넣는다

어느 것이나 겉보기에 편리해 보이지만, 구분이나 운영을 무겁게 만들기 쉬운 항목입니다.

통합 테스트는 실제 파일·실제 스레드·실제 프로세스로 생각한다

logger는 유닛 테스트만으로는 안심하기 어려운 부품입니다. 문자열 정형이나 JSON화만 검증해도, 실제 운영에서 문제가 되는 것은 I/O, 병행성, 순환, 종료 시 flush, 권한 오류 쪽이기 때문입니다.

그래서 통합 테스트에서는 실제 파일, 실제 스레드, 필요하다면 실제 프로세스를 써서 확인하는 것이 중요합니다. 적어도 「평소에는 통과하지만 장애 시에는 신뢰할 수 없다」는 상태는 피하고 싶은 부분입니다.

통과시켜 두고 싶은 통합 테스트 항목

단일 쓰기의 건전성

  • 1 행이 1 JSON 레코드로 되어 있는가
  • UTF-8로 다시 읽을 수 있는가
  • 필수 항목이 매번 들어가 있는가
  • 개행 혼입으로 여러 줄로 깨져 있지 않은가

같은 프로세스 내의 병행 실행

  • 여러 스레드에서 동시에 써도 레코드가 깨지지 않는가
  • 레코드 수의 과부족이 없는가
  • queue 이용 시에 순서나 누락 방침이 사양대로인가

flush와 종료 시의 동작

  • Error / Critical가 즉시 반영되는가
  • 정상 종료 시에 queue 안이 비워지는가
  • 예외 종료에 가까운 경로에서도 필요한 종료 로그가 남는가

순환과 보관

  • 순환 조건을 충족하면 새로운 파일로 전환되는가
  • 보관 상한을 넘은 오래된 파일이 사양대로 삭제되는가
  • 순환 직전·직후에도 JSON 행이 깨지지 않는가

이상계

  • 저장 위치 디렉토리가 존재하지 않을 때의 처리
  • 쓰기 권한이 없을 때의 처리
  • 디스크 풀 상당으로 실패했을 때의 알림이나 반환값
  • queue 넘침 시의 동작

여러 프로세스의 처리

만약 사양이 1 프로세스 1 파일이라면, 다른 프로세스가 같은 파일에 들어가려 하지 않는 것 자체를 확인 대상으로 삼을 수 있습니다. 반대로 집약 프로세스 방식이라면 그쪽으로의 전달 실패까지 포함해 확인이 필요합니다.

v1에서 최소한 통과시키고 싶은 개수

처음에 전부 하려고 하면 테스트가 너무 무거워집니다. v1에서 최소한 통과시키고 싶은 것은 다음 6개 정도입니다.

  1. 단일 스레드에서의 정상 쓰기
  2. 여러 스레드 동시 쓰기
  3. Error / Critical의 flush
  4. 순환과 보관
  5. 저장 위치 이상 시의 실패 알림
  6. 정상 종료 시의 drain과 최종 flush

이 6개가 통과되기만 해도, 「문자열은 나오지만 운영에서 신뢰할 수 없는 logger」에서는 꽤 멀어질 수 있습니다.

정리

자체 제작 logger의 첫 목표는 고기능화가 아니라 「장애 시에 믿을 수 있는 것」입니다. 그러기 위해서는 형식을 UTF-8 JSON Lines로 고정하고, 필수 항목을 좁히고, 1 프로세스 1 파일을 기본으로 하며, flush·순환·보관·실패 시 동작을 일찌감치 정해 두는 것이 유효합니다.

그리고 그 설계가 정말로 기능하는지는 실제 파일·실제 스레드·실제 프로세스를 쓰는 통합 테스트에서 확인할 필요가 있습니다. 구현을 키우기 전에 우선 최소 구성과 최소한의 테스트 세트를 먼저 굳혀 두면, 나중에 무리 없이 키우기 쉬워집니다.

관련 기사

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

관련 토픽

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 주제와 연결되는 서비스

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

블로그 목록으로 돌아가기