산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편

· 업데이트: · · Windows 개발, 장애 조사, 산업용 카메라, 핸들 누수, 로그 설계

수정 이력(9건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635109)
리뷰 지적을 반영해, 이날 추가한 그림 가운데 너비가 너무 컸던 것을 세로 구성으로 고치고, 일부 그림과 캡션 표현을 본문 서술과 맞게 바로잡았습니다. 본문 문장은 바꾸지 않았습니다.
누수 구조와 원인 분리 순서를 그림으로도 따라갈 수 있도록 Mermaid 그림을 14점 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 기존 그림에는 일련번호 캡션을 붙였습니다. 본문 문장은 바꾸지 않았습니다.
글 맨 앞에 '이 글의 지식 맵' 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건) 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
재연결 예에서, 콜백 등록 뒤에 취득 시작이 실패했을 때의 뒷정리를 고쳤습니다. SDK는 등록 때 넘긴 핸들을 유지하므로, 등록을 해제하지 않고 이벤트를 버리면 이미 해제한 핸들로 시그널됩니다. C++판·C#판 모두, 버리기 전에 등록을 해제하는 형태로 바꿨습니다.
핸들 수를 어디서 보는지 표를 추가했습니다(작업 관리자의 열 표시 방법, Process Explorer, `handle -s -p`, `Get-Process`, `typeperf`). 수천 개 누수로 왜 크래시하는지를, 커널 핸들 상한이 아니라 GDI나 SDK 내부 관리 테이블, 32bit 주소 공간이 먼저 한계에 닿는다는 식으로 정리하고, 수정 후 코드를 C++ RAII 타입과 C# 소유권 이전 형태로 추가했습니다.
본문 중 관련 글 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635108)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635108 https://comcomponent.com/ko/blog/handle-leak-industrial-camera-long-run-crash-part1/

DOI(최신 버전)
10.5281/zenodo.21635108
DOI(이 버전)
10.5281/zenodo.22217419

Windows 앱이 장시간 가동 뒤에 갑자기 크래시하면, 먼저 메모리 누수를 의심하고 싶어지는 경우가 꽤 많습니다. 다만 실제로는 핸들 누수가 주범이고, 몇 주가 지나서야 이차 장애로 겉에 드러나는 경우도 적지 않습니다.

이번에 소개하는 것은, 산업용 카메라를 제어하는 Windows 앱이 약 1개월 연속 가동 뒤에 갑자기 크래시한 장애를 조사한 사례입니다. 원인을 분리해 보니, 카메라 재연결 주변의 실패 경로에서 일어난 핸들 누수였습니다.

전편에서는 핸들 누수가 무엇인지, 이 장애의 원인을 어떻게 분리했는지, 재발을 막으려면 어떤 로그를 남겨야 하는지를 정리합니다. 후편에서는 Application Verifier로 만드는 Windows 이상 경로 테스트 기반으로, 이상 경로 테스트 기반을 다룹니다.

고유 명사와 일부 로그 항목은 가렸지만, 생각의 골격 자체는 Windows 장비 제어 앱 전반에서 꽤 공통입니다.

목차

  1. 먼저 결론(한 줄로)
  2. 핸들 누수란 무엇인가
    • 2.1. 여기서 말하는 ‘핸들’
    • 2.2. 왜 장시간 가동에서만 겉으로 드러나기 쉬운가
    • 2.3. 메모리 누수와의 차이
  3. 사례: 산업용 카메라 제어 앱이 1개월 뒤에 갑자기 크래시한다
    • 3.1. 일어나고 있던 증상
    • 3.2. 처음에 본 지표
    • 3.3. 진짜 원인이었던 누수 지점
  4. 원인을 어떻게 분리했는가
    • 4.1. 월 단위 재현을 기다리지 않고 시간을 줄인다
    • 4.2. Handle Count의 기울기로 본다
    • 4.3. create/openclose/dispose의 짝을 본다
    • 4.4. 핸들 누수는 ‘크래시한 곳’이 아니라 ‘샌 곳’을 찾는다
  5. 재발을 막기 위해 필요한 로그
    • 5.1. 먼저 남겨야 할 최소 세트
    • 5.2. 실제로 강화한 로그
    • 5.3. 어느 상세도로 남길 것인가
  6. 대략적인 구분
  7. 정리
  8. 참고 자료

이 글의 지식 맵

이 글은 산업용 카메라 제어 앱이 약 1개월의 연속 가동 후 갑자기 떨어진 사례를 바탕으로, 핸들 누수를 찾는 방법과 로그 설계를 정리한 것입니다. 핸들 누수는 timeout이나 reconnect의 failure path에서 CreateEvent의 핸들이 CloseHandle되지 않고 새는 것에서 일어나며, 돌아오지 않는 Handle Count로 나타나고, GUI가 동거하는 앱에서는 GDI 오브젝트의 상한에 먼저 도달해 이차 장애로 표면화됩니다. 대책은 RAII 형이나 finally로 소유권과 해제의 책임을 가까이 두어, CloseHandle 호출을 잊는 것 자체를 막는 것입니다. 장시간 운전 후 크래시를 구분할 때는 월 단위의 재현을 기다리지 않고 failure path를 짧은 루프로 대량으로 밟으며, Handle Count·Private Bytes·Thread Count와 구조화된 로그를 함께 보는 것이 토대가 되고, 후편의 Application Verifier는 그 위에 놓이는 수단으로 자리매김됩니다.

