관리 메뉴

피터의 개발이야기

[GO] Go Race Detector: 동시성 버그를 잡아내는 강력한 도구 본문

Programming/GO

[GO] Go Race Detector: 동시성 버그를 잡아내는 강력한 도구

기록하는 백앤드개발자 2025. 2. 16. 23:38
반응형

ㅁ 들어가며

  이전에 [GO] Go 언어에서의 "fatal error: concurrent map read and map write" 해결하기에서 동시성으로 인한 오류를 해결하는 과정에서 이를 예방할 수 있는 Race Detector를 알게 되었다. Go 언어는 동시성 프로그래밍을 위한 강력한 기능을 제공하지만, 이와 함께 데이터 레이스(data race)와 같은 동시성 버그의 위험도 존재한다. 이러한 문제를 해결하기 위해 Go는 내장 Race Detector를 제공한다. 

 

ㅁ Race Detector란?

  Go의 Race Detector는 프로그램 실행 중 발생할 수 있는 데이터 레이스 조건을 감지하는 도구이다. Go 프로그램을 실행시키면 deadlock이나 data race condition이 발생할 가능성이 있는 코드를 찾아서 레포팅 해 준다. data race는 여러 고루틴이 동시에 같은 메모리 위치에 접근할 때 발생하며, 이는 예측할 수 없는 동작과 버그의 원인이 된다. 

 

ㅁ 사용 방법

  Race Detector를 사용하는 방법은 매우 간단합니다. Go 명령어에 -race 플래그를 추가하기만 하면 된다.

go test -race mypkg    # 패키지 테스트
go run -race mysrc.go  # 소스 파일 실행
go build -race mycmd   # 명령어 빌드
go install -race mypkg # 패키지 설치

 

ㅁ 작동 원리

  Race Detector가 활성화되면, 컴파일러는 모든 메모리 접근에 대한 코드를 삽입한다. 런타임 라이브러리는 이 코드를 통해 공유 변수에 대한 동기화되지 않은 접근을 감시한다. 레이스 조건이 감지되면 경고가 출력된다.

 

ㅁ 장점

정확성: Race Detector는 거짓 양성(false positive)을 발생시키지 않는다. 따라서 보고된 모든 문제는 실제 버그이다.

상세한 보고: 문제가 발견되면 전체 스택 트레이스를 포함한 상세한 보고서를 제공한다.

개발 주기 전반에 걸친 사용: 단위 테스트뿐만 아니라 개발 및 테스트 환경에서도 사용할 수 있다.

 

ㅁ 주의사항

성능 오버헤드: Race Detector를 사용하면 실행 시간과 메모리 사용량이 크게 증가할 수 있다.

모든 레이스 감지 불가: 실행 중 발생한 레이스만 감지할 수 있으므로, 모든 가능한 레이스를 찾아내지는 못한다.

 


샘플 코드를 작성하여 Race Detector를 사용해 보았다.

 

ㅁ 샘플코드

package main

import "fmt"

func main() {
	done := make(chan bool)
	m := make(map[string]string)
	m["name"] = "world"
	go func() {
		m["name"] = "data race"
		done <- true
	}()
	fmt.Println("Hello,", m["name"])
	<-done
}

ㅇ race.go 코드 작성

 

 

$ go run -race race.go

Hello, world
==================
WARNING: DATA RACE
Write at 0x00c0000a20c0 by goroutine 6:
  runtime.mapaccess2_faststr()
      /Users/seodong-eok/sdk/go1.22.8/src/runtime/map_faststr.go:108 +0x42c
  main.main.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:10 +0x48

Previous read at 0x00c0000a20c0 by main goroutine:
  runtime.evacuate_fast64()
      /Users/seodong-eok/sdk/go1.22.8/src/runtime/map_fast64.go:376 +0x3dc
  main.main()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:13 +0x150

Goroutine 6 (running) created at:
  main.main()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:9 +0x134
==================
==================
WARNING: DATA RACE
Write at 0x00c0000b8088 by goroutine 6:
  main.main.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:10 +0x54

Previous read at 0x00c0000b8088 by main goroutine:
  main.main()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:13 +0x158

Goroutine 6 (running) created at:
  main.main()
      /Users/seodong-eok/ai/transcoder/media-live/temp/test/race.go:9 +0x134
==================
Found 2 data race(s)
exit status 66

 

 

ㅁ Data Race  유발 테스트 코드 작성 및 방어로직 테스트

ㅇ race_detector와 no_race 코드를 나누어 작성해 보았다.

package race_detector

import "sync"

type Counter struct {
	value int
}

func (c *Counter) Increment() {
	c.value++
}

func (c *Counter) GetValue() int {
	return c.value
}

func (c *Counter) IncrementConcurrently(n int) {
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Increment()
		}()
	}
	wg.Wait()
}

ㅇ counter.go를 작성하였다.

 

package race_detector

import (
	"sync"
	"testing"
)

func TestCounter(t *testing.T) {
	c := &Counter{}

	// 동시성 없는 테스트
	t.Run("Sequential Increment", func(t *testing.T) {
		c.Increment()
		if c.GetValue() != 1 {
			t.Errorf("Expected 1, got %d", c.GetValue())
		}
	})

	// 동시성 테스트 (데이터 레이스 발생 가능)
	t.Run("Concurrent Increment", func(t *testing.T) {
		c.IncrementConcurrently(1000)
		if c.GetValue() != 1001 {
			t.Errorf("Expected 1001, got %d", c.GetValue())
		}
	})
}

