exception-handling

2 개의 포스트

코드 품질 개선 기법 20편: 이례적인 예외 과대 포장 (새 탭에서 열림)

리소스를 안전하게 해제하기 위해 사용하는 `use` 패턴이나 커스텀 예외 처리 구현 시, 발생한 여러 예외를 하나의 커스텀 예외로 감싸서(wrapping) 던지는 것은 주의해야 합니다. 이러한 '과대 포장'은 호출자가 기대하는 특정 예외 유형을 가려버려 예외 처리 로직을 무력화시키고 디버깅을 어렵게 만듭니다. 따라서 여러 예외가 동시에 발생할 때는 원인이 되는 주요 예외를 우선시하고, 부수적인 예외는 `addSuppressed`를 통해 전달하는 것이 올바른 품질 개선 방향입니다. ### 예외 과대 포장의 부작용 * 리소스 해제 과정에서 발생하는 예외까지 관리하기 위해 `DisposableException` 같은 별도의 예외 클래스로 감싸게 되면, 원래 발생한 구체적인 예외 정보(예: `IOException`)가 추상화되어 버립니다. * 이 경우 호출부에서 특정 예외를 잡기 위해 작성한 `catch(e: IOException)` 문이 작동하지 않게 되어, 의도치 않은 런타임 오류로 이어질 수 있습니다. * 특히 유틸리티 함수나 보조 함수 내부에서 이러한 포장이 일어날 경우, 호출자는 내부 구현을 상세히 알기 전까지는 예외 처리 실패의 원인을 파악하기 매우 어렵습니다. ### `addSuppressed`를 활용한 예외 우선순위 설정 * 한 코드 블록에서 비즈니스 로직과 리소스 해제(dispose) 로직 모두 예외가 발생할 수 있다면, 어떤 예외가 더 중요한지 판단하여 우선순위를 정해야 합니다. * 일반적으로 비즈니스 로직이 실행되는 `block`에서 발생한 예외가 핵심적인 정보를 담고 있으므로 이를 우선적으로 `throw`해야 합니다. * 리소스 해제 시 발생하는 보조적인 예외는 버리지 않고, 주요 예외의 `addSuppressed` 메서드에 추가함으로써 전체적인 예외 맥락을 보존하면서도 타입 시스템을 해치지 않을 수 있습니다. ### 언어별 예외 처리 시 주의사항 * **Kotlin:** `Closeable.use` 확장 함수는 이미 `addSuppressed`를 활용하여 주요 예외를 우선하는 방식으로 구현되어 있으므로, 커스텀 리소스 클래스 제작 시에도 이와 유사한 패턴을 따르는 것이 좋습니다. * **Java:** Checked Exception이 존재하는 Java에서는 예외를 다른 타입으로 감쌀 때 상속 관계를 신중히 고려해야 합니다. * 복구가 불가능한 경우가 아니라면 Checked Exception을 `RuntimeException`으로 함부로 변환하여 던지지 않아야 하며, 부모 예외 타입으로 뭉뚱그려 잡는 과정에서 예외 처리 누락이 발생하지 않도록 주의가 필요합니다. 리소스 해제와 같은 부수적인 작업에서 발생하는 예외가 본래의 실행 목적을 가진 코드의 예외를 덮어쓰지 않도록 설계해야 합니다. 항상 "어떤 예외가 개발자나 시스템에게 더 중요한 정보인가"를 고민하고, 언어에서 제공하는 예외 억제(suppression) 기능을 활용해 예외의 층위를 명확히 관리할 것을 권장합니다.

.NET 지속적 프로파일러: 예외 및 락 경합 (새 탭에서 열림)

Datadog의 .NET 컨티뉴어스 프로파일러는 애플리케이션의 성능에 보이지 않는 영향을 미치는 예외(Exception)와 락 경합(Lock Contention)을 정밀하게 추적합니다. 단순히 발생 횟수를 세는 것을 넘어, 로우 레벨 CLR 콜백과 메타데이터 분석을 통해 예외 메시지와 락 유지 시간 등 구체적인 컨텍스트를 제공하여 성능 병목의 원인을 정확히 파악하도록 돕습니다. 이를 통해 개발자는 무분별한 예외 발생으로 인한 CPU 낭비와 병렬 알고리즘의 지연 시간을 효과적으로 최적화할 수 있습니다. **예외 발생 데이터 수집 및 타입 분석** * 예외가 던져질 때 CLR은 `ICorProfilerCallback::ExceptionThrown`을 호출하며, 프로파일러는 이를 통해 예외 객체의 `ObjectID`를 획득합니다. * `ICorProfilerInfo::GetClassFromObject`를 사용하여 예외 인스턴스의 `ClassID`를 구하고, 이를 `FrameStore`와 연동하여 클래스 이름을 확인하고 캐싱합니다. * 예외 처리는 `catch` 및 `finally` 블록의 실행을 보장하기 위해 런타임에서 많은 CPU 사이클을 소모하므로, 발생 지점과 타입별 통계를 파악하는 것이 중요합니다. **System.Exception 메시지 추출을 위한 메타데이터 탐색** * 예외의 세부 내용을 파악하기 위해 `_message` 필드의 값을 읽어야 하며, 이를 위해서는 해당 필드의 정확한 메모리 오프셋을 알아야 합니다. * .NET 버전(Framework 또는 Core)에 따라 `mscorlib`나 `System.Private.CoreLib` 모듈을 식별한 후, `IMetaDataImport`를 통해 `System.Exception` 클래스의 메타데이터 토큰을 찾습니다. * `ICorProfilerInfo2::GetClassLayout`을 호출하여 클래스의 필드 레이아웃 정보를 얻고, `_message` 필드의 `COR_FIELD_OFFSET`을 계산하여 문자열 버퍼의 위치를 특정합니다. **락 경합의 지속 시간 및 원인 식별** * .NET 런타임은 `Monitor.Enter` 등을 사용하는 락 패턴에 대해 `ContentionStart`와 `ContentionStop` 이벤트를 발생시킵니다. * .NET Framework와 같이 이벤트 자체에서 지속 시간을 제공하지 않는 경우, 프로파일러가 직접 스레드별로 시작 시점의 타임스탬프를 관리하여 대기 시간을 계산합니다. * .NET 8부터는 `ContentionStart` 이벤트에 락의 `ObjectID`와 현재 락을 점유 중인 스레드 정보가 포함되어, 어떤 스레드가 다른 스레드를 대기하게 만드는지 구체적으로 가시화할 수 있습니다. **EventPipe를 통한 효율적인 실시간 이벤트 모니터링** * .NET 5 이상에서는 `ICorProfilerCallback10::EventPipeEventDelivered` 메서드를 통해 CLR 이벤트를 동기적으로 수신할 수 있습니다. * `ClrEventParser` 클래스는 이벤트 페이로드에서 ID와 키워드를 기반으로 필요한 필드만 추출하여 성능 부하를 최소화합니다. * 이러한 메커니즘을 통해 애플리케이션 실행에 거의 영향을 주지 않으면서도(Negligible impact) 상세한 프로파일링 데이터를 확보합니다. 성능 최적화를 위해서는 `Parse` 대신 `TryParse`를 사용하여 불필요한 예외 비용을 줄이고, 프로파일러가 제공하는 락 지속 시간 데이터를 바탕으로 과도한 동기화 구문을 개선하는 실용적인 접근이 필요합니다.