널 검사는 원인 설명을 대신하지 않는다

포인터가 널일 때 역참조하지 않도록 검사하면 당장의 충돌을 피할 수 있습니다. 하지만 항상 있어야 하는 객체가 사라진 상황이라면 검사 후 조용히 반환하는 코드가 다른 문제를 숨길 수도 있습니다. 먼저 그 포인터가 없을 수 있는 값인지, 없으면 어떤 처리를 해야 하는지 정해야 합니다. 검색 실패를 표현하는 널과 초기화에 실패해 남은 널은 같은 숫자라도 의미가 다릅니다. 디버깅할 때는 오류가 난 위치의 값만 확인하지 말고 해당 포인터를 만드는 함수와 전달하는 호출 경로를 함께 봅니다. 기대 조건을 먼저 정해야 필요한 수정도 분명해집니다.

생성 함수의 반환 직후에 멈추기

객체를 만드는 함수가 포인터를 반환한다면 호출 직후에 정지해 반환값을 확인합니다. 이때 반환값을 담는 레지스터는 대상의 호출 규약과 반환 타입에 따라 달라질 수 있습니다. 생성 직후부터 널이면 입력 파일, 설정 값, 권한, 외부 자원 같은 생성 조건을 조사합니다. 생성 직후에는 정상인데 사용 직전에 널이 되었다면 그 사이에 값을 덮어쓴 경로를 찾습니다. 포인터 복사본이 여러 개 있는 코드에서는 실제 사용한 변수가 어느 복사본인지도 중요합니다. 이름이 비슷한 변수나 클래스 멤버와 지역 변수를 혼동하면 정상 값만 계속 관찰하게 됩니다.

반환 실패를 호출자가 처리했는지 보기

설계상 널을 반환할 수 있는 함수라면 호출자가 그 실패를 어떻게 처리하는지 확인합니다. 예를 들어 검색 결과가 없을 때 다음 단계로 그대로 넘겼다면 문제는 검색 함수보다 호출자의 전제에 있을 수 있습니다. 실패 조건을 재현하는 작은 입력을 만들고 함수 입구와 반환 직후에 각각 멈춰 보세요. 결과를 받았을 때와 없을 때의 분기가 올바른지 비교하면 됩니다. 단순히 널 검사를 넣었다는 사실보다 실패가 사용자에게 어떤 결과로 전달되는지가 중요합니다. 필요한 로그나 오류 반환 없이 값을 버리면 다음 작업이 성공한 것처럼 보일 수 있습니다.

널이 아닌 포인터도 유효성을 보장하지 않는다

널 검사를 통과했다고 메모리 접근이 안전해지는 것은 아닙니다. 해제된 객체의 주소나 초기화되지 않은 포인터는 널이 아닐 수 있습니다. 객체가 살아 있는 기간과 사용하는 기간이 겹치는지, 소유자가 먼저 종료되지 않았는지 확인해야 합니다. 비동기 작업이나 콜백에서는 함수가 반환된 뒤에 지역 변수의 주소를 사용하는 문제가 특히 드러나기 쉽습니다. 수정 방향은 값이 없을 때의 분기 처리인지, 객체 수명 관리인지, 실패한 생성 조건의 수정인지에 따라 달라집니다. 정상 경로뿐 아니라 의도적으로 객체 생성이나 검색을 실패시킨 경로까지 확인해야 회귀를 줄일 수 있습니다.

실패 경로도 정상적으로 설계하기

선택한 항목을 찾지 못하면 널을 돌려주는 검색 함수를 가정해 보세요. 호출자는 결과가 없다는 상태를 화면에 보여줄 수도 있고 상위 함수에 오류를 반환할 수도 있습니다. 어느 쪽이든 다음 단계가 없는 객체를 사용하는 것만 막는 데서 끝나면 안 됩니다. 조회가 실패했는데 성공 메시지를 내보내거나 이전 선택값을 계속 사용하는지도 확인하세요. 디버깅 중에는 존재하는 항목과 없는 항목을 같은 진입점에서 비교하면 분기가 분명해집니다. 없는 입력이 예외적인 사고인지 지원해야 하는 정상적인 결과인지 팀의 계약을 정리하면 필요한 검사와 오류 문구를 결정하기도 쉬워집니다.