채널로 안 되는 일 — sync 패키지가 필요한 순간
채널로도 되긴 하는데, 그게 최선은 아니다
앞 챕터까지 채널로 웬만한 조율은 다 할 수 있다는 인상을 줬을 것이다. 실제로 채널로
뮤텍스를 흉내 낼 수도 있다 — 버퍼 크기 1짜리 채널에 토큰을 넣고 빼는 식으로. 하지만
Go 표준 라이브러리 저자들도 그렇게 하지 않는다. 카운터 증가, 캐시 맵 보호처럼 "짧은
임계 구역을 여러 고루틴이 잠깐씩 스치고 지나가는" 상황에서는 sync.Mutex가 채널보다
빠르고 코드도 짧다. Go 공식 문서(Effective Go)에도 "항상 채널을 써야 하는 건
아니다"라는 문장이 그대로 있다. 이번 장은 채널 대신 sync 패키지를 써야 하는
세 가지 대표 상황을 다룬다.
WaitGroup — "다 끝날 때까지 기다린다"만 하고 싶을 때
2장에서 이미 WaitGroup을 몇 번 썼다. 여기서는 흔히 틀리는 지점을 짚는다.
Add는 반드시 고루틴을 띄우기 전에 호출해야 한다.
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 루프 안, go 앞
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()Add를 고루틴 내부에서 호출하면 레이스가 생긴다 — Wait()이 먼저 실행되어 카운터가
아직 0인 상태로 통과해 버릴 수 있다. 이건 -race 플래그로도 항상 잡히는 게 아니라서
(타이밍에 따라 재현이 안 될 수 있다) 규칙으로 외워두는 게 안전하다: Add는 go
키워드 앞줄에서, 루프를 도는 고루틴 개수만큼 정확히.
Mutex — 임계 구역은 짧게, defer로 반드시 풀기
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}defer c.mu.Unlock()을 Lock() 바로 다음 줄에 쓰는 습관을 들이면 좋다. 함수 중간에
return이 여러 개 있거나 패닉이 날 수 있는 코드에서, 수동으로 Unlock()을 호출하는
경로 하나를 빠뜨리면 그 뮤텍스는 영원히 잠긴 채로 남는다 — 데드락이 발생하는데 스택
트레이스만 봐서는 원인을 짐작하기 어렵다.
RWMutex는 읽기가 압도적으로 많고 쓰기가 드문 경우에 쓴다. RLock()은 여러 고루틴이
동시에 잡을 수 있고, Lock()(쓰기 락)만 배타적이다. 다만 읽기 비율이 그렇게 높지
않다면 RWMutex가 오히려 Mutex보다 느릴 수 있다 — 내부적으로 관리할 상태가 더
많기 때문이다. "읽기 전용 데이터니까 당연히 RWMutex"라고 습관적으로 고르지 말고,
실제 read/write 비율을 먼저 확인하는 게 맞다.
Once — 정확히 한 번만 실행되어야 하는 초기화
싱글턴 초기화나 설정 로딩처럼 "여러 고루틴이 동시에 접근해도 초기화는 딱 한 번만"
실행돼야 하는 경우, sync.Once가 가장 명확한 도구다.
var (
once sync.Once
instance *Config
)
func GetConfig() *Config {
once.Do(func() {
instance = loadConfigFromDisk() // 무거운 작업, 한 번만
})
return instance
}once.Do에 넘긴 함수가 실행되는 동안 다른 고루틴이 같은 Once의 Do를 호출하면,
그 함수가 끝날 때까지 블로킹된다 — 즉 "한 번만 실행"뿐 아니라 "그 실행이 끝난 뒤에야
다른 호출자들도 지나간다"는 것까지 보장한다. if instance == nil { instance = ... }
같은 수동 체크로 같은 걸 흉내 내려 하면 레이스가 생긴다. 초기화 로직이 있다면 그냥
sync.Once를 쓰는 게 맞다 — 직접 구현해서 얻을 이득이 없다.
이 셋 중에 뭘 고를지 헷갈릴 때
세 도구는 목적이 겹치지 않는다. 여러 고루틴의 완료를 기다린다 → WaitGroup.
공유 데이터를 짧게 보호한다 → Mutex(또는 RWMutex). 뭔가를 정확히 한 번만 실행한다
→ Once. 이 중 어느 것도 "값을 주고받는" 용도가 아니라는 걸 눈여겨보면 좋다 —
값 전달이 필요하면 여전히 채널이 먼저다. sync 패키지는 채널의 대체재가 아니라,
채널이 표현하기 번거로운 좁은 문제들을 위한 보완재다.