Go 언어의 철학을 관통하는 가장 중요한 단어는 바로 ‘단순함(Simplicity)’ 입니다. 이는 단순히 기능이 적다는 의미가 아니라, 복잡한 문제를 명료하고 예측 가능하게 해결하기 위한 의도적인 설계 철학입니다. Go의 창시자 중 한 명인 롭 파이크(Rob Pike)는 “단순함은 복잡하다(Simplicity is Complicated)“고 말했습니다. 최고의 단순함을 성취하기 위해 수많은 고민과 트레이드오프가 있었음을 암시하는 말이죠.

이 글에서는 Go의 핵심 철학인 ‘단순함’이 코드 수준에서 어떻게 드러나는지, 그리고 이 철학이 왜 어떤 개발자에게는 최고의 장점이 되고 다른 개발자에게는 답답한 단점으로 여겨지는지 가감 없이 살펴보겠습니다.

(이 글은 Gemini 2.5 Pro 모델에 의해 작성되었으며, 커버하고 있는 세부 항목들과 글의 톤/매너에 대한 요구사항들은 제가 정리해서 Gemini 에 요청했습니다. 따라서 내용에 잘못된 부분이 있을 수 있는데, 그런 경우 잘못된 정보에 대한 댓글을 남겨주시면 감사하겠습니다.)


단순함은 기능의 부재가 아닌, 의도된 설계#

현대의 많은 프로그래밍 언어들은 점점 더 많은 기능을 흡수하며 서로 닮아가는 ‘기능 경쟁’을 벌이고 있습니다. 하지만 Go는 정반대의 길을 선택했습니다. 언어의 사양(specification)을 작게 유지하고, 한 가지 일을 하는 방법은 한두 가지로 제한하며, ‘마법’처럼 보이는 기능을 의도적으로 배제했습니다.

그 목표는 명확합니다.

  • 가독성과 유지보수성: 코드는 쓰는 시간보다 읽는 시간이 훨씬 깁니다. Go는 누가 작성했든 쉽게 읽고 이해할 수 있는 코드를 지향합니다. 이는 여러 개발자가 협업하는 대규모 프로젝트에서 빛을 발합니다.
  • 낮은 인지 부하: 언어가 제공하는 기능이 적으면 개발자는 “어떤 기능을 사용해야 할까?“를 고민하는 대신 문제 해결 자체에 집중할 수 있습니다.
  • 강력한 툴링: 언어가 단순하면 컴파일러, 포맷터(gofmt), 정적 분석기 등 주변 도구를 더 빠르고 강력하게 만들 수 있습니다.

코드로 직접 비교하는 Go의 단순성#

백문이 불여일견입니다. 다른 언어와 코드를 직접 비교하며 Go의 단순함이 실제로 어떻게 나타나는지 살펴보겠습니다. 비교 대상 언어들을 비판하려는 의도는 전혀 없으며, 각 언어의 접근 방식과 철학의 차이를 이해하는 데 목적이 있습니다.

1. 루프(Loop): 단 하나의, for#

프로그래밍의 가장 기본인 반복문부터 시작해 봅시다.

JavaScript / Kotlin JavaScript나 Kotlin 같은 현대 언어들은 데이터 컬렉션을 다루기 위한 매우 표현력 좋고 우아한 도구들을 제공합니다.

// JavaScript: 다양한 방식의 루프
const items = [1, 2, 3];

// 1. 고전적인 for 루프
for (let i = 0; i < items.length; i++) {
  console.log(items[i]);
}

// 2. for...of (iterable 순회)
for (const item of items) {
  console.log(item);
}

// 3. forEach (함수형 스타일)
items.forEach(item => {
  console.log(item);
});

// 4. map (변환 후 새 배열 반환)
const doubled = items.map(item => item * 2);

이러한 접근법은 매우 강력합니다. 특히 map, filter, reduce 같은 함수형 메서드는 코드를 간결하고 선언적으로 만듭니다. 하지만 동시에 “지금 상황에선 어떤 루프를 쓰는 게 가장 좋을까?“라는 행복한 고민을 안겨주기도 합니다.

Go Go는 단 하나의 반복문, for 키워드만 제공합니다. 이 하나로 모든 종류의 반복을 표현합니다.

package main

import "fmt"

func main() {
	items := []int{1, 2, 3}

	// 1. C 스타일의 고전적 for 루프
	for i := 0; i < len(items); i++ {
		fmt.Println(items[i])
	}

	// 2. while 문처럼 사용 (조건식만 사용)
	i := 0
	for i < len(items) {
		fmt.Println(items[i])
		i++
	}

	// 3. for...range (컬렉션 순회)
	for index, value := range items {
		fmt.Printf("인덱스: %d, 값: %d\n", index, value)
	}

    // 4. 무한 루프
    for {
        // ...
        break
    }
}

Go의 선택은 명확합니다. 개발자에게 선택지를 줄이는 대신, 언제나 동일한 패턴으로 코드를 작성하게 하여 일관성과 가독성을 높이는 것입니다.

2. 오류 처리: 명시적인 if err != nil#

오류 처리는 언어의 철학이 가장 극명하게 드러나는 부분 중 하나입니다.

Java / Kotlin (try-catch) Java를 비롯한 많은 언어는 예외(Exception)를 통해 오류를 처리합니다. try-catch 구문은 정상적인 코드 흐름과 오류 처리 로직을 분리해 가독성을 높일 수 있는 강력한 메커니즘입니다.

