Go언어, 동시성 철학: “공유 메모리가 아닌, 통신으로 메모리를 공유하라”

Go 언어에는 독특한 동시성 철학이 존재한다.
바로 "Don't communicate by sharing memory; instead, share memory by communicating."
이는 단순한 코드 스타일 가이드가 아니라, Go가 동시성 문제를 어떻게 바라보는지를 명확히 보여주는 핵심 원칙이다.


공유 메모리 중심의 전통적인 방식

다른 언어(C, Java 등)에서는 여러 스레드가 같은 변수나 객체에 접근하면서 동기화를 위한 수단으로 락(Mutex), 세마포어 등을 사용한다.
이러한 방식은 메모리를 중심으로 동시성을 처리한다. 예를 들면 아래와 같다:

var count int
var mu sync.Mutex

func increment() {
    mu.Lock()
    count++
    mu.Unlock()
}

이 방식은 단순해 보일 수 있지만, 스레드가 늘어날수록 코드 복잡성이 증가하고, 데드락이나 레이스 컨디션과 같은 동시성 오류 가능성도 함께 높아진다.


Go의 철학: 채널을 통한 명시적 통신

Go는 이러한 전통적인 접근 대신, 채널(channel) 을 통해 고루틴 간의 명시적인 통신을 권장한다.
핵심은 데이터는 공유하지 않고, 메시지를 통해 주고받는다는 점이다.

예시는 다음과 같다:

func counter(ch chan int) {
    count := 0
    for {
        ch <- count
        count++
    }
}

func main() {
    ch := make(chan int)
    go counter(ch)

    fmt.Println(<-ch) // 0
    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
}

이 코드에서는 count 변수에 여러 고루틴이 직접 접근하지 않는다.
counter 함수 내부에서만 count가 관리되며, 외부 고루틴은 오직 채널을 통해서만 값을 받아볼 수 있다.
이로 인해 락 없이도 안전한 동시성 처리가 가능해진다.


왜 이 방식이 중요한가?

Go의 채널 기반 동시성은 단순히 문법상의 편의가 아니다.
이는 안전성과 명확성을 최우선으로 하는 설계 철학이다.
메모리를 공유하려면 반드시 의도를 갖고 명시적으로 통신을 해야 하며, 이는 코드의 안정성과 가독성, 유지보수성에 큰 영향을 미친다.

또한 이러한 설계는 CSP(Communicating Sequential Processes)라는 이론적 기반 위에 놓여 있다.
Go는 이 개념을 실질적인 프로그래밍 모델로 끌어내렸고, 실제로도 복잡한 동시성 문제를 효과적으로 다룰 수 있게 한다.


정리

Go 언어가 권장하는 동시성 방식은 다음과 같은 점에서 유의미하다:

  • 명시적 통신을 통해 동시성을 관리하므로, 코드의 의도가 명확하다.
  • 락과 같은 저수준 동기화 기법보다 추론하기 쉽고 오류 가능성이 낮다.
  • 복잡한 공유 메모리 모델보다 훨씬 단순하고 직관적인 동시성 모델을 제공한다.

Go에서 동시성을 구현할 때는 항상 이 문장을 떠올리는 것이 좋다:

“공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라.”

이 철학을 이해하고 따르는 것이 Go스러운 코드를 작성하는 출발점이 될 수 있다.