본문으로 건너뛰기

© 2026 Molayo

GeekNews헤드라인2026. 06. 28. 09:44

Go에서 과도한 nil 포인터 검사

요약

Go 언어에서 발생하는 과도한 nil 포인터 검사 문제와 에러 핸들링의 한계를 분석합니다. Rust의 Option<T>와 같은 명시적 널 가능성 타입의 부재가 개발자의 추론 부담을 높인다는 점을 지적합니다.

핵심 포인트

  • Go는 명시적인 널 가능성(nullability) 타입이 없어 nil 체크 부담이 큼
  • 에러 스택 추적의 표준 부재로 인해 에러 메시지 관리가 어려움
  • assert를 활용해 유효하지 않은 상태를 명확히 전달하는 방안 제시
  • 컴파일러가 아닌 프로그래머의 머릿속에서 안전 경계를 추론해야 하는 문제

이후 각 계층은 에러가 어디서 났는지만 덧붙이고, 가장 안쪽 err가 무엇이 일어났는지를 알려주는 구조가 좋음

아쉽게도 에러에 대한 통일된, 사실상 표준인 스택 추적이 없음
실제로 “래핑”은 에러 문자열을 grep하고, 그 문자열이 유일하길 바라며, 문자열을 유일하게 만들기 위해 억지로 창의력을 발휘하는 일이 되기 쉽다

에러 스택이 너무 길다고 불평하는 사람도 있지만, 대부분은 이런 메시지가 조치 가능하고 유용하다고 봄
예전에 네트워킹 제품에서 어떤 엔지니어가 수백 개의 에러 메시지를 고치는 데 한 달을 썼는데, 로그에 “What the f-ck?”가 찍히는 건 최종 사용자에게 도움이 안 됐기 때문임
그 메시지들을 유용하게 바꾸고, 위와 같은 이유로 에러 스택도 추가해야 했음

Go에 명시적인 널 가능성(nullability) 이 있었다면 이 문제 자체가 거의 사라졌을 것임

이름 붙일 수 있는 타입의 제로 초기화를 막을 방법이 없어 보여서, 실수는 언제든 숨어들 수 있음

글의 이 문장이 근본 문제를 잘 드러낸다고 느낌
“무엇을 넘겨받을지 통제할 수 없으니, 그 경계에서 nil인지 확인하는 건 합리적이다”라는 부분임
외부 입력에는 맞는 말이지만, 모든 포인터가 nil 가능하면 코드베이스 안에서 안전한 경계를 추적하는 데 추론이 필요함
Go의 문제는 이 추론을 컴파일러가 아니라 모든 프로그래머의 머릿속에서 하도록 강제한다는 데 있음

Rust에는 Option<T>가 있고 C#에는 널 가능 타입이 있음
2026년에 이런 문제를 아직 겪고 있을 필요가 없다고 봄

반대편 입장에서 보자면, “없음”이나 “누락”을 간결하게 표현하는 능력은 특히 JSON 같은 임의 자료구조를 다룰 때 매우 유용함
언어에서 문법은 보통 덜 흥미로운 요소지만, 좋아하는 스크립트 언어에서 foo.bar.baz를 쓰는 게 Rust의 foo.unwrap().bar.unwrap().baz보다 훨씬 쉽다
Rust를 좋아하는 입장에서도 그렇고, Go와 Rust가 종종 같은 묶음으로 취급되지만 Go는 C 프로그래머가 다시 만든 스크립트 언어에 훨씬 가깝다고 봄
그래도 언어가 null을 쓴다면 기본값은 널 불가가 더 낫다. 특히 ?나 .?처럼 짧은 문법이 있다면 큰 프로젝트에서 문법적 부담을 감수할 만함

포인터를 안 쓰면 null도 없으니 만세… 😭

