Go에서 오류 처리 구문 지원을 추가할지 여부에 대해 논의하고, 과거의 제안들과 최근의 논의, 그리고 커뮤니티의 의견을 바탕으로 앞으로의 방향성을 안내합니다.
Go에서 가장 오래되고 끈질긴 불만 중 하나는 오류 처리의 장황함입니다. 우리 모두는 이 코드 패턴에 너무나도 익숙합니다(어떤 이들은 고통스럽다고까지 말합니다):
gox, err := call() if err != nil { // 오류 처리 }
if err != nil
테스트가 너무 자주 등장해 코드의 나머지 부분이 묻혀버리는 경우가 많습니다. 이는 API 호출이 많은 프로그램, 그리고 오류 처리가 단순해서 단순 반환만 하는 프로그램에서 흔히 벌어집니다. 아래와 같이 생긴 코드를 본 적 있을 것입니다:
gofunc printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return err } y, err := strconv.Atoi(b) if err != nil { return err } fmt.Println("result:", x + y) return nil }
함수 본문 10줄 중, 실질적으로 일을 하는 줄은 호출 부분과 마지막 두 줄을 합쳐 네 줄뿐입니다. 나머지 여섯 줄은 잡음처럼 느껴집니다. 이 장황함은 현실이고, 이로 인해 오류 처리 방식에 대한 불만은 수년간 Go 사용자 조사에서 항상 상위권을 차지했습니다. (잠깐, 제네릭 미지원이 상위권이던 시기도 있었지만, Go에서 제네릭을 지원하게 되면서 다시 오류 처리가 1위로 올라왔습니다.)
Go 팀은 커뮤니티의 피드백을 진지하게 받아들이며, 지난 수년간 이 문제에 대한 해결책을 Go 커뮤니티와 함께 고민해 왔습니다.
Go 팀이 처음으로 명시적으로 나선 시점은 2018년입니다. 당시 Russ Cox가 우리가 "Go 2" 프로젝트라 불렀던 작업의 일환으로 문제를 공식적으로 기술했고, Marcel van Lohuizen의 초안 설계를 기반으로 check
와 handle
메커니즘을 제안했습니다. 이 설계는 매우 포괄적이었으며, 다른 언어들의 접근 방식과 대안들을 상세히 분석했습니다. 여러분이 떠올린 오류 처리 아이디어가 예전에 검토된 적이 있는지 궁금하다면, 반드시 이 문서를 읽어보시기 바랍니다!
go// 제안된 check/handle 메커니즘을 사용한 printSum 구현 예시 func printSum(a, b string) error { handle err { return err } x := check strconv.Atoi(a) y := check strconv.Atoi(b) fmt.Println("result:", x + y) return nil }
하지만 check
와 handle
방식은 너무 복잡하다는 평가를 받았고, 1년 뒤인 2019년에는 악명높은try
제안으로 단순화되었습니다. 이는 check
와 handle
의 아이디어를 가져왔으나, check
는 내장 함수 try
로 바꾸고 handle
은 생략하는 식이었습니다. try
의 영향을 알아보기 위해 기존 오류 처리 코드를 try
를 사용하여 변환하는 tryhard 도구도 만들었습니다. 해당 제안은 GitHub 이슈에서 900여 개가 넘는 댓글로 뜨거운 논란을 일으켰습니다.
go// 제안된 try 메커니즘을 사용한 printSum 구현 예시 func printSum(a, b string) error { // defer를 사용해 반환 전 오류를 보강할 수 있음 x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
하지만 try
는 오류 발생 시, 외부 함수에서 반환하는 식으로 흐름 제어에 영향을 미쳤고, 이로 인해 코드의 흐름이 감춰지는 문제가 있었습니다. 그래서 많은 이들에게 이 제안은 거부감을 샀고, 결국 이 노력도 접을 수밖에 없었습니다. 돌이켜 보면, 만약 새로운 키워드를 도입했다면(GO는 현재 go.mod
와 파일별 지시자를 통한 버전 제어가 가능하니) 더 좋았을지도 모릅니다. 또한 try
의 사용을 변수 할당 및 문장으로만 제한했다면 일부 우려가 해소됐을지도 모릅니다. 최근 Jimmy Frasche의 새 제안은 원래의 check
/handle
설계로 되돌아가 그 한계를 보완하는 방향으로 나아가고 있습니다.
try
제안의 여파는 Russ Cox의 블로그 시리즈(“Thinking about the Go Proposal Process”)로까지 이어졌습니다. 결론 중 하나는, 거의 완성된 제안을 우선 제시해 커뮤니티의 피드백 여지를 줄이고, "위협적인" 구현 일정을 공표함으로써 더 나은 결과의 가능성을 스스로 줄였다는 것이었습니다. “Go Proposal Process: Large Changes”에서: "회고컨대, try
는 충분히 큰 변화였으므로 우리가 발표한 새 설계는 [...] 두 번째 초안이 되었어야 하며, 구현 타임라인이 걸린 제안이 되어서는 안 됐다"라고 합니다. 아무튼 과정과 소통 문제가 있었든 아니든, 이 제안에 대한 사용자 반향은 분명히 부정적이었습니다.
당시는 더 나은 해결책이 없었기에 이후로 수년간 오류 처리 관련 구문 변경을 더 이상 추진하지 않았습니다. 하지만 커뮤니티에서 영감을 얻은 수많은 사람이 유사하거나 새로운 오류 처리 제안들을 꾸준히 제출했습니다. 이를 정리하기 위해 1년 뒤 Ian Lance Taylor가 우산 이슈를 만들어 현재까지 제안된 모든 주요 오류 처리 변경안의 현황을 요약했고, 피드백 및 토론, 기사 등을 모은 Go 위키가 생겼습니다. 별도로 Sean K. H. Liao의 블로그처럼, 역사상 모든 오류 처리 제안을 정리한 글도 있습니다. 그 양이 놀라울 따름입니다.
오류 처리의 장황함에 대한 불만은 여전했고(Go 개발자 설문조사 2024 H1 결과 참고), 결국 보다 다듬어진 여러 내부 제안을 거쳐, Ian Lance Taylor가 2024년 “? 기호를 사용한 오류 처리 보일러플레이트 감소” 제안을 발표했습니다. 이는 Rust의 ?
연산자에서 차용한 것으로, 이미 자리잡힌 표기법을 활용하고 그간의 교훈까지 반영해 드디어 진전을 이루려 했던 시도였습니다. 비공식 사용자 연구에서 Go 코드의 ?
사용 의미를 대부분의 참가자가 정확히 추측하기도 했습니다. 영향 분석을 위해, Ian은 기존 Go 코드를 새 구문으로 변환하는 도구와 프로토타입까지 만들었습니다.
go// 제안된 "?" 문장을 사용한 printSum 구현 예시 func printSum(a, b string) error { x := strconv.Atoi(a) ? y := strconv.Atoi(b) ? fmt.Println("result:", x + y) return nil }
그러나 이 역시 다른 오류 처리 제안과 마찬가지로 개별 취향에 따라 소폭의 조정을 요구하는 수많은 댓글이 몰렸습니다. Ian은 이 제안을 닫고 내용을 토론으로 옮겨 피드백을 수집하였습니다. 약간 수정된 버전이 조금 더 긍정적으로 받아들여지긴 했으나, 광범위한 지지는 여전히 부족했습니다.
Go 팀의 세 차례 전면적 제안과 진짜로 수백 (!)건에 달하는 커뮤니티 제안이 있었으나, 대다수는 변주에 가까웠고, 모두 충분한(혹은 압도적) 지지를 받지 못했습니다. 이제 우리의 질문은: 앞으로 어떻게 할 것인가? 계속 추진해야 할까?
우리는, 그렇지 않다고 생각합니다.
좀 더 정확히 말하면, 적어도 당분간 구문적(syntactic) 문제 해결 시도는 멈춰야 한다는 것입니다. 제안 프로세스는 이 결정을 뒷받침합니다:
제안 프로세스의 목적은 (이슈 트래커의 논의를 통해) 합리적인 시간 내에 광범위한 합의를 이루는 것입니다. 만약 합의에 이르지 못하면, 제안은 보통 거부됩니다.
더불어서:
아주 드물게, 논의에서 합의가 도출되지 않더라도 제안을 즉시 거절하지는 않습니다. 만약 제안 검토 그룹이 합의점이나 다음 단계를 찾지 못하면, Go 아키텍트가 논의를 검토하고 내부적으로 합의점을 찾아 결정합니다.
어떤 오류 처리 제안도 합의에 가까운 찬성을 얻지 못해서 모두 거부되었습니다. Google 내 Go 팀 최고참 멤버 간에도 지금은 가장 좋은 방안에 대한 견해 일치가 없습니다(언젠가는 바뀔 수도 있습니다). 강력한 합의 없이는, 합리적으로 나아갈 수 없는 겁니다.
현 상태의 장점도 분명 있습니다:
Go가 처음부터 오류 처리 전용 구문 당의를 도입했다면, 지금처럼 갑론을박이 길지 않았을 것입니다. 하지만 이제 도입 15년이 지났고, 장황해 보일지언정 Go에는 문제없는 오류 처리 방법이 자리잡았습니다.
또 다른 관점에서, 오늘날 완벽한 해결책이 나온다 해도, 언어에 포함시키면 도리어 한 불만 집단(변화를 원하는 쪽)에서 다른 불만 집단(현 상태 선호 쪽)으로 갈등이 옮겨갈 뿐입니다. 제네릭 도입 때도 비슷한 논란이 있었으나, 결정적 차이점은: 제네릭은 사용하지 않아도 되며, 좋은 제네릭 라이브러리는 사실상 타입 추론 덕분에 일반 사용자들은 그 존재를 크게 의식하지 않아도 됩니다. 반면, 오류 처리 구문 추가는 거의 모든 사용자가 반드시 적응해야 하고, 그렇지 않으면 비정상적인 코드가 됩니다.
추가적인 구문 제공을 최소화하는 것은 Go의 설계 원칙 중 하나입니다. (예외적으로 활용 빈도가 매우 높을 땐 예외를 허용함; 예: 대입 연산자). 아이러니하게도, 재대입(변수 재선언) 가능한 짧은 변수 선언 (:=
) 도입은 오류 처리 때 맞닥뜨리는 "err" 변수명을 매번 새로 지정하거나 별도의 선언문을 추가하는 번거로움을 줄이기 위한 것이었습니다. 당시 오류 처리 구문을 더 넣었으면 재선언 규칙이 필요 없었을지도 모릅니다.
실제로 오류를 "제대로" 처리할 경우 장황함은 크게 두드러지지 않습니다. 좋은 오류 처리는 추가 정보를 오류에 덧붙이는 경우가 많습니다. (예: 스택트레이스) 아래(조금 인위적인) 예시를 보면, 보일러플레이트 비중이 훨씬 작아집니다:
gofunc printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return fmt.Errorf("invalid integer: %q", a) } y, err := strconv.Atoi(b) if err != nil { return fmt.Errorf("invalid integer: %q", b) } fmt.Println("result:", x + y) return nil }
cmp.Or
로 여러 오류를 한 번에 처리할 수도 있습니다:gofunc printSum(a, b string) error { x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { return err } fmt.Println("result:", x+y) return nil }
코드를 작성, 읽기, 디버깅하는 것은 각기 다릅니다. 반복 오류 체크는 작성 때 지루할 수 있지만, 현대 IDE는 LLM 기반 코드 완성도 제공합니다. 단순 오류 체크는 이런 도구에 적합합니다. 장황함은 코드 "읽기" 때 가장 두드러지며, 이때 IDE가 오류 처리 코드를 숨기는 토글 스위치를 제공할 수도 있습니다(함수 본문 등 다른 코드 구역에는 이미 존재합니다).
디버깅 중에는 println
추가나, 전용 줄/위치에 브레이크포인트를 쉽게 찍을 수 있는 게 중요합니다. 이 기준에서라면 이미 전용 if
문이 있는 것이 낫습니다. 모든 오류 처리 로직이 check
, try
, 또는 ?
등으로 숨겨져 있으면, 다시 if
문으로 바꾸어야 하니 디버깅이 복잡해지고 미세한 버그가 생길 수 있습니다.
현실적으로는, 오류 처리용 새 구문 아이디어는 내기 쉽지만, 괜찮은 언어 변화는 제대로 설계·구현까지 하려면 많은 노력이 필요합니다. 그리고 진짜 비용은 이후, 즉 기존 코드 변화, 문서 및 툴 보완에 들어갑니다. 언어 변화의 비용은 매우 크고, Go 팀은 비교적 적으며, 처리해야 할 다른 우선순위도 많습니다. (이 점들은 향후 달라질 수 있습니다. 우선순위와 인원이 변할 수 있기 때문입니다.)
마지막으로, 최근 Google Cloud Next 2025에서 Go 부스를 운영하며 소규모 모임도 했는데, 만난 대부분의 Go 사용자는 오류 처리 구문 개선을 위해 언어를 바꾸면 안 된다고 강하게 주장했습니다. 대부분은 Go만큼 간결한 오류 처리 지원이 없는 타 언어 출신일 때에만 불편함을 느낀다고 전했습니다. Go에 익숙해질수록, 그리고 더 관용적인 Go 코드를 쓰게 될수록 불만이 점차 줄어든다고들 했습니다. (물론 이것만으로 전체 여론을 대표할 순 없지만, GitHub 등에서 볼 수 있는 목소리와는 또 다른 데이터 포인트입니다.)
물론, 변화가 필요하다는 주장에도 일리는 있습니다:
더 낫은 오류 처리 지원 미제공은 여전히 사용자 조사에서 1위 불만입니다. 정말 Go 팀이 사용자 의견을 존중한다면 언젠가는 움직여야 할지도 모릅니다. (다만 압도적 지지가 있는 것도 아닙니다.)
단순히 글자 수 줄이기에 집중하는 것은 잘못일 수 있습니다. "err != nil"식 보일러플레이트는 줄이면서, 기본 오류 처리에 키워드를 도입해 더 잘 보이도록 하는 편이 나을 수도 있습니다. 이러면 코드 리뷰어가 "두 번 쳐다보지 않아도" 오류 처리를 인지할 수 있어 코드 품질과 안전성이 올라갑니다. 이는 다시 check
와 handle
논의의 시발점으로 돌아가는 일이 되겠지요.
실은 단순한 구문장황이 문제인지, 제대로 된 오류 정보 제공의 장황함이 더 문제인지는 아직 연구가 더 필요합니다.
그럼에도, 지금까지 그 어떤 시도도 충분한 동력을 얻지 못했습니다. 솔직히 우리가 어디에 와 있는지 돌아보자면, 문제에 대한 공감대도 없을 뿐 아니라, 문제가 실재하는지조차 모두 인정하지 않는 상황입니다. 이런 현실적 상황에서, 다음과 같은 실용적 결정을 내립니다:
가까운 미래에는 오류 처리의 구문적 언어 변경을 더 이상 추진하지 않겠습니다. 오류 처리의 구문에 관한 모든 열린/신규 제안 역시 추가 조사 없이 종료하겠습니다.
커뮤니티는 이 문제를 탐색·토의·토론하며 방대한 노력을 쏟았습니다. 비록 오류 처리 구문 자체는 변화하지 못했지만, 이 과정은 Go의 많은 발전과 프로세스 개선을 가져왔습니다. 언젠가 미래에는 오류 처리에 대한 뚜렷한 결론이 나올지도 모릅니다. 그때까지는 이 놀라운 열정을 Go를 모두에게 더 나은 언어로 만드는 데 집중하겠습니다.
감사합니다!