빠른 흐름
go test -race ./...
go test -race ./internal/service
go run -race ./cmd/apiGo data race 점검은 공유 상태가 있는 동시성 코드에서 -race를 켜고 실제 실행 경로를 태우는 것부터 시작합니다.
기본 흐름
data race는 동시에 접근한 공유 메모리 문제다
var count int
go func() {
count++
}()
go func() {
count++
}()둘 이상의 goroutine이 같은 변수에 동시에 접근하고, 그중 하나 이상이 write라면 data race가 될 수 있습니다. 결과가 가끔 맞아 보여도 안전한 코드는 아닙니다.
race detector는 실행된 경로에서 찾는다
go test -race ./...-race는 테스트나 실행 중 실제로 지나간 경로에서 data race를 찾습니다. 테스트가 해당 동시성 경로를 실행하지 않으면 race도 드러나지 않을 수 있습니다.
리포트는 접근 위치와 goroutine 생성 위치를 같이 본다
race detector가 출력하는 리포트에는 충돌한 read/write 위치와 goroutine이 어디서 만들어졌는지가 나옵니다. 수정할 때는 에러가 난 줄 하나보다 공유 상태의 소유권을 먼저 봐야 합니다.
보호 방법
공유 상태는 mutex로 보호한다
var mu sync.Mutex
count := 0
mu.Lock()
count++
mu.Unlock()여러 goroutine이 같은 map이나 struct를 수정하면 sync.Mutex로 보호하는 흐름이 가장 직접적입니다.
작은 counter는 atomic도 가능하다
var count atomic.Int64
count.Add(1)단순 counter, flag처럼 매우 작은 상태는 sync/atomic이 맞을 수 있습니다. 여러 필드를 함께 일관되게 바꿔야 하면 mutex가 더 읽기 쉽습니다.
소유권 이동은 channel이 맞을 수 있다
updates := make(chan Update)상태를 여러 goroutine이 직접 만지는 대신, 한 goroutine이 소유하고 channel로 요청만 받게 만들 수도 있습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 전체 테스트 race 점검 | go test -race ./... |
| 서버 실행 경로 점검 | go run -race ./cmd/app |
| 공유 map/struct 보호 | sync.Mutex |
| 단순 counter/flag | sync/atomic 검토 |
| 상태 소유권을 한 곳으로 모음 | channel |
주의할 점
race detector는 강력하지만 정적 증명 도구는 아닙니다. 실행되지 않은 경로의 race는 찾지 못합니다. 또한 -race는 실행 비용이 커지므로 보통 로컬 검증이나 CI의 별도 job에서 사용합니다. race가 보이면 sleep을 넣어 타이밍을 피하기보다 공유 상태의 소유권과 보호 방식을 고쳐야 합니다.
참고 링크
3 sources