Go는 널 불가 객체를 잘 모델링하는 언어가 아니라고 이해하고 있음
이 점에서는 C와 비슷하고, Option<T>는 T로 표현될 수 있지만 T가 반드시 Option<T>를 뜻하지는 않음
전반적으로 글에는 동의함. 임베디드 펌웨어 회사에서 일할 때도 C++ 코드에 null 체크를 여기저기 쓰지 말고 assert를 쓰자고 설득했음
assert는 디버깅하기 쉽고, 커버리지 관점에서 분기로 잡히지 않으며, 읽는 사람에게 기대 조건을 명확히 전달함. 릴리스 빌드에서는 제외되므로 더 효율적이기도 함
다만 Go에서는 nil 역참조가 이미 좋은 디버깅 정보를 주므로 assert의 이점이 C++만큼 크지는 않다고 이해하고 있음

Go의 nil 역참조는 C의 null 포인터 역참조보다 낫게 결정적으로 panic을 내지만, 실제 포인터가 역참조될 때에야 에러가 나기 때문에 그렇게 훌륭하진 않음
글의 예시라면 checkLimit 깊숙한 곳에서 터질 것이고, 거기서 nil의 출처를 역추적해야 함. 시스템이나 아키텍처에 따라 꽤 복잡할 수 있음
그래서 NewRateLimiter 바로 안에서 assert하는 건 분명 이득이 있음. 예시 코드에서는

if client == nil {
return nil, errors.New("redis client is nil")
}

if client == nil {
panic("redis client is nil")
}

로 바꾸는 셈임
다만 Go 팀은 assertion에 강하게 반대하고, panic도 이상적이지 않아서 잡히지 않으면 전체 런타임을 크래시시킴

null 체크와 assert는 완전히 다르다고 봄
assert는 “이 상태는 유효하지 않다”는 뜻이고, assert 매크로는 릴리스 빌드에서 그 null 체크를 무동작으로 만들 수 있음
assert 매크로 정의 방식에 따라 정의되지 않은 동작 관련 최적화가 일어나 이후 체크가 제거되고 헷갈리는 크래시로 이어질 수 있음
예를 들어 assert(p); if (!p) { ... }에서 뒤의 체크가 제거되는 식의 assert 정의를 본 적이 있음
무작정 “null 체크하지 말고 assert를 써라”는 건 상태 불변식에는 맞을 수 있지만, 에러 확인에는 맞지 않음

결론 부분에 좋은 조언이 있음 nil 체크가 곳곳에 나타난다면 둘 중 하나임. 신뢰할 수 없는 경계 입력을 방어하는 정상적인 코드이거나, 코드베이스가 불변식을 세우지 못한 설계 문제임
어떤 매개변수도 신뢰할 수 없는 시스템에서 해법은 체크를 더 추가하는 게 아님. 당장은 그래야 할 수 있지만, 진짜 일은 그 체크들이 대신하고 있는 불변식을 세우고, 두려움에서 나온 잡음을 시스템이 의존할 수 있는 보장으로 점차 바꾸는 것임
이건 nil 체크를 넘어선다고 봄. 시스템의 “잎” 부분에 체크나 방어 코드를 추가하는 건, 불변식이 부족하거나 제대로 강제되지 않는 증상을 처리하려는 방식으로 자주 나타남
“체크 하나 더 추가”는 기본값으로 삼기 쉽지만 확장 한계가 있다. 어느 순간 체크 로직이 기능 로직보다 많아지고 전체 복잡도가 걷잡을 수 없이 커짐
버그 한두 개를 막기 위한 추가 체크는 보통 해롭지 않지만, 체크 수와 복잡도가 너무 늘어난다고 느껴질 때는 계속 잎만 고치기보다 한 발 물러서서 근본 원인을 찾는 편이 장기적으로 시스템과 유지보수자의 삶에 더 좋았음

불변식을 assert하는 건 처음부터 그렇게 시작하고 계속 유지할 때 훌륭함
다만 개발자들이 방어적 프로그래밍을 멈추도록 훈련시키는 게 더 어려운 문제임

이런 불변식, 여기서는 널 불가성 같은 것은 Go보다 표현력이 풍부한 타입 시스템에서 훨씬 잘 모델링할 수 있음
이 주제에서 가장 좋아하는 글은 Alexis King의 2019년 글 Parse, don't validate임
원칙은 어디에나 적용 가능하지만, Haskell의 타입 시스템에서는 정말 쉬워 보임. TypeScript에서 Alexis의 조언을 몇 년간 따르려 했지만 쉽지 않았음