산업용 카메라 장기 가동 크래시 조사(핸들 누수 편)장시간 운전 후 크래시를 구분할 때 Handle Count와 Private Bytes·Thread Count를 함께 보는 것, failure path에서의 close 누락이 핸들 누수를 거쳐 이차 장애로 표면화되는 것, RAII와 CloseHandle에 의한 방지, 구조화된 로그와 Application Verifier의 자리매김을 보여주는 그림.원인이 될 수 있다에서 확인할 수 있다에서 확인할 수 있다전제로 한다권장되는 대응보다 먼저 해야 한다방지한다이용한다완화한다원인이 될 수 있다권장되는 대응방지한다원인이 될 수 있다권장되는 대응권장되는 대응권장되는 대응권장되는 대응권장되는 대응핸들 누수Handle Countfailure path(오류 흐름 중 실패 경로)메모리 누수Private BytesCreateEventCloseHandle구조화된 로그(structured log)Application VerifierRAII(Resource Acquisition Is Initialization)2차 장애(secondary failure)GDI 개체장시간 가동 후 크래시 원인 분리Thread Count

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 18건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론(한 줄로)

  • 장시간 가동 뒤에만 크래시하는 제어 앱에서는 Private Bytes뿐 아니라 Handle Count도 반드시 본다
  • 핸들 누수는 정상 경로가 아니라 timeout / reconnect / 중간 실패 / early return 경로에 숨기 쉽다
  • 실제로 크래시하는 줄은 샌 곳이 아니라, 나중에 새 핸들을 만들지 못하게 된 곳인 경우가 많다
  • 먼저 필요한 로그는 operation/session 문맥, process의 handle count, resource의 open/close 짝, Win32 / HRESULT / SDK 오류
  • 월 단위 재현을 기다리기보다, 연결·끊김·재연결·실패 경로를 짧은 루프로 수천 번 돌리는 편이 빠르다
  • 후편에서 다루는 Application Verifier는 꽤 유효하지만, 그 전에 자체 로그로 lifetime이 깨지는 모습을 쫓을 수 있게 해 두는 것이 토대입니다

요컨대 이런 건에서 먼저 할 일은, ‘오래 돌린 뒤에 크래시했다’는 사실을 바라보는 것이 아니라, 자원이 늘어나는 방식과 실패 경로를 관측할 수 있는 형태로 만드는 것입니다.

핸들 누수는 찾아냈을 때 이미 이차 장애의 얼굴을 하고 있는 경우가 많습니다. 그래서 크래시 순간의 예외만 보고 있으면, 꽤 빗나간 쪽으로 걷기 쉽습니다.

먼저 해야 할 일의 구도크래시 순간의 예외만 바라보면 빗나간 쪽으로 나아가기 쉽고, 먼저 자원이 늘어나는 방식과 실패 경로를 관측할 수 있는 형태로 만들어야 이차 장애의 얼굴을 한 핸들 누수에 도달할 수 있음을 나타낸다.크래시 순간의 예외만 본다빗나간 쪽으로 걷는다자원이 늘어나는 방식과 실패 경로를 관측누수의 정체에 도달할 수 있다

그림 1: ‘크래시했다는 사실’을 바라보기보다 먼저, 늘어나는 방식과 실패 경로를 관측할 수 있는 형태로 만든다.

2. 핸들 누수란 무엇인가

2.1. 여기서 말하는 ‘핸들’

여기서 말하는 핸들은, Windows 프로세스가 OS 자원을 가리키기 위한 식별자입니다. 대상이 되는 것은 예를 들어 다음입니다.

분류
커널 오브젝트 event, mutex, semaphore, thread, process, waitable timer
I/O file, pipe, socket, device에 대한 open
장비 제어에서 자주 나오는 것 카메라 SDK의 내부 event, callback 등록에 묶인 대기 오브젝트, 촬영 스레드 관련 핸들

제어 앱에서 특히 문제가 되기 쉬운 것은, ‘어떤 작업을 위해 잠시 연 자원을, 중간 실패 경로에서 닫지 않는’ 패턴입니다.

전형적으로는 이런 흐름입니다.

  • 재연결할 때마다 event를 1개 만든다
  • callback 등록이나 촬영 시작이 도중에 실패한다
  • success path에서는 close되지만, failure path에서는 close되지 않는다
  • 평소의 짧은 테스트는 성공 경로만 지나므로 놓친다

이 유형은 코드 리뷰에서도 실제 운영에서도, 꽤 흔하게 숨습니다.

실패 경로에서만 새는 전형 패턴재연결할 때마다 event를 만들고, callback 등록이나 촬영 시작이 성공하면 close되지만, 중간 실패 경로에서는 close되지 않고 새며, 짧은 테스트는 성공 경로만 지나 놓친다는 것을 나타낸다.성공중간 실패재연결할 때마다 event를 만든다등록·시작은 성공했는가success path에서 close된다failure path에서 close되지 않는다짧은 테스트는 성공 경로만 지나 놓친다

그림 2: 잠시 연 자원을 중간 실패 경로에서 닫지 않는다. 제어 앱에서 특히 많은 형태.

2.2. 왜 장시간 가동에서만 겉으로 드러나기 쉬운가

핸들 누수는 한 번에 화려하게 부서진다고 할 수 없습니다. 오히려 까다로운 것은, 한 번 실패할 때 하나만 새는 완만한 기울기의 누수입니다.

평소 가동가끔 timeout / reconnect실패 경로에서 Event Handle을 만든다CloseHandle이 호출되지 않는다Handle Count가 조금만 늘어난다몇백 번 반복한다CreateEvent / SDK open이 실패다른 곳에서 크래시 / 정지

그림 3: 한 번에 하나인 작은 누수가, 24/7 가동의 경계 조건에서 몇백 번 쌓여 겉으로 드러난다.

재연결 한 번에 하나만 샌다면, 몇 분 안에는 아무 일도 없습니다. 다만 24/7로 도는 장비 제어 앱에서는 timeout, 재초기화, 끊김 복구 같은 경계 조건이 여러 번 일어납니다. 그 결과 몇 주 뒤에만 겉으로 드러난다는, 묘한 모습이 됩니다.

여기서 중요한 것은, 핸들 누수 자체가 크래시 줄이 된다고 할 수 없다는 점입니다. 많은 것은 이런 부서짐입니다.

  • 새 event / file / thread를 만드는 API가 실패한다
  • SDK가 내부에서 필요한 자원을 만들지 못하고, 일반적인 실패 코드만 돌려준다
  • 실패 뒤의 오류 처리가 얇아 null / invalid handle을 밟고 크래시한다
  • timeout이 늘어 결과적으로 watchdog이나 상위 제어에 kill된다