func TestCounterRace(t *testing.T) {
	c := &Counter{}

	// 고의적으로 데이터 레이스 유발
	go func() {
		c.Increment()
	}()
	c.GetValue()
}

ㅇ counter_test.go를 작성하였다.

 

$ go test -race
==================
WARNING: DATA RACE
Read at 0x00c00000e268 by goroutine 10:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).Increment()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:10 +0x7c
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:23 +0x78

Previous write at 0x00c00000e268 by goroutine 9:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).Increment()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:10 +0x8c
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:23 +0x78

Goroutine 10 (running) created at:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:21 +0x58
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounter.func2()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:20 +0x34
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40

Goroutine 9 (finished) created at:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:21 +0x58
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounter.func2()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:20 +0x34
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40
==================
==================
WARNING: DATA RACE
Write at 0x00c00000e268 by goroutine 11:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).Increment()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:10 +0x8c
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:23 +0x78

Previous write at 0x00c00000e268 by goroutine 15:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).Increment()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:10 +0x8c
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:23 +0x78

Goroutine 11 (running) created at:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:21 +0x58
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounter.func2()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:20 +0x34
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40

Goroutine 15 (running) created at:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).IncrementConcurrently()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:21 +0x58
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounter.func2()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:20 +0x34
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40
==================
--- FAIL: TestCounter (0.01s)
    --- FAIL: TestCounter/Concurrent_Increment (0.01s)
        counter_test.go:22: Expected 1001, got 903
        testing.go:1398: race detected during execution of test
==================
WARNING: DATA RACE
Write at 0x00c00039a0d8 by goroutine 1010:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).Increment()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:10 +0x44
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounterRace.func1()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:32 +0x30

Previous read at 0x00c00039a0d8 by goroutine 1009:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.(*Counter).GetValue()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter.go:14 +0xa8
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounterRace()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:34 +0xa0
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40

Goroutine 1010 (running) created at:
  github.kakaocorp.com/ai-platform/media-live/temp/race_detector.TestCounterRace()
      /Users/seodong-eok/ai/transcoder/media-live/temp/race_detector/counter_test.go:31 +0x9c
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.(*T).Run.gowrap1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x40

Goroutine 1009 (running) created at:
  testing.(*T).Run()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1742 +0x5e4
  testing.runTests.func1()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:2161 +0x80
  testing.tRunner()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:1689 +0x180
  testing.runTests()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:2159 +0x6e0
  testing.(*M).Run()
      /Users/seodong-eok/sdk/go1.22.8/src/testing/testing.go:2027 +0xb74
  main.main()
      _testmain.go:49 +0x294
==================
--- FAIL: TestCounterRace (0.00s)
    testing.go:1398: race detected during execution of test
FAIL
exit status 1
FAIL    github.kakaocorp.com/ai-platform/media-live/temp/race_detector  0.389s

 

package no_race

import "sync"

type Counter struct {
	mu    sync.Mutex
	value int
}

func (c *Counter) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

func (c *Counter) GetValue() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.value
}

func (c *Counter) IncrementConcurrently(n int) {
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Increment()
		}()
	}
	wg.Wait()
}

ㅇ Mutext를 통해 data race를 방지하였다.

 

package no_race

import (
	"sync"
	"testing"
)

func TestCounter(t *testing.T) {
	c := &Counter{}

	// 동시성 없는 테스트
	t.Run("Sequential Increment", func(t *testing.T) {
		c.Increment()
		if c.GetValue() != 1 {
			t.Errorf("Expected 1, got %d", c.GetValue())
		}
	})

	// 동시성 테스트 (데이터 레이스 발생 가능)
	t.Run("Concurrent Increment", func(t *testing.T) {
		c.IncrementConcurrently(1000)
		if c.GetValue() != 1001 {
			t.Errorf("Expected 1001, got %d", c.GetValue())
		}
	})
}

// WaitGroup을 지정하여 TestCounterRace의 data race를 방지하였다.
func TestCounterNoRace(t *testing.T) {
	c := &Counter{}
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		defer wg.Done()
		c.Increment()
	}()
	go func() {
		defer wg.Done()
		_ = c.GetValue()
	}()
	wg.Wait()
}

 

$ go test -race
PASS
ok      github.kakaocorp.com/ai-platform/media-live/temp/no_race        1.369s

ㅇ 수정된 소스의 경우 data race가 발생하지 않았다.

 

ㅁ 마무리

  Go의 Race Detector는 동시성 프로그래밍의 복잡성을 다루는 데 큰 도움이 되는 도구이다. 개발 과정에서 정기적으로 사용하면 잠재적인 버그를 조기에 발견하고 수정할 수 있어, 더 안정적인 동시성 프로그램을 작성하는 데 도움이 된다.

  Race Detector를 통해 동시성 관련 버그를 미리 방지하고, 더 안정적인 소프트웨어를 만들 수 있을 것이다.

 

ㅁ 함께 보면 좋은 사이트

 Go Race Detector 소개

 [Go] 데이터 경합을 감지해보자, Data Race Detector

반응형
Comments