Go 언어 수신자 및 뮤텍스 주의사항

Go 언어에서 메서드 수신자(receiver)와 뮤텍스(mutex)를 사용할 때 유의해야 할 몇 가지 중요한 사항이 있습니다.

1. 메서드 수신자: 값 vs. 포인터

객체의 상태를 변경해야 하는 경우에는 포인터 수신자를 사용하고, 객체의 상태를 변경할 필요가 없는 경우에는 값 수신자를 사용하는 것이 좋습니다.

예제 코드

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

// 값 수신자: 객체의 상태를 변경하지 않음
func (p Person) SetName(newName string) {
	p.Name = newName // 이 변경은 함수 스코프 내에서만 유효합니다.
}

// 포인터 수신자: 객체의 상태를 변경함
func (p *Person) SetAge(newAge int) {
	p.Age = newAge // 원래 객체의 Age 필드가 변경됩니다.
}

func main() {
	person := Person{Name: "Alice", Age: 30}
	fmt.Println("초기 상태:", person) // 출력: 초기 상태: {Alice 30}

	person.SetName("Bob")
	fmt.Println("SetName 후:", person) // 출력: SetName 후: {Alice 30} (Name이 변경되지 않음)

	person.SetAge(31)
	fmt.Println("SetAge 후:", person) // 출력: SetAge 후: {Alice 31} (Age가 변경됨)
}

실행 결과 분석

위 코드를 실행하면 다음과 같은 결과가 나옵니다.

초기 상태: {Alice 30}
SetName 후: {Alice 30}
SetAge 후: {Alice 31}

SetName 메서드는 값 수신자(p Person)를 사용하므로, 함수 내부에서 p.Name을 변경해도 원래 person 변수의 Name 필드는 변경되지 않습니다. 이는 SetName 함수가 호출될 때 Person 객체의 복사본이 전달되기 때문입니다.

반면에 SetAge 메서드는 포인터 수신자(p *Person)를 사용합니다. 이 경우, 함수는 person 변수의 메모리 주소를 참조하게 됩니다. 따라서 p.Age = newAge와 같은 변경은 원본 person 객체의 Age 필드에 직접 적용되어 값이 변경됩니다.

2. 뮤텍스 복사 시 주의사항

뮤텍스(sync.Mutex)를 포함하는 구조체를 값으로 복사하면, 뮤텍스 자체도 복사되어 각 복사본이 별개의 잠금 상태를 가지게 됩니다. 이는 동시성 제어가 필요한 상황에서 예상치 못한 문제를 일으킬 수 있습니다.

예제 코드

package main

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

type DataStore struct {
	sync.Mutex       // 뮤텍스 필드
	Counter map[string]int
}

// 값 수신자를 사용하는 메서드 (문제 발생 가능성 있음)
func (ds DataStore) Increment(key string) {
	ds.Lock() // 복사된 뮤텍스를 잠급니다.
	defer ds.Unlock()
	ds.Counter[key]++
}

func main() {
	store := DataStore{Counter: map[string]int{"a": 0, "b": 0}}

	// 고루틴에서 Increment 메서드 호출
	runIncrement := func(key string, count int) {
		for i := 0; i < count; i++ {
			store.Increment(key) // 여기서 store가 값으로 복사됩니다.
		}
	}

	go runIncrement("a", 10000)
	go runIncrement("a", 10000)

	// 잠시 대기하여 고루틴이 완료되도록 함
	time.Sleep(2 * time.Second)

	// 예상치 못한 결과 또는 런타임 패닉이 발생할 수 있습니다.
	fmt.Println("최종 카운터:", store.Counter)
}

실행 결과 분석

위 코드를 실행하면 다음과 유사한 런타임 패닉이 발생할 수 있습니다:

fatal error: concurrent map writes

이 문제는 Increment 메서드가 값 수신자(ds DataStore)를 사용하기 때문에 발생합니다. store.Increment(key)가 호출될 때마다 DataStore 구조체의 복사본이 생성됩니다. 이 복사본에는 sync.Mutex의 복사본도 포함됩니다. 그러나 Counter 필드인 map은 참조 타입이므로, 모든 복사본이 동일한 map을 계속 참조하게 됩니다.

결과적으로 여러 고루틴이 서로 다른 DataStore 복사본에서 Lock()을 호출하더라도, 이들은 실제로는 동일한 map에 대한 동시 접근을 제어하지 못합니다. 각 고루틴은 자신의 복사본에 있는 뮤텍스를 잠그지만, 다른 고루틴은 여전히 동일한 map에 접근하여 쓰기 작업을 수행할 수 있습니다. 이로 인해 map의 동시 쓰기 충돌이 발생하여 프로그램이 패닉을 일으키거나 예상치 못한 결과를 초래합니다.

이 문제를 해결하려면 Increment 메서드에 포인터 수신자(ds *DataStore)를 사용해야 합니다. 이렇게 하면 모든 고루틴이 동일한 DataStore 인스턴스와 그 뮤텍스를 공유하게 되어 동시 접근이 안전하게 제어됩니다.

태그: go 메서드 수신자 포인터 수신자 값 수신자 동시성

7월 22일 11:39에 게시됨