즉 크래시 지점은 ‘마지막 피해자’이지, ‘최초의 범인’이라고 할 수 없습니다.

크래시 지점은 마지막 피해자어딘가에서 핸들을 계속 새게 두면 이윽고 새 자원을 만드는 API가 실패하고, 오류 처리가 얇은 다른 곳에서 크래시나 정지로 겉에 드러나므로, 크래시한 줄은 최초의 범인이 아니라 마지막 피해자일 뿐임을 나타낸다.어딘가에서 핸들을 계속 새게 둔다새 자원을 만드는 API가 실패다른 곳에서 크래시·정지크래시한 줄은 마지막 피해자에 지나지 않는다

그림 4: 누수 자체가 크래시 줄이 된다고 할 수 없다. 부서지는 방식은 대개 이차 장애.

여기서 당연한 의문이 나옵니다. 고작 수천 개 핸들로 왜 크래시하는가입니다.

숫자만 보면 상한은 꽤 멉니다. 커널 오브젝트 핸들은 프로세스당 2^24(약 1677만)이 이론상 상한입니다. 다만 핸들은 페이지 풀에 놓이므로, 실제로 만들 수 있는 수는 쓸 수 있는 메모리에 따라 정해지고, 32bit Windows에서는 이론값보다 훨씬 적어집니다.

요컨대, 이론 상한에 닿아 크래시하는 경우는 오히려 소수입니다. 실제로 먼저 한계에 부딪히는 것은, 대개 다음 중 하나입니다.

먼저 한계에 부딪히는 것 가늠 해당되는 상황
GDI 객체 세션당 이론상 65,536. 여기에 프로세스당 기본 상한이 있고, 레지스트리의 GDIProcessHandleQuota로 256~65,536 범위에서 바꿀 수 있다 GUI가 같이 있는 앱. 수천 개 자릿수에서 흔히 한계에 부딪힙니다
SDK 내부의 관리 테이블 벤더에 따라 다름 카메라 SDK가 내부에 가진 핸들 테이블이나 고정 길이 배열이 먼저 채워진다
페이지 풀 같은 커널 자원 머신 전체에서 공유 핸들 이외의 자원도 함께 먹고 있는 경우
32bit 프로세스의 가상 주소 공간 2GB / 3GB 핸들 자체보다, 핸들에 붙어 확보되는 버퍼가 영향을 준다

즉 ‘상한까지 아직 여유가 있으니 괜찮다’는 읽기는 성립하지 않습니다. 상한에 닿는지가 아니라, 돌아와야 할 것이 돌아와 있는지로 봐야 합니다. 기울기가 선 시점에서 이미 이상이라고 보는 편이 안전합니다.

이론 상한보다 먼저 한계에 부딪히는 것커널 핸들의 이론 상한에 닿아 크래시하는 경우는 소수이고, 실제로는 GDI 객체 상한, SDK 내부의 관리 테이블, 32bit 프로세스의 주소 공간 등이 먼저 한계에 부딪히므로, 돌아와야 할 것이 돌아와 있는지로 봐야 함을 나타낸다.커널의 이론 상한은 약 1677만거기까지 닿는 경우는 소수실제로 먼저 한계에 부딪히는 것GDI의 상한SDK 내부의 관리 테이블32bit의 주소 공간돌아오는지 돌아오지 않는지로 판단한다

그림 5: ‘상한까지 여유가 있다’는 안심할 근거가 되지 않는다. 기울기가 선 시점에서 이상으로 본다.

2.3. 메모리 누수와의 차이

장시간 가동 뒤의 장애에서는 먼저 메모리 누수를 의심하고 싶어집니다. 물론 그 자체는 자연스럽지만, 핸들 누수는 다른 축으로 보는 편이 빠를 때가 있습니다.

관점 메모리 누수 핸들 누수
먼저 보는 지표 Private Bytes, Commit, Working Set Handle Count
전형 증상 메모리 압박, paging, 느려짐, OOM Create* / Open* / SDK 내부 초기화 실패, 이차 장애
숨기 쉬운 곳 캐시, 참조 유지, 해제 누락 create/openclose/dispose의 비대칭
보이는 모습 메모리가 조금씩 늘어난다 handle count가 조금씩 늘어난 뒤 돌아오지 않는다

그래서 장시간 가동의 원인을 분리할 때는 ‘메모리만 본다’로는 한쪽만 보고 가는 상태가 되기 쉽습니다. 적어도 Handle CountThread Count는 함께 보는 편이 꽤 정리하기 쉬워집니다.

장시간 가동의 원인 분리에서 함께 보는 지표메모리 누수와 핸들 누수는 보는 지표가 다르므로, Private Bytes 같은 메모리 계열뿐 아니라 Handle Count와 Thread Count도 함께 보면 한쪽만 보고 가는 상태를 피할 수 있음을 나타낸다.장시간 가동의 원인 분리메모리 계열(Private Bytes 등)Handle CountThread Count늘어난 뒤 돌아오지 않으면 핸들 누수

그림 6: 메모리만 보는 것은 한쪽 눈으로 운전하는 셈이다. 핸들과 스레드 수도 같은 화면에서 쫓는다.

3. 사례: 산업용 카메라 제어 앱이 1개월 뒤에 갑자기 크래시한다

3.1. 일어나고 있던 증상

장애는 단순했습니다.

  • 산업용 카메라를 제어하는 Windows 앱이 24/7로 돌고 있다
  • 평소에는 평범하게 동작한다
  • 약 1개월쯤 지나면, 어느 날 갑자기 앱이 크래시한다
  • 다시 시작하면, 또 한동안은 동작한다

먼저 곤란한 것은 ‘크래시할 때까지가 길다’는 점입니다. 한 번 재현에 1개월을 기다리는 것은, 조사로서 꽤 가혹합니다.

게다가 까다로웠던 것은, 크래시하는 곳이 매번 딱 같지는 않았다는 점입니다. 어떤 때는 재연결 시작 직후, 어떤 때는 촬영 시작 시, 어떤 때는 SDK 호출 실패 뒤였습니다.

