Go는 가비지 컬렉터로 메모리를 자동 관리하여, 수동 할당/해제로부터 자유롭게 합니다. 그 GC는 일시 정지 시간을 최소화하도록 설계된 동시적, 저지연, 비세대(non-generational) 마크 앤 스위프 컬렉터입니다 — Go가 지연에 민감한 서비스에 적합한 핵심 이유입니다.
스택 vs 힙과 이스케이프 분석
{
x :=
x
}
* {
x :=
&x
}
Go 컴파일러는 컴파일 시점에 이스케이프 분석을 수행합니다: 함수보다 오래 살지 않는 값은 저렴하고 자동으로 해제되는 스택에 머물고; 참조가 이스케이프하는 값은 힙(GC가 관리)으로 갑니다. 스택 할당은 사실상 공짜이므로, 힙 이스케이프를 최소화하면 GC 작업이 줄어듭니다.
Go GC 특성:
✓ 마크 앤 스위프 — 도달 가능한 객체를 표시(루트에서), 나머지를 쓸어냄
✓ 동시적 — 대부분 프로그램과 나란히 실행(stop-the-world 아님)
✓ 저지연 — 처리량이 아닌 짧은 일시 정지(보통 1밀리초 미만)에 최적화
✓ 비세대 — JVM과 달리 모든 객체를 균일하게 취급(더 단순)
Go는 의도적으로 처리량보다 낮은 일시 정지 시간을 우선합니다 — GC가 대부분의 작업을 프로그램과 동시에 수행하여 "stop-the-world" 일시 정지를 매우 짧게 유지합니다. 이는 Go를 지연에 민감한 서버에 강력하게 만듭니다.
GOGC=100 # 기본값: 마지막 수집 이후 힙이 100% 자라면 GC 트리거
# 높은 GOGC → 덜 빈번한 GC(메모리 더, CPU 덜)
# 낮은 GOGC → 더 빈번한 GC(메모리 덜, CPU 더)
GOMEMLIMIT=2GiB # 소프트 메모리 한계(Go 1.19+) — 근처에서 GC가 더 열심히 일함
// 1. sync.Pool로 객체를 재사용하여 할당 감소
var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
// 2. 알려진 용량으로 slice를 미리 할당(반복적 증가/재할당 방지)
data := make([]int, 0, 1000)
// 3. 힙 이스케이프 최소화; 핫 루프에서 불필요한 포인터/할당 회피
할당이 적을수록 = GC 작업이 적습니다. sync.Pool(객체 재사용), 미리 할당, 이스케이프 최소화가 핫 패스에서 GC 압력을 줄이는 주된 방법입니다.
GC는 도달 불가능한 객체만 해제. 누수는 남아있는 참조에서 옴:
✗ goroutine 누수(블록된 goroutine이 참조 메모리를 살려둠)
✗ 커지는 전역 map/slice, 닫지 않은 자원, 잊은 구독
Go의 자동 메모리 관리는 큰 생산성·안전성 이점이지만(수동 free 없음, 댕글링 포인터 없음), 어떻게 동작하는지 이해하는 것은 성능이 중요하고 지연에 민감한 애플리케이션 — Go가 자주 선택되는 — 에 가치가 있습니다.
핵심 통찰: 이스케이프 분석이 스택 vs 힙 할당을 결정하고(스택은 공짜; 힙 이스케이프 최소화가 GC 작업을 줄임), Go의 동시적, 저지연 GC가 처리량보다 짧은 일시 정지를 우선합니다(처리량 중심 컬렉터와 달리 서버에 이상적).
GC 압력을 줄이는 방법(sync.Pool, 미리 할당, 핫 루프에서 할당 감소), 튜닝(GOGC, GOMEMLIMIT), 그리고 남아있는 참조(특히 goroutine 누수)로부터 누수가 여전히 발생한다는 점을 인식하는 것은 고성능 Go 서비스를 운영하는 데 중요합니다.
이는 시니어 Go 개발자를 구분 짓는 깊고 실무적인 지식이며 성능에 민감한 직무의 흔한 고급 면접 주제입니다.