Go 원자적 연산, 락(Lock) 없이 성능을 잡는 방법
sync.Mutex 대신 원자적 연산(Atomic Operations)을 사용하면 고루틴(Goroutine) 스케줄러를 우회하여 성능을 향상시킬 수 있음
Go 1.19부터 제공되는 타입 기반 API를 통해 안전하고 간결한 코딩 가능
atomic.Value 사용 시, 최초 저장된 타입과 일치해야 하는 제약 조건에 주의해야 함
CAS(Compare-and-Swap) 루프를 활용한 락 프리(Lock-free) 프로그래밍과 실용적인 예시 제시
원자적 연산(Atomic Operations)의 기본 원리
원자적 연산은 CPU 레벨의 명령어를 사용하여 락(Lock) 없이 메모리 접근을 제어한다. 특히, `counter++`와 같은 연산은 읽기, 증가, 쓰기의 세 단계로 이루어지는데, 여러 고루틴이 동시에 접근할 경우 데이터 레이스(Data Race)가 발생할 수 있다. 원자적 연산은 메모리 버스(Memory Bus)를 짧은 시간 동안 독점하여 이러한 문제를 해결하며, 스케줄러의 개입 없이 하드웨어 수준에서 연산을 수행한다.
타입 기반 API와 안전성
Go 1.19 이전에는 `sync/atomic` 패키지가 타입 안전성을 제공하지 않아, 32비트 시스템에서 64비트 정수를 사용할 때 런타임 에러가 발생할 수 있었다. 하지만, 타입 기반 API를 통해 이러한 문제를 해결하고, 코드의 가독성을 높였다. `atomic.Int64`, `atomic.Pointer[T]`와 같은 타입 래퍼(Type Wrapper)를 사용하면, 실수로 잘못된 포인터를 전달하는 실수를 방지할 수 있으며, 코드의 유지보수성(Maintainability)을 향상시킨다.
atomic.Value의 함정: 타입 일관성
atomic.Value는 임의의 타입을 저장할 수 있지만, 최초 저장된 타입과 일관성을 유지해야 한다. 최초에 `map[string]int`를 저장한 후, `struct{ Name string }`을 저장하려고 하면 런타임 에러가 발생한다. 이는 `atomic.Value`가 내부적으로 타입을 고정(Type Locking)하기 때문이다. 따라서, 타입이 명확한 경우에는 `atomic.Pointer[T]`를 사용하는 것이 더 안전하고 효율적이다.
CAS(Compare-and-Swap) 루프와 실용적인 예시
CAS(Compare-and-Swap)는 락 없이 값을 업데이트하는 방법으로, 다른 고루틴이 먼저 값을 변경했을 경우 재시도하는 방식으로 동작한다. 본문에서는 RateLimiter 구현 예시를 통해 CAS 루프의 실용성을 보여준다. 이 방식은 경합(Contention)이 발생할 때만 재시도하므로, 과도한 스핀(Spin)을 방지하고 성능을 유지할 수 있다. 특히, Config 핫 리로드(Hot Reload)와 같은 상황에서 유용하게 활용될 수 있다.
Mutex vs. Atomics: 선택의 기준
원자적 연산은 단일 값을 보호할 때 유용하며, 여러 변수를 함께 업데이트하거나 복잡한 조건 로직이 필요한 경우에는 sync.Mutex를 사용하는 것이 더 적합하다. 예를 들어, 큐 상태를 추적하기 위해 여러 `atomic.Int64`를 사용하려다 실패한 사례가 있다. 원자적 연산은 단일 값에 대한 원자성(Atomicity)을 보장하지만, 여러 값 간의 일관성을 유지하는 데는 한계가 있다. 따라서, 트레이드오프를 고려하여 적절한 도구를 선택해야 한다.