이 모습이면 처음에는 다음 중 어느 것도 의심할 수 있습니다.

  • 카메라 SDK 쪽의 불안정
  • 통신이나 디바이스 끊김에서 온 일시 장애
  • 메모리 누수
  • 스레드 주변의 race
  • 로그에 나오지 않은 초기화 실패

즉, ‘왠지 수상한 것’이 너무 많은 상태였습니다.

이 사례의 조사를 어렵게 한 두 가지약 1개월 연속 가동 뒤에 갑자기 크래시하므로 한 번 재현에 1개월이 걸리는 것과, 크래시하는 곳이 매번 딱 같지 않은 것이 겹쳐, SDK나 통신이나 메모리 등 수상한 후보가 너무 많은 상태였음을 나타낸다.약 1개월 뒤에 갑자기 크래시한다한 번 재현에 1개월이 걸린다크래시하는 곳이 매번 조금 다르다수상한 후보가 너무 많은 상태가 된다

그림 7: ‘크래시할 때까지가 길다’‘크래시하는 곳이 흔들린다’ 두 가지가 겹치면, 감으로 찍어서는 나아갈 수 없다.

3.2. 처음에 본 지표

그래서 처음에 한 것은, process 전체의 자원이 늘어나는 방식을 보는 일이었습니다. 이번 사례에서 관측 결과는 대체로 이런 경향이었습니다.

지표 관측된 경향 해석
Handle Count reconnect나 timeout 뒤에 조금씩 늘어나고, 돌아오지 않는다 핸들 누수를 의심한다
Private Bytes 증감은 있지만, 단조 증가의 기울기는 약하다 주범이 heap이라고 할 수는 없다
Thread Count 거의 변동 없음 thread leak 가능성은 낮다
크래시하는 곳 매번 조금 다르다 이차 장애 가능성이 높다

이 시점에서 시선은 꽤 좁혀졌습니다. ‘1개월 뒤에 크래시한다’가 아니라, ‘도중에 무언가를 조금씩 새게 두고, 그 결과 1개월 뒤에 크래시한다’고 보는 편이 자연스러웠기 때문입니다.

처음의 지표 관측에서 좁혀진 가늠Handle Count만 늘어난 뒤 돌아오지 않고, Private Bytes의 기울기는 약하고, Thread Count는 변동이 거의 없으며, 크래시하는 곳이 매번 다르다는 관측에서, 조금씩 샌 결과 1개월 뒤에 크래시한다는 가늠으로 좁혀졌음을 나타낸다.Handle Count가 늘어난 뒤 돌아오지 않는다시선이 좁혀진다Private Bytes의 기울기는 약하다Thread Count는 거의 변동 없음조금씩 새게 두어 1개월 뒤에 크래시한다

그림 8: 네 지표의 형태를 늘어놓으면, ‘1개월 뒤에 크래시한다’가 아니라 ‘계속 새고 있다’가 보인다.

3.3. 진짜 원인이었던 누수 지점

최종적으로 원인이었던 것은, 카메라 재연결 시 초기화 실패 경로에서 만든 event handle의 close 누락이었습니다.

흐름을 단순화하면 이렇게 됩니다.

카메라 SDKWindows제어 앱카메라 SDKWindows제어 앱failure path에서 returnCloseHandle이 호출되지 않는다loop[여러 번 reconnect]CreateEventcallback 등록중간 실패 / timeoutHandle Count가 조금씩 늘어난다다음 CreateEvent / Open실패이차 장애로서 크래시

그림 9: 진짜 원인은 재연결 실패 경로에서의 event handle close 누락. 쌓인 끝에 다른 곳에서 크래시한다.

코드의 이미지로는, 이런 누수입니다.

handle = CreateEvent(...)

if (!RegisterCallback(handle))
{
    return Error;   // CloseHandle(handle)이 빠져 있다
}

if (!StartAcquisition())
{
    return Error;   // 여기서도 close가 빠진다
}

...
CloseHandle(handle)

짧은 테스트에서 놓치기 쉬운 이유도, 꽤 알기 쉽습니다.

  • 정상 시작 -> 정상 종료에서는 close된다
  • 실패하는 것은 reconnect 도중뿐
  • 그 failure path를 대량으로 밟는 테스트가 없다
  • 운영에서는 몇 주에 걸쳐 조금씩 쌓인다

즉, ‘정상 경로만 보면 보이지 않지만, 이상 경로에서는 흔히 샌다’는 구조였습니다.

수정 방침은 화려하지 않습니다.

  • create/openclose/dispose의 책임을 가깝게 둔다
  • 중간 실패여도 반드시 해제되도록 finally / destructor / session object 쪽으로 모은다
  • callback 등록이나 촬영 시작의 전후로 ownership을 명확히 한다
  • ‘누가 닫는가’를 comments가 아니라 코드의 책임으로 나타낸다
수정 방침의 골격create와 close의 책임을 가깝게 두고, 중간 실패여도 반드시 해제되도록 finally나 소멸자나 session object 쪽으로 모으며, 누가 닫을지를 주석이 아니라 코드의 책임으로 나타낸다는 수정 방침을 보인다.수정 방침create와 close의 책임을 가깝게 둔다해제를 finally나 소멸자로 모은다소유권을 코드의 책임으로 나타낸다주석에 맡기는 규약으로 두지 않는다

그림 10: 화려한 수정이 아니다. 자원의 수명을 코드 구조 자체에 심는다.

문장만으로는 알기 어려우니, 같은 처리를 다시 쓴 형태도 두겠습니다.

C++라면, 핸들을 가진 작은 RAII 타입을 하나 마련하고, 생 HANDLE을 함수 안에 두지 않도록 합니다.

// C++17 / Windows
#include <windows.h>
#include <utility>

class UniqueHandle
{
public:
    UniqueHandle() noexcept = default;
    explicit UniqueHandle(HANDLE h) noexcept : h_(h) {}

    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;

    UniqueHandle(UniqueHandle&& other) noexcept
        : h_(std::exchange(other.h_, nullptr)) {}

