본문으로 건너뛰기

sync 패키지 — 동시성 기본 도구

채널이 "통신을 통한 공유"라면, sync 패키지는 전통적인 뮤텍스(Mutex) 기반 동기화를 제공합니다. 두 방식은 서로 보완적이며 상황에 맞게 사용합니다.

sync.Mutex — 상호 배제

여러 고루틴이 동시에 같은 변수를 수정하면 데이터 레이스(data race) 가 발생합니다.

package main

import (
"fmt"
"sync"
)

// ❌ 데이터 레이스 발생 예시
type UnsafeCounter struct {
count int
}

func (c *UnsafeCounter) Increment() {
c.count++ // 읽기-수정-쓰기가 원자적이지 않음!
}

// ✅ Mutex로 보호
type SafeCounter struct {
mu sync.Mutex
count int
}

func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // defer로 반드시 잠금 해제
c.count++
}

func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}

func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}

wg.Wait()
fmt.Println("최종 카운트:", counter.Value()) // 항상 1000
}

Mutex 사용 규칙:

  • Lock() 이후 반드시 Unlock() 호출 — defer 사용 권장
  • Lock된 Mutex를 다시 Lock하면 데드락 발생
  • Mutex를 값으로 복사하면 안 됨 — 항상 포인터로 전달

sync.RWMutex — 읽기/쓰기 분리

읽기 작업이 많고 쓰기가 적은 경우 RWMutex로 성능을 향상시킬 수 있습니다.

package main

import (
"fmt"
"sync"
"time"
)

type Cache struct {
mu sync.RWMutex
data map[string]string
}

func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}

// 읽기 시 RLock 사용 — 동시에 여러 고루틴이 읽기 가능
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}

// 쓰기 시 Lock 사용 — 단독 접근 보장
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}

func main() {
cache := NewCache()
var wg sync.WaitGroup

// 초기 데이터 설정
cache.Set("name", "Alice")
cache.Set("language", "Go")

// 동시 읽기 — RLock이므로 병렬 실행 가능
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if val, ok := cache.Get("name"); ok {
fmt.Printf("Reader %d: %s\n", id, val)
}
}(i)
}

// 쓰기 — 단독 접근
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
cache.Set("name", "Bob")
fmt.Println("이름 업데이트 완료")
}()

wg.Wait()
}
잠금 유형메서드동시 접근
쓰기 잠금Lock() / Unlock()단독 (읽기·쓰기 모두 차단)
읽기 잠금RLock() / RUnlock()공유 (다른 읽기는 허용)

sync.Once — 한 번만 실행

초기화 코드처럼 딱 한 번만 실행되어야 하는 작업에 사용합니다.

package main

import (
"fmt"
"sync"
)

type Singleton struct {
value string
}

var (
instance *Singleton
once sync.Once
)

func getInstance() *Singleton {
once.Do(func() {
fmt.Println("초기화 실행 (한 번만!)")
instance = &Singleton{value: "유일한 인스턴스"}
})
return instance
}

func main() {
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
inst := getInstance()
fmt.Printf("고루틴 %d: %s\n", id, inst.value)
}(i)
}

wg.Wait()
}
// "초기화 실행 (한 번만!)"는 정확히 한 번만 출력

sync.WaitGroup — 완료 대기 (심화)

WaitGroup의 고급 활용 패턴입니다.

package main

import (
"fmt"
"sync"
"time"
)

// 워커 풀 패턴
func workerPool(jobs <-chan int, results chan<- int, workerCount int) {
var wg sync.WaitGroup

for i := 0; i < workerCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
// 작업 처리
time.Sleep(100 * time.Millisecond)
result := job * job
fmt.Printf("Worker %d: %d → %d\n", id, job, result)
results <- result
}
}(i)
}

// 모든 워커가 끝나면 결과 채널 닫기
go func() {
wg.Wait()
close(results)
}()
}

func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)

// 워커 풀 시작 (3개 워커)
workerPool(jobs, results, 3)

// 작업 전송
for i := 1; i <= 9; i++ {
jobs <- i
}
close(jobs)

// 결과 수집
sum := 0
for r := range results {
sum += r
}
fmt.Println("합계:", sum) // 1+4+9+...+81 = 285
}

sync.Cond — 조건 변수

특정 조건이 만족될 때까지 대기하고 싶을 때 사용합니다.

package main

import (
"fmt"
"sync"
"time"
)

type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}

func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}

func (q *Queue) Push(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // 대기 중인 고루틴 하나 깨우기
}

func (q *Queue) Pop() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // Mutex 해제하고 대기, 깨어나면 다시 Lock
}
item := q.items[0]
q.items = q.items[1:]
return item
}

func main() {
q := NewQueue()

// 소비자
go func() {
for {
item := q.Pop()
fmt.Println("소비:", item)
}
}()

// 생산자
for i := 1; i <= 5; i++ {
time.Sleep(200 * time.Millisecond)
q.Push(i)
fmt.Println("생산:", i)
}

time.Sleep(500 * time.Millisecond)
}

sync.Map — 동시성 안전한 맵

기본 Go 맵은 동시 쓰기가 안전하지 않습니다. sync.Map은 동시성 안전한 맵을 제공합니다.

package main

import (
"fmt"
"sync"
)

func main() {
var sm sync.Map

var wg sync.WaitGroup

// 동시 쓰기
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key%d", id)
sm.Store(key, id*id) // 저장
}(i)
}

wg.Wait()

// 읽기
if val, ok := sm.Load("key5"); ok {
fmt.Println("key5:", val)
}

// 존재하면 반환, 없으면 저장
actual, loaded := sm.LoadOrStore("key5", 999)
fmt.Printf("key5: %v (기존 존재: %v)\n", actual, loaded)

// 전체 순회
sm.Range(func(key, value any) bool {
fmt.Printf("%s = %v\n", key, value)
return true // false 반환 시 순회 중단
})

// 삭제
sm.Delete("key3")
}

sync.Map 적합한 사용 사례:

  • 쓰기가 드물고 읽기가 많은 캐시
  • 여러 고루틴에서 서로 다른 키에 접근하는 경우

일반 map + Mutex가 더 나은 경우:

  • 맵에 많은 쓰기가 발생하는 경우
  • 구조적으로 단순한 동시 접근

핵심 정리

타입용도
sync.Mutex단순 상호 배제
sync.RWMutex읽기가 많을 때 성능 향상
sync.Once초기화 코드를 한 번만 실행
sync.WaitGroup여러 고루틴 완료 대기
sync.Cond조건 기반 대기/알림
sync.Map동시성 안전 맵
  • 뮤텍스 vs 채널: 상태 보호 → Mutex, 데이터 전달 → 채널
  • defer Unlock() 패턴 — Lock 후 반드시 defer로 Unlock
  • sync 타입은 복사 금지— 항상 포인터로 전달