Skip to main content
  1. Guides 리스트/
  2. Go Concurrency 마스터하기/

Go Concurrency 마스터하기: 동시성의 내부, 스케줄러부터 프로파일링까지

·549 words·3 mins·
Go Concurrency 완벽 가이드 - This article is part of a series.
Part 13: This Article

우리는 지금까지 “고루틴은 가볍다”, “런타임이 알아서 관리해 준다"라고만 알고 있었습니다.

하지만 어떻게 그게 가능한 걸까요?

이번 마지막 장에서는 Go 동시성의 엔진룸을 열어보고, 성능을 최적화하는 도구들을 살펴보겠습니다.

동시성 (Concurrency) vs 병렬성 (Parallelism)
#

  • 하드웨어 레벨: CPU 코어 수만큼 병렬로 명령어를 실행합니다. (4코어 = 4개 동시 실행)
  • OS 레벨 (스레드): 운영체제 스케줄러가 스레드들을 CPU 코어에 번갈아 할당합니다. (시분할)
  • Go 런타임 레벨 (고루틴): Go 스케줄러가 고루틴들을 OS 스레드에 번갈아 할당합니다.

Go 런타임은 M:N 모델을 사용합니다.

M개의 고루틴을 N개의 OS 스레드 위에서 실행시키는 것이죠.

이 덕분에 고루틴은 수천, 수만 개가 생성되어도 OS 스레드 생성 비용이 들지 않아 매우 가볍습니다.

고루틴 스케줄러 (Goroutine Scheduler)
#

Go 스케줄러의 핵심 원리는 작업 훔치기(Work Stealing)와 선점형 스케줄링(Preemptive Scheduling)입니다.

  1. 큐(Queue): 실행 대기 중인 고루틴들이 큐에 쌓입니다.
  2. 실행: 스레드가 큐에서 고루틴을 꺼내 실행합니다.
  3. 블로킹 처리: 고루틴이 채널 대기 등으로 멈추면, 스레드는 즉시 다른 고루틴을 실행합니다.
  4. 시스템 콜: 만약 고루틴이 시스템 콜(파일 읽기 등)로 스레드 자체를 블로킹시키면? 런타임은 새로운 스레드를 생성하거나 대기 중인 스레드를 깨워서 나머지 고루틴들을 계속 실행시킵니다.

그리고 10ms마다 실행 중인 고루틴을 잠시 멈추고 다른 고루틴에게 기회를 주는 선점형 스케줄링 덕분에, 특정 고루틴이 CPU를 독점하는(Starvation) 현상을 막습니다.

GOMAXPROCS
#

GOMAXPROCS는 동시에 실행될 수 있는 최대 OS 스레드 개수를 결정합니다.

기본값은 CPU 코어(논리 프로세서) 수와 같습니다.

    fmt.Println(runtime.GOMAXPROCS(0)) // 현재 설정값 확인
    runtime.GOMAXPROCS(2)              // 2개로 제한

Go 1.25+ 업데이트:

이전에는 컨테이너 환경(Docker/K8s)에서 CPU 제한을 걸어도 GOMAXPROCS가 호스트 머신의 전체 코어 수를 따라가는 문제가 있었습니다.

하지만 Go 1.25부터는 CPU Quota(cgroup)를 자동으로 인식하여 적절한 GOMAXPROCS 값을 설정해 줍니다. (이제 uber-go/automaxprocs 같은 라이브러리가 필요 없어졌습니다!)

동시성 원시 타입의 내부 (Internals)
#

  • 고루틴 (runtime.g): 2KB의 작은 스택으로 시작하며, 필요에 따라 동적으로 늘어납니다.
  • 채널 (runtime.hchan): 내부적으로 링 버퍼(배열), 대기열 큐, 그리고 상태 보호를 위한 뮤텍스(lock)를 가지고 있습니다.
  • Select (runtime.selectgo): 준비된 케이스를 무작위로 선택하여 기아 현상을 방지합니다.

스케줄러 지표 (Metrics)
#

프로그램이 느려지면 런타임 내부를 들여다봐야 합니다.

runtime/metrics 패키지를 통해 다양한 지표를 볼 수 있습니다.

  • /sched/goroutines:goroutines: 현재 살아있는 고루틴 수 (누수 탐지에 유용)
  • /sched/goroutines/runnable:goroutines: 실행 대기 중인 고루틴 수 (너무 많으면 CPU 부족 신호)

보통은 프로메테우스(Prometheus) 같은 모니터링 도구와 연동하여 시각화합니다.

프로파일링 (Profiling) - pprof
#

Go는 pprof라는 강력한 프로파일링 도구를 내장하고 있습니다.

성능 병목을 찾고 싶다면 net/http/pprof만 import 하면 됩니다.

import _ "net/http/pprof"

func main() {
    go http.ListenAndServe("localhost:6060", nil)
    // ...
}

이제 브라우저나 터미널에서 프로파일을 수집할 수 있습니다.

  • CPU 프로파일: 어떤 함수가 CPU를 많이 쓰는지 보여줍니다.
  • Heap 프로파일: 메모리 누수나 과도한 할당을 찾습니다.
  • Block / Mutex 프로파일: 고루틴이 어디서 오래 대기하는지, 락 경합이 심한 곳은 어딘지 알려줍니다. (활성화 필요)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile

위 명령어를 실행하면 웹 브라우저에서 플레임 그래프(Flame Graph)를 통해 성능 병목 지점을 직관적으로 확인할 수 있습니다.

트레이싱 (Tracing)
#

프로파일링이 “어디가 느린가"를 알려준다면, 트레이싱(Tracing)은 “시간 순서대로 무슨 일이 일어났나"를 보여줍니다.

고루틴이 언제 생성되고, 언제 스케줄링되고, 언제 블로킹되었는지 타임라인으로 볼 수 있습니다.

    f, _ := os.Create("trace.out")
    trace.Start(f)
    defer trace.Stop()

수집된 파일은 go tool trace 명령어로 시각화해서 분석할 수 있습니다.

특히 예측 불가능한 지연 시간(Latency) 문제를 해결할 때 매우 유용합니다.

플라이트 레코더 (Flight Recorder) - Go 1.25+
#

항상 트레이싱을 켜두면 데이터가 너무 커집니다.

Go 1.25에 추가된 trace.FlightRecorder는 비행기 블랙박스처럼 최근 N초간의 데이터만 메모리에 유지하다가, 문제가 발생했을 때(예: 에러 로그 발생 시) 파일로 저장할 수 있게 해 줍니다.


이로써 “Go Concurrency 마스터하기” 시리즈가 막을 내립니다.

고루틴의 기초부터 채널, 컨텍스트, 뮤텍스, 원자적 연산, 테스트, 그리고 내부 동작 원리까지 긴 여정을 함께해 주셔서 감사합니다.

동시성 프로그래밍은 어렵지만, Go가 제공하는 도구들을 올바르게 이해하고 사용한다면 그 어떤 언어보다 쉽고 강력하게 멀티 코어 시스템을 구축할 수 있습니다.

Go Concurrency 완벽 가이드 - This article is part of a series.
Part 13: This Article