    UniqueHandle& operator=(UniqueHandle&& other) noexcept
    {
        if (this != &other)
        {
            reset(std::exchange(other.h_, nullptr));
        }
        return *this;
    }

    ~UniqueHandle() { reset(); }

    HANDLE get() const noexcept { return h_; }
    explicit operator bool() const noexcept { return h_ != nullptr; }

    void reset(HANDLE h = nullptr) noexcept
    {
        if (h_ != nullptr)
        {
            ::CloseHandle(h_);
        }
        h_ = h;
    }

private:
    HANDLE h_ = nullptr;
};

이것을 쓰면, 실패 경로에 CloseHandle을 덧쓸 필요가 없어집니다.

// CameraSession의 멤버: UniqueHandle frameReady_;
bool CameraSession::Reconnect()
{
    UniqueHandle frameReady{ ::CreateEventW(nullptr, TRUE, FALSE, nullptr) };
    if (!frameReady)
    {
        return false;   // 생성 자체가 실패. 닫을 것은 없다
    }

    if (!RegisterCallback(frameReady.get()))
    {
        return false;   // 여기서 return해도 소멸자가 닫는다
    }

    if (!StartAcquisition())
    {
        // 등록이 끝난 뒤의 실패는, 닫기 전에 등록을 해제한다.
        // 해제하지 않고 빠져나가면 소멸자는 CloseHandle을 하지만, SDK 쪽은
        // 넘겨받은 핸들을 그대로 쥐고 있다. 다음 프레임에서 이미 해제한 번호로
        // 시그널하게 되고, 그 번호가 다른 자원에 재사용되어 있으면
        // 「관련 없는 이벤트가 제멋대로 켜진다」는 형태로 겉에 드러난다
        UnregisterCallback();
        return false;
    }

    // 성공했을 때만 소유권을 session 쪽으로 넘긴다
    frameReady_ = std::move(frameReady);
    return true;
}

C#이라면, using만으로 끝내지 못하는 경우가 많으므로, ‘소유권을 넘겼는지’를 플래그로 두고, 넘기지 못했을 때만 finally에서 버리는 형태로 합니다. 단순히 using var로 쓰면, 성공했을 때까지 해제되어 버리기 때문입니다.

// C# / .NET 8
// CameraSession의 필드: private ManualResetEvent? _frameReady;
public bool Reconnect()
{
    var frameReady = new ManualResetEvent(false);
    var handedOver = false;
    var registered = false;

    try
    {
        if (!RegisterCallback(frameReady))
        {
            return false;
        }

        registered = true;

        if (!StartAcquisition())
        {
            return false;
        }

        _frameReady?.Dispose();
        _frameReady = frameReady;
        handedOver = true;
        return true;
    }
    finally
    {
        if (!handedOver)
        {
            // 버리기 전에, 외부가 쥐고 있는 참조를 먼저 해제한다.
            // SDK는 등록 때 넘겨받은 핸들을 유지하므로,
            // 순서를 반대로 하면 이미 해제한 핸들을 두드리게 된다
            if (registered)
            {
                UnregisterCallback();
            }

            frameReady.Dispose();
        }
    }
}

어느 쪽이든, 하는 일은 같습니다. 도중에 어디서 빠져도, 소유자가 정해지지 않은 자원은 반드시 버려진다는 구조로 두고 있습니다. ‘실패하면 닫는다’를 사람이 매번 쓰는 것이 아니라, 타입과 finally에 대신 맡긴다는 이야기입니다.

소유권 이전으로 정해지는 해제 담당처리 도중에 어디서 빠져도, 소유권을 session 쪽으로 넘겼다면 이후 해제는 session이 맡고, 넘기지 못했다면 타입이나 finally가 반드시 버리며, 버리기 전에는 SDK 등록을 해제한다는 구조를 나타낸다.넘겼다넘기지 못했다처리 도중에 어디서 빠져도소유권은 넘겼는가session 쪽이 이후 해제를 맡는다타입이나 finally가 반드시 버린다버리기 전에 등록을 해제한다

그림 11: ‘실패하면 닫는다’를 매번 쓰지 않고, 소유권의 행선지로 해제 담당을 정한다.

여기는 특별한 기교라기보다, 자원 수명을 코드에 심는 정리입니다.

4. 원인을 어떻게 분리했는가

이 장부터 조사 쪽 영어가 그대로 나옵니다. 먼저 짧게 역주를 두겠습니다.

용어 한국어로 말하면 이 글에서의 의미
baseline 기준값 워밍업이 끝나 안정된 시점의 값. 여기부터의 차이로 봅니다
leakSlope 누수의 기울기 사이클당 몇 개 늘었는가. 늘어나는 속도를 나타내는 자작 지표입니다
structured log 구조화 로그 문장이 아니라 key=value처럼 항목을 정해 내는 로그. 나중에 기계적으로 집계할 수 있습니다
heartbeat 정기 보고 일정 간격으로 생존 확인과 자원 값을 계속 내는 로그
harness 테스트용 하네스 본체 앱 대신, 시험하고 싶은 처리만 반복 돌리는 작은 실행 프로그램
phase 단계 OpenStart, ReconnectStart처럼, 지금 처리의 어느 단계에 있는지의 표시

4.1. 월 단위 재현을 기다리지 않고 시간을 줄인다

이런 조사에서 1개월을 매번 기다리는 것은 방향이 나쁩니다. 해야 할 일은, 수상한 경로를 짧은 시간에 여러 번 지나는 것입니다.

이번 사례에서는 이런 루프를 돌려 재현을 압축했습니다.

아니요시작카메라 open촬영 시작모의 timeout / 끊김재연결촬영 재개N회 반복종료 시의 차이을 확인

그림 12: 월 단위 재현을 기다리지 않고, open·끊김·재연결의 경계만 짧은 루프로 수천 번 돌린다.

포인트는, 평소의 ‘찍히고 있는’ 시간이 아니라, 경계의 수명 조작에 시간을 쓰는 것입니다.