// Java: try-catch-finally
public void readFile() {
    FileReader reader = null;
    try {
        reader = new FileReader("file.txt");
        // 파일 읽기 로직...
    } catch (FileNotFoundException e) {
        System.err.println("파일을 찾을 수 없습니다: " + e.getMessage());
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

장점은 오류 처리 코드를 한곳에 모을 수 있다는 것이지만, 단점은 try 블록 안의 메서드가 어떤 예외를 던지는지, 그리고 그 예외가 어디서 처리되는지 코드 흐름을 따라가기 어려울 수 있다는 점입니다.

Go Go는 try-catch가 없습니다. 대신, 오류가 발생할 수 있는 함수는 결괏값과 함께 error 타입을 명시적으로 반환합니다. 함수를 호출한 쪽은 즉시 오류를 확인하고 처리해야 합니다.

// Go: 명시적인 오류 값 반환 및 처리
func readFile() {
	data, err := os.ReadFile("file.txt")
	if err != nil {
		// 오류 발생 시 즉시 처리
		log.Fatalf("파일 읽기 실패: %v", err)
		return // 실제론 Fatalf가 프로그램을 종료하므로 필요 없지만, 명시성을 위해
	}
	// 성공 시 로직 계속 진행
	fmt.Println(string(data))
}

if err != nil 패턴은 Go를 처음 접하는 개발자들이 가장 많이 비판하는 부분입니다. 코드가 길어지고 지루해 보이기 때문입니다. 하지만 Go의 설계자들은 이것이 의도된 장점이라고 말합니다.

  • 오류는 예외적인 상황이 아니라, 함수의 또 다른 반환 값일 뿐입니다.
  • 오류를 무시하고 넘어갈 수 없도록 강제합니다.
  • 코드의 제어 흐름이 눈에 보이는 그대로입니다. 갑자기 다른 곳으로 점프하는 ‘마법’이 없습니다.

Rust 참고로 Rust는 Go와 try-catch의 중간 형태인 Result 열거형과 ? 연산자를 통해 명시적이면서도 간결한 오류 처리를 제공하여 많은 사랑을 받고 있습니다.

// Rust: Result 타입과 `?` 연산자
use std::fs;

fn read_file() -> Result<String, std::io::Error> {
    let content = fs::read_to_string("file.txt")?; // 오류 발생 시 자동으로 리턴
    Ok(content)
}

단순성이 가져다주는 명확한 장점 ✨#

  • 압도적인 가독성: Go 프로젝트는 어떤 것을 열어봐도 코드 스타일과 구조가 거의 비슷합니다. 새로운 프로젝트에 적응하는 시간이 매우 짧습니다.
  • 빠른 컴파일 속도와 강력한 툴링: 언어 사양이 작고 의존성 관리 모델이 단순해 컴파일 속도가 매우 빠릅니다. 코드를 저장하는 즉시 gofmt가 자동으로 코드를 포맷팅해주므로, 팀 내에서 코드 스타일로 논쟁할 일이 사라집니다.
  • 배우기 쉬운 언어: 키워드가 25개뿐이고(C++이나 Java의 수백 개와 비교), 기능이 적어 언어의 모든 것을 익히는 데 걸리는 시간이 짧습니다.
  • 간결한 동시성 모델: go 키워드 하나로 함수를 비동기적으로 실행(고루틴)하고, 채널(chan)을 통해 안전하게 데이터를 주고받는 모델은 복잡한 동시성 코드를 놀라울 만큼 쉽게 작성하도록 돕습니다.

단순함의 이면: 트레이드오프와 비판 🤔#

물론, Go의 단순함이 항상 장점인 것은 아닙니다. 이는 명백한 트레이드오프이며, 많은 개발자들이 단점으로 지적하는 부분이기도 합니다.

  • 때로는 장황하게 느껴지는 코드: 위에서 본 if err != nil 패턴이 대표적입니다. 제네릭(Generics)이 도입되기 전까지는 간단한 데이터 구조에 대해서도 타입별로 중복 코드를 작성해야 했습니다.
  • 표현력의 한계: Python이나 Kotlin처럼 간결하고 우아한 한 줄짜리 코드를 작성하기 어렵습니다. 삼항 연산자(condition ? a : b)조차 없습니다. 모든 것은 명시적이고, 때로는 직설적으로 보입니다.
  • 느리게 추가된 ‘현대적’ 기능들: 제네릭은 Go 커뮤니티의 오랜 요구사항이었지만, Go 1.18에서야 매우 신중하게 추가되었습니다. Go 팀은 새로운 기능을 추가할 때 그것이 Go의 단순함이라는 핵심 철학을 해치지 않는지 극도로 보수적으로 접근합니다. 이로 인해 다른 언어에서는 당연하게 여겨지는 기능들이 없어서 불편함을 느낄 수 있습니다.

결론: 목적이 있는 도구#

Go의 단순함은 ‘미완성’이거나 ‘기능 부족’이 아닌, ‘대규모 소프트웨어를 안정적으로 개발하고 유지보수하기’ 라는 명확한 목적을 위해 고도로 다듬어진 설계 철학의 결과물입니다.

Java나 Kotlin의 풍부한 기능과 표현력, Rust의 강력한 타입 시스템과 메모리 안정성, JavaScript의 유연함과 거대한 생태계는 각자의 영역에서 매우 훌륭한 장점입니다. Go는 그들과 경쟁하는 대신, ‘명료함’과 ‘예측 가능성’ 이라는 다른 가치를 최우선으로 둡니다.

Go의 철학이 당신에게 매력적으로 다가올지, 혹은 답답한 제약으로 느껴질지는 당신이 해결하려는 문제와 당신의 개발 가치관에 달려 있습니다. Go는 모든 문제에 대한 최고의 해결책은 아니지만, 자신이 목표하는 영역에서는 그 어떤 언어보다 강력한 생산성과 안정성을 보여주는, 목적이 뚜렷한 훌륭한 도구임이 틀림없습니다.