정보기술 · 정보통신 ·
Go 1.24 버전, 맵 구현 방식 개선에도 메모리 최적화 기법 무용지물...스위스 테이블 도입으로 구조 변화
Go 언어 최신 버전에서 전통적인 메모리 절약 기법 효과 사라져, 메모리 정렬 규칙이 원인으로 밝혀져
[한국정보기술신문] Go 프로그래밍 언어의 최신 버전에서 오랫동안 사용되어 온 메모리 최적화 기법이 더 이상 효과를 발휘하지 못하는 것으로 나타났다. Go 1.24 버전부터 도입된 스위스 테이블 방식의 해시맵 구현으로 인해 기존의 메모리 절약 전략이 무력화된 것이다.
Go 언어에는 별도의 집합 자료구조가 없어 개발자들은 해시맵을 활용해왔다. 고유한 값을 추적할 때 일반적으로 map[int]bool 형태를 사용하지만, 숙련된 개발자들은 메모리를 절약하기 위해 map[int]struct{} 형태를 선호해왔다. 빈 구조체는 메모리에서 0바이트를 차지하는 제로 크기 타입으로, 컴파일러가 이를 인식하여 값 저장을 생략하고 키만 보관하는 방식으로 최적화를 수행했기 때문이다.
그러나 Go 1.24 버전으로 업그레이드한 한 개발자는 이론적으로 10만 개 이상의 정수를 저장할 수 있어야 하는 상황에서 map[int]bool에서 map[int]struct{}로 변경했음에도 프로덕션 환경에서 메모리 소비량에 변화가 없다는 사실을 발견했다.
스위스 테이블 구현 방식의 변화
문제의 원인은 Go 1.24부터 도입된 스위스 테이블이라는 새로운 맵 구현 방식에 있었다. 스위스 테이블은 평균적으로 모든 사용 사례에서 더 적은 메모리를 소비한다고 알려져 있지만, 특정 최적화 기법에는 예상치 못한 영향을 미쳤다.
Go의 맵 구현 소스코드를 분석한 결과, 각 슬롯은 키와 값의 쌍으로 구성된 구조체 형태를 띠고 있었다. 키는 8바이트를 필요로 하고 빈 구조체는 0바이트지만, 여기에 함정이 있었다. 구조체가 사용하는 메모리 양은 단순히 필드들의 합과 같지 않다는 점이다.
메모리 정렬 규칙의 영향
구조체는 CPU의 적절한 메모리 정렬을 보장하기 위해 패딩이 필요하다. 구조체의 마지막 필드가 0바이트인 경우, Go 컴파일러는 메모리 접근 위반 없이 포인터 연산으로 해당 필드를 실제로 참조할 수 있도록 크기를 1바이트로 만든다.
첫 번째 항목이 8바이트이고 구조체가 1바이트이므로, 빈 구조체도 정렬이 필요하다. 구조체가 8의 배수인 적절한 정렬을 갖도록 하기 위해 사용되지 않는 7바이트가 끝에 추가된다.
불린 타입 역시 Go에서 1바이트를 차지하며 위와 동일한 정렬 규칙을 따른다. 결국 map[int]bool과 map[int]struct{} 모두 동일한 메모리를 소비하게 되는 것이다.
과거 구현 방식과의 차이
Go 1.24 이전 버전에서는 맵이 다르게 구현되었다. 키와 값이 별도의 고정 크기 배열로 저장되었으며, 키를 위한 배열 다음에 값을 위한 배열이 배치되는 방식이었다. 빈 구조체를 사용할 경우 컴파일러가 값 배열을 완전히 생략했기 때문에 메모리 절약 효과가 있었다.
자체 호스팅 컴파일러의 이점
이번 발견은 자체 호스팅 컴파일러의 장점을 보여주는 사례이기도 하다. Go 컴파일러 자체가 Go로 작성되어 있어, 개발자들이 특정 부분의 실제 구현을 빠르게 확인할 수 있었다. 상대적으로 단순한 언어인 Go의 소스코드는 C로 작성된 Python의 딕셔너리 구현보다 이해하기 훨씬 쉬운 것으로 평가받는다.
전문가들은 많은 개발자가 여전히 이 기법에 의존하고 있지만, 새로운 버전의 Go에서는 작동하지 않는다고 지적했다. 또한 빈 구조체 사용이 코드 가독성을 해친다는 점도 단점으로 꼽혔다. 이번 사례는 대형 언어 모델이 제공하는 정보를 무조건 신뢰해서는 안 된다는 교훈도 남겼다.
한국정보기술신문 정보기술분과 유상헌 기자 news@kitpa.org