구체적으로 효과가 있는 것은 이런 시나리오입니다.

  • open -> start -> stop -> close를 대량으로 돌린다
  • timeout을 의도적으로 일으켜 reconnect를 돌린다
  • callback 등록 직후에 실패시킨다
  • 끊김 중단, 재연결 중단, shutdown 경합을 넣는다

1개월분의 실제 운영을 완벽하게 재현할 필요는 없습니다. 오히려, 의심하고 있는 lifetime edge를 수천 번 밟는 편이 원인에는 훨씬 가깝습니다.

4.2. Handle Count의 기울기로 본다

그 전에, Handle Count는 어디서 보는가를 적어 둡니다. 여기가 없으면 이 절은 전부 그림의 떡입니다.

수단 조작 맞는 장면
작업 관리자 ‘자세히’ 탭을 열고, 열 머리글을 마우스 오른쪽 클릭 →’열 선택’→’핸들’에 체크한다 지금 몇 개인지를 바로 보고 싶을 때
Process Explorer 프로세스를 골라 속성을 열고, Process Performance 탭의 Handle Count를 본다. 아래 창의 Handles 뷰를 Type 순으로 정렬하면 종류별 내역도 알 수 있다 어떤 핸들이 늘고 있는지 알고 싶을 때
handle.exe handle -s -p CameraApp으로 종류별 집계를 텍스트로 얻는다 고정 시점 관측을 로그로 남기고 싶을 때
PowerShell Get-Process -Name CameraApp \| Select-Object Name, Id, HandleCount 스크립트로 정기 수집하고 싶을 때
typeperf typeperf "\Process(CameraApp)\Handle Count" -si 60 -sc 1440 -o handles.csv CSV로 장시간 그대로 기록하고 싶을 때
앱 자신 GetProcessHandleCount 또는 Process.HandleCount를 heartbeat 로그에 넣는다 운영 기기에서 로그만 회수하고 싶을 때

종류별 내역이나, 이름 없는 이벤트가 늘어나는 모습을 쫓는 절차는 Process Explorer / Handle / VMMap 실전 쪽에 정리해 두었습니다.

장기 가동 조사에서 주력이 되는 것은, 맨 아래의 ‘앱 자신이 낸다’입니다. 사람이 작업 관리자를 지켜보는 일은 24/7에서는 이어지지 않습니다.

핸들 누수 조사에서는 절댓값만 봐도 알기 어려울 때가 있습니다. 중요한 것은, 돌아와야 할 조작 뒤에 돌아와 있는지와, 몇 번의 조작으로 몇 개 늘어나는지입니다.

보는 방식으로는, 대체로 다음 순서가 알기 쉽습니다.

  1. 워밍업 뒤의 baseline을 정한다
  2. reconnect / start-stop / close 뒤에 Handle Count를 기록한다
  3. 사이클당 차이을 본다
  4. 몇 사이클을 모은 기울기도 본다

예를 들어 이런 보기입니다.

leakSlope =
    (currentHandleCount - baselineHandleCount)
    / reconnectCount

절댓값 2000이 많은지 적은지는 앱에 따라 흔들립니다. 다만 reconnect 1회당 +1로 돌아오지 않는다면, 그것은 꽤 수상합니다.

그러면 정상 경로는 어떻게 보여야 하는가, 하는 가늠도 적어 둡니다. 숫자 자체는 앱 나름이므로, 형태로 판단합니다.

  • 시작 직후에는 늘어납니다. 여기는 읽지 않습니다
  • 워밍업이 끝나면, 조작에 따라 늘었다 줄었다 하면서 일정 범위를 오가는 형태가 되어야 합니다
  • open -> start -> stop -> close를 1사이클 돌린 뒤, 값이 사이클 전과 거의 같게 돌아오는 것이 정상입니다
  • 100사이클을 돌려 baseline과의 차가 몇 개 이내이면, 우선 건전합니다
  • 반대로, 사이클 수에 비례해 깨끗이 우상향이면, 그 기울기만큼 매번 새고 있습니다

보는 것은 ‘많은가 적은가’가 아니라, 돌아오는가 돌아오지 않는가입니다. 여기를 바꾸어 읽으면, 정상인 앱을 의심하며 시간을 낭비합니다.

Handle Count 기울기의 읽는 법워밍업 뒤에 baseline을 정하고 사이클당 차이을 보아, 사이클 뒤에 원위치로 돌아오면 우선 건전하고, 사이클 수에 비례해 우상향이면 그 기울기만큼 매번 새고 있다고 판단하는 읽는 법을 나타낸다.돌아온다비례해 우상향워밍업 뒤에 baseline을 정한다사이클당 차이을 본다사이클 뒤에 원위치로 돌아오는가우선 건전기울기만큼 매번 새고 있다

그림 13: 절댓값의 많고 적음이 아니라, ‘돌아오는가 돌아오지 않는가’의 형태로 판단한다.

여기에서의 요령은, Handle Count 단독으로 보지 말고, 최소한 다음을 함께 적는 것입니다.

  • Handle Count
  • Private Bytes
  • Thread Count
  • ReconnectCount
  • 지금 어느 phase인지

이것으로 ‘메모리가 늘고 있는가’, ‘스레드가 늘고 있는가’, ‘재연결할 때마다 자원이 돌아오지 않는가’를 꽤 빨리 알 수 있습니다.

4.3. create/openclose/dispose의 짝을 본다

process 전체의 Handle Count가 수상하다는 것을 알아도, 그것만으로는 누수 지점까지 갈 수 없습니다. 다음에 필요한 것은, 자원의 수명을 짝으로 보는 로그입니다.

이미지로는 이런 structured log입니다.

CameraSession session=421 cameraId=CAM01 phase=ReconnectStart reason=FrameTimeout handleCount=1824 privateBytesMB=418

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Create osHandle=0x00000ABC handleCount=1825

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Close osHandle=0x00000ABC handleCount=1824

