Go(고) 언어 개발자들이 코드 안정성을 위해 흔히 사용하는 nil 포인터 검사가 오히려 시스템의 문제를 모호하게 만들 수 있다는 비판적인 시각이 제기되었습니다. nil 검사는 프로그램의 패닉(panic)을 막는 데 필수적이지만, 이를 남용하거나 잘못된 위치에서 사용하면 코드가 ‘무엇이 nil일 수 있는가’를 스스로 설명하지 못하게 되어 디버깅을 어렵게 하고 잠재적인 버그를 숨길 수 있다는 지적입니다.
주요 문제는 Redis(레디스) 클라이언트와 같은 필수 의존성(dependency)을 내부 메서드에서 검사할 때 발생합니다. 예를 들어, RateLimiter(레이트리미터)가 Redis 클라이언트를 필드로 가지고 Allow() 메서드 내부에서 r.redis != nil을 확인하는 코드는 겉보기엔 안전해 보입니다. 그러나 Redis 클라이언트가 nil이라면 문제는 Allow() 실행 시점이 아니라 RateLimiter 객체 생성 시점에 이미 발생한 것입니다. 내부 메서드에서 nil을 확인하는 것은 생성 실패 상태로 계속 동작하는 것을 허용 가능한 상태처럼 취급하게 만들어, 객체의 출처, 초기화 책임, 그리고 nil이 불가능해야 하는 불변 조건(invariant)을 코드가 잃어버렸다는 신호가 됩니다. 올바른 접근 방식은 NewRedisClient(addr)와 같은 초기화 지점에서 오류를 즉시 처리하고, 유효한 클라이언트만 NewRateLimiter()에 전달하는 것입니다.
또한, HTTP 핸들러나 RPC(원격 프로시저 호출) 디스패치, 큐 컨슈머(queue consumer)와 같이 외부에서 들어오는 요청 객체(request object)와 같은 값들은 경계 계층(boundary layer)에서 한 번만 검증하고, 내부 로직은 그 보장을 신뢰해야 합니다. RateLimiter.Allow(ctx, req) 내부에서 req == nil을 확인하는 것도 의존성 nil 검사와 같은 실수입니다. 요청은 Allow()에서 처음 들어온 것이 아니라 더 앞단의 전송 경계에서 들어와 코드 내부를 이동한 값이기 때문입니다. 경계에서 검증을 마친 후에는 내부 함수들이 해당 값이 유효하다고 신뢰하고 실제 비즈니스 로직에 집중해야 합니다.
이러한 '조용한 실패(silent failure)'는 명확성, 즉시성, 귀속성이라는 에러 반환의 세 가지 중요한 성질을 모두 잃게 만듭니다. 실패가 조용히 사라지고, 더 많은 코드가 실행된 뒤 나중에 증상으로 나타나며, 증상이 보일 때는 원인을 식별하기 어려워집니다. 프로그램이 잘못된 상태로 살아남는 호출이 늘어날수록 원인과 증상 사이의 간격은 더욱 커집니다. 이는 결국 메트릭(metric), 대시보드(dashboard), 알림(alert)과 같은 관측 인프라를 구축하여 사라진 신호를 복원하는 추가적인 엔지니어링 비용을 발생시킵니다.
결론적으로, nil 검사는 신뢰할 수 없는 경계 입력을 보호하거나 의도적인 선택적 상태를 모델링할 때 유용합니다. 하지만 프로그램이 불가능하다고 간주하는 상태를 조용히 처리하는 nil 검사는 의심해야 합니다. Go 언어는 명시적인 널 가능성(nullability) 타입이 없어 개발자가 모든 포인터의 nil 가능성을 머릿속으로 추론해야 하는 어려움이 있습니다. 이는 Rust(러스트)의 Option<T>나 C#(C#)의 널 가능 타입과 비교되며, 언어 차원에서 널 불가(non-nullable)를 기본값으로 제공하는 것이 더 나은 설계라는 의견도 있습니다. 올바른 위치의 nil 검사는 안전한 코드를 만들지만, 잘못된 위치의 검사는 시스템의 불변 조건을 제대로 세우지 못했다는 신호가 됩니다.