요약하면 문제는 체크가 너무 많은 게 아니라, nil을 값으로 감싸는 것임

이 문제는 반복해서 나왔는데, 에러 처리가 일급 기능이 아닌 언어의 결과라고 봄
기억상 다른 스레드에서도 나왔듯, 사실상 표준 린터들이 이런 구조를 강제하게 됨
이 nil 체크들이 논리적으로 나쁜지는 모르겠음. 많은 언어가 에러 처리를 내장하고 있고, 차이는 전파의 일관성과 단순성 정도임
에러를 내는 인터페이스에 대응하는 선택지는 대략 네 가지임: 처리하고 복구하기, 무시하기, 에러 전파하기, 에러를 버리고 자기 에러를 전파하기이며 마지막은 기존 에러를 래핑할 수도 있음
에러 처리가 일급 기능인 언어는 보통 2번과 3번을 쉽게 만들고, 현대 언어일수록 그렇다. 그래서 4번도 언어에 따라 꽤 깔끔해질 수 있음
1번은 그런 처리가 필요하다는 점을 더 명시적으로 만드는 것 말고는 일급 지원으로도 크게 도울 수 없음
근본적으로 함수가 에러를 낼 수 있다면 모든 언어는 구현 여부와 별개로 {error,result} = functioncall() 뒤에 if (error) { ... }를 하는 셈임
Go는 에러 처리가 일급이 아니어서 많은 함수가 선제적으로 (result, err) 튜플을 반환하고, 린터가 err != nil 체크를 사실상 강제하면서 코드가 그 패턴으로 가득 차는 인상을 줌
올바른 에러 처리를 언어가 직접 다루지 않는 건 언어 설계 결함이라고 보지만, 일단 그 위치에 있다면 이 모델이 아마 최선에 가까워 보임
Go 코드가 관용적으로 선택적 반환 타입을 써서 기능적으로 무시 가능한 에러와 “신경 써야 하는” 에러를 구분하는지는 잘 모르겠음. 그런 경우에도 항상 에러 타입을 반환하는 게 관용이라면 린터가 늘 이 패턴을 강제할 듯함
Go를 싫어하는 건 아니고 설계 선택 하나에 동의하지 않는 것뿐임. 거의 모든 언어의 설계 선택에는 불평할 수 있음
Go의 가장 큰 실수는 사실상 모든 곳에서 명시적 err != nil 체크가 기능적으로 필수이고, 그래서 린터들도 요구하게 된다는 점이라고 봄

Go가 처음 나왔을 때도 수백 명이 이 전체 구조가 얼마나 우스운지 짚었음
하지만 언어가 큰 인기를 얻었고, Rob Pike가 더 잘 안다는 분위기 속에서 비판은 묵살됐음
이제야 사람들이 논리적인 근거로 정상적으로 논의하는 모습을 보니 좋음
이게 수십 년 전부터 나쁜 아이디어로 알려져 있지 않았던 것도 아닌데, Google이 하면 좋은 거겠지… 맞지?

Go 팬은 아니지만, 이런 프레이밍은 거슬림
“우스운 헛소리”라고 부르면 더 보고 싶다고 한 논리적 사고 자체를 억누르기 쉽기 때문임
어느 Oxide 팟캐스트였는지는 잊었지만 Bryan Cantrill이 “더 잘 싫어하기 위해 이것을 연구하고 싶다”는 식으로 말한 적이 있음
그런 의미에서 2010년대에 사람들이 왜 Go에 열광했는지 이해하고 싶음. 일부는 분명 과대광고였고, 당시 직장에서 개발자들이 왜 좋은지 설명하지 못하면서도 열광하는 모습을 직접 봤음
하지만 순수한 과대광고만은 아니었을 것임. 그 시절 Go를 쓰자는 가장 강한 steel-man argument는 무엇이었을까 궁금함

AI 자동 생성 콘텐츠

본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0