여기서 중요한 것은, osHandle에만 기대지 않는 것입니다. Windows의 핸들 값은 나중에 재사용되는 경우가 있으므로, 로그에서는 적어도 다음을 가지게 하는 편이 쫓기 쉽습니다.

  • sessionId
  • resourceId
  • kind
  • action(Create/Open/Register/Close/Dispose/Unregister)
  • osHandle
  • phase

이렇게 두면, Create는 있는데 Close가 없다는 짝이 안 맞는 흐름을 찾기 쉬워집니다.

자원 수명을 짝으로 쫓는 로그Create와 Close를 짝으로 기록하고 sessionId와 resourceId로 같은 자원을 이으면, Create는 있는데 Close가 없는 짝이 안 맞는 흐름을 찾을 수 있다. osHandle은 재사용되므로 단독으로는 쫓을 수 없음도 나타낸다.Create와 Close를 짝으로 기록한다sessionId와 resourceId로 잇는다Close되지 않은 Create를 발견osHandle은 재사용되므로 단독으로는 불가

그림 14: 프로세스 전체의 수에서 누수 지점으로 내려가려면, 자원 수명을 짝으로 한 로그가 필요하다.

4.4. 핸들 누수는 ‘크래시한 곳’이 아니라 ‘샌 곳’을 찾는다

여기는 꽤 중요합니다.

핸들 누수는 자주 이런 형태로 보입니다.

  • 크래시한 줄: CreateEvent 실패
  • 진짜 누수: 며칠 전부터 failure path에서 CloseHandle이 빠져 있었다

즉 마지막에 크래시한 API는 피해의 출구이지, 원인의 입구라고 할 수 없습니다.

그래서 조사 순서로는,

  1. 어느 자원이 계속 늘고 있는지를 본다
  2. 어느 조작 경계에서 돌아오지 않는지를 본다
  3. create/openclose/dispose의 짝이 깨져 있는 곳을 찾는다
  4. 마지막에 크래시 지점을 읽는다

이 순서 쪽이 꽤 길을 잃기 어렵습니다.

샌 곳으로 추적하는 조사 순서어느 자원이 계속 늘고 있는지를 보고, 어느 조작 경계에서 돌아오지 않는지를 보고, create와 close의 짝이 깨진 곳을 찾고, 마지막에 크래시 지점을 읽는 순서 쪽이 길을 잃기 어렵다는 것을 나타낸다.계속 늘고 있는 자원을 본다돌아오지 않는 조작 경계를 본다create와 close 짝의 무너짐을 찾는다마지막에 크래시 지점을 읽는다

그림 15: 크래시 지점은 출구에 지나지 않는다. 입구인 ‘샌 곳’부터 추적한다.

5. 재발을 막기 위해 필요한 로그

5.1. 먼저 남겨야 할 최소 세트

이번 조사에서 효과가 있었던 것은, 단순히 로그량을 늘리는 일이 아닙니다. ‘나중에 원인에 도달할 수 있는 정보’를 정리해 늘리는 것이었습니다.

최소한 다음은 남겨 두고 싶습니다.

분류 최소한 원하는 항목 이유
조작 문맥 cameraId, sessionId, operationId, reconnectCount, phase 어느 조작의 몇 번째에서 일어났는지를 묶기 위해
process 자원 handleCount, privateBytes, workingSet, threadCount 무엇이 늘고 있는지를 먼저 구분하기 위해
resource lifecycle action, resourceId, kind, osHandle, owner create/openclose/dispose의 짝을 쫓기 위해
외부 호출 결과 win32Error, HRESULT, sdkError, timeoutMs 실패의 종류를 나중에 비교하기 위해
상태 전이 OpenStart, OpenDone, ReconnectStart, ReconnectDone, ShutdownStart 어느 phase 도중에 무너졌는지를 알기 위해
실행 환경 pid, tid, buildVersion, machineName dump / symbol / 배포물과의 대응을 잡기 위해

이것으로 충분하다고는 하지 않습니다. 다만 적어도 이것이 없으면, ‘크래시했다’는 사실만 남는 로그가 되기 쉽습니다.

5.2. 실제로 강화한 로그

이 사례에서는 로그를 다음 방향으로 강화했습니다.

  1. 정기 heartbeat
    • 1~5분마다 Handle Count / Private Bytes / Thread Count / ReconnectCount를 낸다
  2. 카메라 session 단위의 경계 로그
    • OpenStart
    • CallbackRegistered
    • AcquisitionStart
    • TimeoutDetected
    • ReconnectStart
    • ReconnectDone
    • CloseStart
    • CloseDone
  3. 자원 수명 로그
    • event / thread / file / timer / SDK registration token의 Create/Open/RegisterClose/Dispose/Unregister
  4. 오류의 정규화
    • 예외 message만으로 끝내지 않고, win32Error, HRESULT, sdkError, phase를 동시에 낸다

중요한 것은, 성공 때와 실패 때 로그의 형을 바꾸지 않는 것입니다. 이상 때만 다른 형식이 되면, 나중에 집계하기 어려워집니다.

강화한 네 계통의 로그정기 heartbeat, 카메라 session 단위의 경계 로그, 자원 수명 로그, 오류의 정규화라는 네 계통을 맞추고, 성공 때와 실패 때 형을 바꾸지 않음으로써 나중에 원인에 도달할 수 있는 로그가 됨을 나타낸다.정기 heartbeat(자원의 값)원인에 도달할 수 있는 로그session의 경계 로그자원 수명 로그오류의 정규화성공 때와 실패 때 형을 바꾸지 않는다

그림 16: 로그량을 늘리는 것이 아니라, 나중에 맞댈 수 있는 네 계통을 갖춘다.

5.3. 어느 상세도로 남길 것인가

여기서 하기 쉬운 것이 ‘일단 전부 INFO로 뱉는다’입니다. 다만 그렇게 하면, 나중에 읽을 때 로그의 벽이 생깁니다. 이것은 꽤 힘듭니다.

상세도로는, 대체로 다음 나눔이 현실적입니다.

  • 정기 감시
    • Handle Count, Private Bytes, Thread Count, ReconnectCount
  • 조작 경계
    • session의 start / done / fail
  • 자원 경계
    • create/open/registerclose/dispose/unregister
  • 이상 시 상세
    • error code, stack, dump 채취 트리거

