지루한 소프트웨어에 대한 논증: Go가 종종 올바른 선택인 이유
현대 소프트웨어 엔지니어링 시대에는 복잡성을 증가시키는 흐름이 만연합니다. 6개월마다 라우팅 규칙을 바꾸는 JavaScript 메타‑프레임워크의 급증, 간단한 폼을 제공하기 위해 Kubernetes 클러스터를 도입하는 사례, 초당 몇 건의 요청만 처리하는 CRUD 애플리케이션에 Rust를 고집하는 현상을 볼 수 있습니다. ‘최고의’ 도구를 찾다 보면 많은 팀이 깨지기 쉬운, 과도하게 설계된, 유지보수가 힘든 시스템을 우연히 만들게 됩니다.
가장 생산적인 길은 ‘지루한’ 선택을 받아들이는 것이라는 강력한 주장이 있습니다. 구체적으로는 Go에 대한 논증: 영리함을 위해 설계된 것이 아니라 배포를 위해 설계된 언어입니다.
지루함의 철학
Go는 의도적으로 제한됩니다. 데코레이터, 메타클래스, 매크로, 그리고 Haskell이나 Rust에서 볼 수 있는 복잡한 추상화가 없습니다. 대신 구조체, 함수, 인터페이스, goroutine, 채널이라는 소수의 기본 요소만 제공합니다.
이 최소주의는 버그가 아니라 기능입니다. 언어가 지루할 때, 2년 전 수석 엔지니어가 작성한 코드는 오늘 입사한 주니어도 쉽게 읽을 수 있습니다. gofmt가 강제하는 정확히 하나의 코드 포맷 방식이 있어 공백에 대한 "성전"을 없애고 diff를 깔끔하게 유지합니다. "영리함"을 제한함으로써 Go는 코드베이스를 이해할 수 없게 만드는 17계층 추상화의 생성을 방지합니다.
프레임워크로서의 표준 라이브러리
Go의 가장 큰 장점 중 하나는 포괄적인 표준 라이브러리입니다. 많은 개발자가 "올바른" 프레임워크(Express, Django, Rails 등)를 찾느라 몇 주를 소비하는 반면, Go는 표준 라이브러리가 바로 프레임워크라고 제안합니다.
라우팅을 위한 net/http, 렌더링을 위한 html/template, 영속성을 위한 database/sql만으로도 거의 제3자 의존성 없이 완전한 웹 애플리케이션을 만들 수 있습니다. 이 접근 방식은 Node.js 생태계에서 흔히 볼 수 있는 "dependency hell"을 없애줍니다. 단 하나의 패키지가 삭제되면 새벽 3시에 프로덕션 빌드가 무너지는 상황을 방지합니다.
또한 Go가 io.Reader와 io.Writer 인터페이스를 활용함으로써 강력하고 조합 가능한 생태계를 만들었습니다. 거의 모든 패키지가 이 두 인터페이스를 구현하므로 HTTP 응답을 gzip 라이터에 파이프하고 다시 디스크 파일에 쓰는 작업이 몇 줄의 코드로 손쉽게 수행됩니다.
동시성 및 배포
Go의 동시성 모델은 goroutine—스택을 갖춘, 다중화된 스레드—을 중심으로 설계되었습니다. Node.js 이벤트 루프나 전통적인 OS 스레드와 달리, 수십만 개의 goroutine을 일반 노트북에서 시스템이 다운되지 않을 정도로 실행할 수 있습니다. 채널을 통한 동기화와 결합하면, Go는 병렬 처리를 라이브러리 수준이 아닌 언어 수준의 일등 시민으로 만듭니다.
이 단순함은 배포에도 그대로 이어집니다. Go는 단일 정적 링크 바이너리로 컴파일됩니다. 배포 과정은 간단한 복사 명령으로 축소됩니다: 바이너리를 빌드하고, scp로 서버에 전송한 뒤 서비스를 재시작하면 됩니다. 복잡한 Dockerfile, 다단계 빌드, 혹은 단순 애플리케이션을 위한 서비스 메쉬와 같은 오버헤드를 완전히 생략할 수 있습니다.
반론: Go가 부족한 부분
Go는 강점에도 불구하고 만능은 아닙니다. 커뮤니티와 비평가들은 다음과 같은 반복적인 문제점을 지적합니다:
1. 장황한 오류 처리
범용적인 if err != nil 패턴은 가장 흔한 불만 사항입니다. 옹호자들은 모든 실패 지점을 의식적으로 처리하도록 강제한다며 장점으로 내세우지만, 비판자들은 실제 비즈니스 로직을 가리는 "if 문 벽"을 만든다고 주장합니다. 한 댓글 작성자는 다음과 같이 말했습니다:
"코드에 거의 동일한
if문이 여기저기 흩어져 있어... 모든if문을 보면서 눈이 멀어 차이를 찾을 수 없습니다."
2. 생태계 격차
표준 라이브러리는 깊지만, 특정 작업에 대해서는 빈약할 수 있습니다. 예를 들어 Go에는 .NET의 Entity Framework Core나 Rails의 ActiveRecord에 버금가는 내장형 강력한 데이터베이스 마이그레이션 도구가 없습니다. 개발자는 이러한 유틸리티를 직접 구현하거나 파편화된 서드파티 라이브러리 사이를 헤매야 합니다.
3. 타입 시스템 제한
Rust나 Swift와 비교하면 Go의 타입 시스템은 단순합니다. 대수 데이터 타입(ADTs)과 enum이 없기 때문에 복잡한 데이터 상태를 표현하기가 번거롭고, 다른 언어가 기본 제공하는 타입 안전성을 얻기 위해 종종 "해킹"이 필요합니다.
4. “Fat Binary” 보안 문제
단일 바이너리가 배포를 단순화하지만 보안 스캔을 복잡하게 만들 수 있습니다. 표준 라이브러리가 바이너리에 포함되기 때문에 CVE 스캐너가 애플리케이션이 실제로 사용하지 않는 라이브러리 부분의 취약점을 자주 표시해, 거짓 양성 경보가 폭주합니다.
결론: 올바른 도구 선택
Go와 경쟁 언어 간 논쟁은 종종 개발자 표현력과 운영 단순성 사이의 트레이드오프로 귀결됩니다.
절대적인 타입 안전성과 메모리 효율성이 가장 중요한 고복잡도 시스템을 구축한다면 Rust가 더 나은 선택일 수 있습니다. 빠른 웹 개발을 위한 실용적이고 배터리 포함 프레임워크가 필요하다면 Rails나 Django가 여전히 강력한 옵션입니다.
하지만 대부분의 백엔드 서비스—특히 수년간 교체되는 개발 팀이 유지보수해야 하는 경우—에서는 지루한 선택이 종종 올바른 선택입니다. 형식과 영리함을 없애고 Go는 팀이 실제로 중요한 일, 즉 작동하는 소프트웨어를 배포하는 일에 집중하도록 해줍니다.