error-handling

3 개의 포스트

코드 품질 개선 기법 23편: 반환의 끝이 에지 케이스의 끝 (새 탭에서 열림)

조기 반환(Early Return)은 에러 케이스를 미리 배제하여 함수의 주요 로직에 집중하게 돕는 훌륭한 기법이지만, 모든 상황에서 정답은 아닙니다. 만약 에러 케이스와 정상 케이스의 처리 방식이 본질적으로 같다면, 이를 분리하기보다 하나의 흐름으로 통합하는 것이 코드의 복잡성을 낮추는 데 더욱 효과적입니다. 무분별한 조기 반환 대신 언어의 특성과 라이브러리 기능을 활용해 에지 케이스를 정상 흐름에 포함시키는 것이 코드 품질 개선의 핵심입니다. ### 조기 반환 대신 정상 케이스로 통합하기 * **빈 컬렉션 순회 활용**: `map`, `filter`, `sum`과 같은 고차 함수는 컬렉션이 비어 있어도 오류 없이 자연스럽게 동작하므로, `isEmpty()`를 통한 별도의 조기 반환 처리가 불필요한 경우가 많습니다. * **Safe Call과 엘비스 연산자**: `null`을 체크하여 조기 반환하는 대신, `?.`(세이프 콜)이나 `?:`(엘비스 연산자)를 사용하면 `null`을 정상적인 데이터 흐름의 일부로 처리할 수 있어 코드가 간결해집니다. * **인덱스 범위 체크의 추상화**: 리스트 인덱스를 직접 조사하기보다 `getOrNull`이나 `getOrElse` 같은 함수를 사용하면, 범위를 벗어난 경우를 `null` 처리 흐름에 통합하여 조건문을 줄일 수 있습니다. ### 속성 의존성 및 예외 처리의 최적화 * **무의미한 대입 배제 지양**: UI 요소의 가시성(`isVisible`)에 따라 텍스트 대입 여부를 결정할 때, 조기 반환으로 대입을 막기보다는 가시성 여부와 상관없이 값을 대입하도록 로직을 통합하는 것이 상태 관리에 더 유리할 수 있습니다. * **flatMap을 이용한 연쇄 함수 호출**: 여러 단계에서 발생하는 예외를 각각 `try-catch`와 조기 반환으로 처리하면 흐름이 복잡해집니다. 이때 `Result` 객체와 `flatMap`을 활용하면 성공과 실패 케이스를 동일한 파이프라인에서 처리할 수 있습니다. * **성능과 가독성의 균형**: 로직을 통합하는 과정에서 인스턴스 생성 등으로 인한 미세한 성능 저하가 발생할 수 있으나, 대부분의 경우 코드의 명확성과 유지보수성이 주는 이점이 더 큽니다. 조기 반환을 작성하기 전, 현재 다루고 있는 에지 케이스가 정말로 '별도의 처리'가 필요한 예외 상황인지, 아니면 '일반적인 처리' 과정에 자연스럽게 녹여낼 수 있는 데이터의 한 형태인지 고민해보는 것이 좋습니다. 에러 케이스와 정상 케이스의 경계를 허물 때 코드는 더욱 단순하고 견고해집니다.

코드 품질 개선 기법 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) 기능을 활용해 예외의 층위를 명확히 관리할 것을 권장합니다.

코드 품질 개선 기법 16편: 불이 'null'인 굴뚝에 연기가 'null'이 아닐 수 없다 (새 탭에서 열림)

널 객체(null object) 패턴은 `null` 대신 '비어 있음'을 나타내는 객체를 사용하여 호출부의 코드를 단순화하고 예외 처리를 줄이는 유용한 디자인 패턴입니다. 그러나 일반적인 상태와 오류 상태를 명확히 구분해야 하는 상황에서 이 패턴을 무분별하게 사용하면, 컴파일러의 정적 검증을 우회하게 되어 오히려 버그를 발견하기 어렵게 만듭니다. 따라서 오류 처리가 필수적인 로직에서는 널 객체 대신 언어 차원의 `null`이나 `Optional` 타입을 사용하여 타입 안정성을 확보하는 것이 권장됩니다. ### 널 객체 패턴의 활용과 장점 널 객체 패턴은 유효하지 않은 값이나 비어 있는 상태를 특정 객체로 정의하여 프로그램의 흐름을 끊지 않도록 돕습니다. - **코드 단순화**: 컬렉션의 경우 `null` 대신 빈 리스트(`.orEmpty()`)를 반환하면 호출 측에서 별도의 널 체크 없이 즉시 순회(iteration) 로직을 수행할 수 있습니다. - **폴백 데이터 제공**: UI 표시를 위한 데이터 모델에서 '알 수 없는 사용자'와 같은 기본 객체를 정의하면, 데이터가 없는 경우에도 화면 레이아웃을 깨뜨리지 않고 기본 정보를 안전하게 보여줄 수 있습니다. - **로직 통합**: 경계 조건이나 오류 상황을 일반적인 비즈니스 로직에 자연스럽게 통합시켜 코드의 가독성을 높입니다. ### 널 객체 패턴이 유발하는 타입 안정성 문제 오류 상태를 일반 객체처럼 취급하게 되면 개발자가 의도적으로 해당 상태를 확인해야 하는 로직을 누락했을 때 이를 잡아낼 방법이 부족해집니다. - **컴파일 타임 검증 부재**: `isInvalid`와 같은 속성으로 오류를 확인해야 하는 널 객체를 사용하면, 확인 로직을 잊더라도 컴파일러는 이를 정상적인 코드로 인식합니다. - **런타임 버그 발생**: 유효하지 않은 널 객체가 시스템 내부에서 계속 전달되다가 예상치 못한 지점에서 오작동을 일으킬 수 있으며, 이는 즉시 런타임 오류가 발생하는 것보다 원인 파악이 더 어렵습니다. - **대안으로서의 정적 타입**: Kotlin의 널 가능 타입(`?`)이나 Swift의 `Optional`을 사용하면 컴파일러가 강제로 널 처리를 요구하므로, 오류 조건과 일반 조건을 명확히 분리하여 처리할 수 있습니다. ### 널 객체 패턴 사용 시 주의할 점: 동일성과 동등성 널 객체를 정의하고 비교할 때는 객체의 비교 방식에 각별히 유의해야 합니다. - **동일성(Identity) 문제**: `UserModel.INVALID`와 같은 정적 인스턴스를 `==` 연산자로 비교할 때, 해당 클래스에 `equals`가 적절히 구현되어 있지 않으면 내용이 같더라도 다른 객체로 판별될 위험이 있습니다. - **값 기반 비교의 한계**: 단순히 기본값(ID 0, 빈 문자열 등)을 채워 넣은 새 객체를 생성해 비교할 경우, 실제 '무효한 상태'를 나타내는 싱글톤 객체와 일치하지 않아 로직 오류가 발생할 수 있습니다. ### 상황에 맞는 도구 선택 제안 널 객체 패턴은 만능 해결책이 아니며, 상황에 따라 적절한 도구를 선택하는 것이 중요합니다. - **널 객체를 권장하는 경우**: 일반적인 경우와 오류/경계 상황을 굳이 구분할 필요가 없거나, '오류'를 나타내는 후보가 너무 많아 정적 검증이 오히려 복잡해질 때 사용합니다. - **정적 타입을 권장하는 경우**: 비즈니스 로직상 오류 상태를 반드시 인지하고 별도의 처리(예: 에러 다이얼로그 표시)를 수행해야 한다면 널 객체 대신 언어에서 제공하는 `null`이나 `Optional`을 활용하여 타입 시스템의 보호를 받아야 합니다.