매 프레임의 상세 로그는 보통 필요 없습니다. 오히려, ‘어느 책임이 열고, 어느 책임이 닫았는가’를 읽을 수 있는 로그 쪽이 장시간 장애에는 효과가 있습니다.

로그 상세도의 나눔정기 감시는 자원 카운트, 조작 경계는 session의 시작과 종료, 자원 경계는 create와 close의 짝, 이상 때만 상세를 깊게 취하는 나눔으로 두고, 전부를 INFO로 뱉어 로그의 벽을 만들지 않음을 나타낸다.로그의 상세도정기 감시: 카운트조작 경계와 자원 경계이상 때만 상세를 깊게전부 INFO로 뱉는다읽을 수 없는 로그의 벽

그림 17: 매 프레임의 상세보다, ‘누가 열고 누가 닫았는가’를 읽을 수 있는 상세도로 맞춘다.

6. 대략적인 구분

  • 며칠~몇 주 뒤에만 크래시한다
    • 먼저 Handle Count / Private Bytes / Thread Count의 heartbeat를 넣는다
  • retry / reconnect / shutdown이 있다
    • 그 경계만 대량으로 돌리는 harness를 먼저 만든다
  • native SDK / P/Invoke / Win32를 많이 쓴다
    • 후편의 Application Verifier를 적용할 가치가 높습니다
  • GUI도 같이 있다
    • Handle Count에 더해 GDI Objects / USER Objects도 보는 편이 좋습니다
  • 크래시 순간의 예외만으로는 아무것도 알 수 없다
    • operation / session / resource lifecycle의 structured log를 먼저 갖추는 편이 빠릅니다

마지막 한 항목은 꽤 중요합니다. 장애 조사에서는 해석 기술 자체보다, 관측할 수 있는 형태로 되어 있는가가 승부를 가르는 일이 자주 있습니다.

7. 정리

장시간 가동 뒤에만 크래시하는 앱에서는 메모리뿐 아니라 Handle Count도 볼 것. 핸들 누수는 정상 경로가 아니라 이상 경로의 failure path에 숨기 쉽고, 크래시 지점은 샌 지점이 아니라 이차 장애의 출구인 경우가 많다. 증상을 읽는 법으로는, 결국 이 세 가지로 모입니다.

재발 방지로는 create/openclose/dispose의 책임을 가깝게 두고, session / operation 단위로 문맥을 가진 로그를 남기며, process 자원과 resource lifecycle 양쪽을 기록해 둔다. 테스트에서는 월 단위 재현을 기다리지 말고 timeout / reconnect / shutdown을 짧은 루프로 돌리고, ‘부서지지 않는 것’뿐 아니라 ‘부서졌을 때 쫓을 수 있는 것’을 합격 조건으로 한다. 이번에 효과가 있었던 것은 이 조합입니다. 후편에서는 Application Verifier를 써서, 메모리 부족이나 핸들 이상처럼 나오기 어려운 부서짐을 앞당겨 겉으로 드러냅니다.

제어 앱에서는 정상 경로가 통과하는 것도 중요하지만, 부서졌을 때 ‘무엇이 일어났는지 알 수 있는’ 것이 장기 운용에서는 꽤 효과가 있습니다.

핸들 누수는 바로 그 차이가 효과가 있는 유형의 장애입니다. 일어난 순간에만 보지 말고, 늘어나는 방식, 경계, 책임의 짝으로 보도록 하면, 꽤 쫓기 쉬워집니다.

후편: Application Verifier로 만드는 Windows 이상 경로 테스트 기반

8. 참고 자료

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

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

실제 정리와 개선 진행 방식이 가까운 사례 페이지입니다.

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

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

핸들 누수란 무엇인가요?
Windows 프로세스가 event, mutex, file, socket 같은 OS 자원을 가리키는 핸들을 닫지 않아 Handle Count가 계속 늘어나는 상태입니다. 특히 어떤 작업을 위해 잠시 연 자원을 timeout·reconnect·early return처럼 중간에 실패하는 경로에서 닫지 않는 패턴이 많고, 평소의 짧은 테스트는 성공 경로만 지나기 때문에 놓치기 쉽습니다.
메모리 누수와 핸들 누수는 어떻게 구별하나요?
보는 지표가 다릅니다. 메모리 누수는 Private Bytes나 Commit가 조금씩 늘어나는 반면, 핸들 누수는 Handle Count가 조금씩 늘어난 뒤 다시 내려오지 않습니다. 장시간 가동의 원인을 분리할 때 메모리만 보면 한쪽만 보고 가는 셈이 되기 쉬우므로, Handle Count와 Thread Count도 함께 보는 것이 기본입니다. GUI가 같이 있으면 GDI Objects / USER Objects도 봅니다.
핸들 누수가 있으면 왜 장시간 가동 후에만 크래시하나요?
한 번 실패할 때 핸들 하나만 새는 완만한 기울기의 누수는 몇 분 안에는 아무 일도 없지만, 24/7 가동에서는 timeout이나 재연결과 같은 경계 조건이 반복되어 몇 주에 걸쳐 쌓이기 때문입니다. 결국 새 event/file/thread를 만드는 API가 실패하는 시점에 이차 장애로 드러납니다. 크래시 지점은 샌 곳이 아니라 마지막 피해자인 경우가 많다는 점도 중요합니다.
핸들 누수는 어떻게 조사하면 되나요?
월 단위 재현을 기다리지 말고, open -> start -> stop -> close나 timeout·reconnect처럼 수명이 수상한 작업의 경계를 짧은 루프로 수천 번 돌려 재현 시간을 줄입니다. 워밍업 뒤 baseline을 정하고 Handle Count의 사이클당 차이와 기울기를 본 다음, create/open과 close/dispose 짝이 깨진 곳을 sessionId·resourceId·action을 담은 structured log로 찾고, 크래시 지점은 맨 마지막에 읽는 순서가 길을 잃기 어렵습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기