Go의 비밀스러운 삶: Concurrency

발행: (2026년 1월 19일 오후 05:10 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

경쟁 조건의 혼란에 질서를 부여한다.

Chapter 15: Sharing by Communicating

그 화요일, 보관소는 평소와 달리 시끄러웠다. 사람들의 목소리가 아니라 구리 지붕을 두드리는 빗소리, 고층 천장을 가득 메운 혼돈의 드럼 비트였다.

Ethan은 서성을 돌고 있었다. 그의 노트북 팬은 최대 속도로 회전하고 있었다.

“작동은 하는데, 안 돼,” 그는 머리를 휘저으며 말했다. “이 천 개의 로그 파일을 처리하려고 해. 각 파일마다 백그라운드 작업을 만들기 위해 go 키워드를 사용했지. 정말 빠르거든.”
“결과는 어때?” Eleanor가 차를 차분히 저으며 물었다.
“쓰레기야. 가끔은 998개의 결과가 나오고, 가끔은 1005개가 나오고, 가끔은 맵 할당 오류로 프로그램이 크래시돼. 완전 혼란이야.”

그는 코드를 보여주었다:

func processLogs(logs []string) map[string]int {
    results := make(map[string]int)

    for _, log := range logs {
        go func(l string) {
            // Simulate processing
            user := parseUser(l)
            results[user]++ // THE BUG IS HERE
        }(log)
    }
    return results
}

“맞아,” Eleanor가 몸을 기울이며 말했다. “클래식한 race condition이야. 천 개의 goroutine을 띄웠는데, 모두가 같은 맵을 놓고 경쟁하고 있어. 아무것도 그들을 막아주지 않기 때문에 서로의 작업을 덮어쓰고 있거든.”

Ethan: “그럼 락이 필요해? Mutex?”

Eleanor: “Mutex를 사용할 수도 있지, 하지만 그럼 그 변수 하나를 관리하느라 모든 작업을 계속 멈추게 돼. Go에서는 그걸 피하려고 해. 우리에게는 이런 말이 있거든: ‘Do not communicate by sharing memory; share memory by communicating.’

The Goroutine

“먼저,” Eleanor가 말했다, “네가 실제로 만든 것이 뭔지 보자. goroutine은 단순히 함수 호출이 아니야. fire‑and‑forget이야. Go 스케줄러가 그것들을 관리하고, 수천 개의 goroutine을 몇 개의 OS 스레드에 멀티플렉싱해 주지.”

“효율적이겠네,” Ethan이 말했다.
“그렇지. 하지만 독립적이기 때문에, results를 반환하는 메인 함수는 그들 중 어느 것도 기다리지 않아. 작업자들이 아직 끝도 못 했는데 이미 반환해 버려.”
“그게 데이터가 사라지는 이유구나,” Ethan이 깨달았다. “작업자들이 백그라운드에서 실행되는 동안 나는 빈 맵을 반환하고 있었어.”
“정확해. 데이터를 안전하게 다시 받아올 방법이 필요해. 모두가 맵을 건드리는 대신, 데이터를 너에게 다시 전달하도록 하자.”

The Channel

그는 새 파일을 열었다. “우리는 channel을 사용해. 실행 중인 작업들 사이에 데이터를 직접 전달하는 파이프야.”

ch := make(chan string) // Create a channel of strings

“이게 동기화를 대신 해줄 거야,” Eleanor가 설명했다. “데이터를 채널에 보내면, 누군가 받을 때까지 코드가 멈춰. 양쪽이 정확히 맞춰서 동작하게 만들지.”

그는 Ethan의 코드를 리팩터링했다:

func processLogs(logs []string) map[string]int {
    results := make(map[string]int)
    userChan := make(chan string)

    // Spawn workers
    for _, log := range logs {
        go func(l string) {
            user := parseUser(l)
            userChan <- user
        }(log)
    }

    // Collect results
    for i := 0; i < len(logs); i++ {
        user := <-userChan
        results[user]++
    }
    return results
}

“차이점이 보이니?” Eleanor가 물었다. “작업자들은 사용자를 계산하지만 맵을 건드리지 않아. 결과만 전달해 주는 거지. 메인 함수가 기다렸다가 값을 받아서 맵을 업데이트해. 메모리를 건드리는 건 오직 하나뿐이야.”
“그러니까 채널이 사실상 쓰기를 순차화하는 거군요,” Ethan이 깨달았다.
“정확해. 진입점을 하나로 만들었지.”

Blocking Is a Feature

“하지만,” Ethan이 물었다. “채널이 막히면 어떻게 해?”
“기본적으로 백업은 없어,” Eleanor가 대답했다. “직접 전달이니까. 작업자가 userChan에 보낼 때, 메인 goroutine이 값을 받을 때까지 블록돼. 서로를 기다리는 거지.”

Buffered Channels

“이제,” Eleanor가 덧붙였다, “가끔은 그렇게 자주 잠기지 않게 하고 싶을 때가 있어. 큐가 좀 필요하거든.”

ch := make(chan string, 100) // Buffer of 100 slots

“이렇게 하면 버퍼가 생겨. 작업자들은”

can drop off 100 items without waiting. It lets them run a bit faster than the consumer for short bursts. But be careful—once that buffer is full, they block again.

The select Statement

“One last thing,” Eleanor said. “What if you’re waiting on two things? Like getting a result or hitting a timeout?”

select {
case msg := <-ch1:
    // handle msg from ch1
case ch2 <- value:
    // send value to ch2
case <-time.After(5 * time.Second):
    // handle timeout
}

“Do not communicate by sharing memory; instead, share memory by communicating.”
“메모리를 공유하여 소통하지 말고, 소통을 통해 메모리를 공유하라.”

Avoid having multiple goroutines access the same variable (e.g., a map) directly. Instead, pass the data through a channel to a single owner goroutine.

Buffered Channels

ch := make(chan int, 100) // capacity of 100
  • 버퍼드 채널은 고정된 용량을 갖습니다.
  • 전송은 버퍼가 가득 찼을 때 오직 차단되고, 수신은 버퍼가 비었을 때 차단됩니다.

select

  • 하나의 고루틴이 여러 채널 연산을 동시에 대기하도록 허용합니다.
  • 준비된 첫 번째 케이스를 실행합니다.
  • 타임‑아웃, 취소, 그리고 다중 I/O 처리를 위해 필수적입니다.
select {
case msg := <-ch1:
    // handle msg from ch1
case ch2 <- value:
    // send value to ch2
case <-time.After(5 * time.Second):
    // handle timeout
}

다음 장: Context 패키지 – Ethan이 runaway 고루틴을 중지하고 요청 마감 시간을 정중하게 관리하는 방법을 배웁니다.

Aaron Rose는 tech-reader.blog의 소프트웨어 엔지니어이자 기술 작가이며, Think Like a Genius의 저자입니다.

Back to Blog

관련 글

더 보기 »