Go의 Map: 생성, 사용 및 반복의 이해
Daniel Hayes
Full-Stack Engineer · Leapcell

컴퓨터 과학 분야에서 해시 테이블(map 또는 associative array라고도 함)은 핵심 데이터 구조입니다. 키와 값을 매핑하여 데이터를 저장하고 검색하는 매우 효율적인 방법을 제공합니다. Go는 현대적이고 실용적인 언어로서 맵에 대한 최상위 지원을 제공하여 개발자에게 필수적인 도구로 만듭니다. 이 글에서는 Go의 map
유형의 복잡성을 살펴보고, 생성, 일반적인 연산 및 효율적인 순회를 위한 다양한 기법을 탐구합니다.
Go의 맵 이해하기
Go의 map
은 키-값 쌍의 순서 없는 컬렉션을 나타내는 내장 유형입니다. 맵의 각 키는 고유해야 하며 정확히 하나의 값에 매핑됩니다. 키 유형은 비교 가능한 임의의 유형(예: 정수, 문자열, 부울, 모든 필드가 비교 가능한 구조체)이 될 수 있으며, 값 유형은 임의의 유형이 될 수 있습니다.
맵을 사용하는 이유?
맵은 다음과 같은 시나리오에 매우 유용합니다.
- 빠른 조회: 키를 기반으로 값을 검색하는 것은 매우 효율적이며, 일반적으로 평균 O(1) 시간 복잡도를 가집니다.
- 연관 저장: 숫자 인덱스가 아닌 의미 있는 키를 사용하여 데이터를 저장하고 검색합니다.
- 유연한 데이터 구조: 정확한 키-값 매핑이 필요한 데이터 관계를 나타냅니다.
맵 생성하기
Go에서 맵을 선언하고 초기화하는 방법에는 여러 가지가 있습니다.
1. var
를 사용한 선언 (nil 맵)
초기화 없이 var
를 사용하여 맵을 선언하면 nil
맵이 됩니다. nil
맵에는 용량이 없으며 어떤 키-값 쌍도 저장할 수 없습니다. nil
맵에 요소를 추가하려고 하면 런타임 패닉이 발생합니다.
package main import "fmt" func main() { var employeeSalaries map[string]float64 fmt.Println("employeeSalaries가 nil인가?", employeeSalaries == nil) // 출력: employeeSalaries가 nil인가? true fmt.Println("employeeSalaries 길이:", len(employeeSalaries)) // 출력: employeeSalaries 길이: 0 // 이것은 패닉을 유발합니다: nil 맵의 항목에 할당 // employeeSalaries["John Doe"] = 50000.0 }
nil
맵은 쓸 수는 없지만 읽을 수 있고(값 유형의 제로 값을 반환함) 길이를 확인할 수 있습니다.
2. make()
사용
make()
함수는 Go에서 초기화된 맵을 만드는 주요 방법입니다. make()
를 사용하여 맵을 만들 때 Go는 필요한 내부 데이터 구조를 할당합니다.
package main import "fmt" func main() { // 기본 생성: make(map[KeyType]ValueType) countryCapitals := make(map[string]string) fmt.Println("countryCapitals:", countryCapitals) // 출력: countryCapitals: map[] fmt.Println("countryCapitals가 nil인가?", countryCapitals == nil) // 출력: countryCapitals가 nil인가? false // 초기 용량 힌트와 함께 생성: make(map[KeyType]ValueType, capacity) // 용량 힌트는 Go가 메모리를 미리 할당하여 저장할 요소 수를 대략적으로 알고 있을 때 // 성능을 향상시킬 수 있는 리해싱을 줄이는 데 도움이 됩니다. // 이는 엄격한 한계가 아니라 힌트입니다. productPrices := make(map[string]float64, 100) fmt.Println("productPrices:", productPrices) // 출력: productPrices: map[] }
3. 맵 리터럴 사용 (초기화)
초기 키-값 쌍이 알려진 맵의 경우 맵 리터럴을 사용하면 간결하게 만들고 채울 수 있습니다.
package main import "fmt" func main() { // 맵 초기화를 위한 맵 리터럴 userStatuses := map[int]string{ 101: "Active", 102: "Inactive", 103: "Pending", } fmt.Println("userStatuses:", userStatuses) // 출력: userStatuses: map[101:Active 102:Inactive 103:Pending] // 문맥상 명확한 경우 오른쪽의 유형 선언을 생략할 수 있습니다. // var employees map[int]string = map[int]string{ ... }는 // employees := map[int]string{ ... }와 동일합니다. // 가독성을 위해 줄 바꿈이 있는 맵 리터럴 inventory := map[string]int{ "Laptop": 50, "Mouse": 200, "Keyboard": 150, "Monitor": 30, "Webcam": 75, // 마지막 쉼표는 허용되며 종종 좋은 습관입니다. } fmt.Println("inventory:", inventory) }
맵 사용: CRUD 작업
맵이 생성되면 표준 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있습니다.
1. 요소 추가/업데이트 (생성/업데이트)
할당 연산자(=
)를 사용하여 새 키-값 쌍을 추가하거나 기존 항목을 업데이트합니다. 키가 존재하지 않으면 새 항목이 생성됩니다. 이미 존재하는 경우 해당 값이 덮어씁니다.
package main import "fmt" func main() { // 맵 생성 studentGrades := make(map[string]int) // 새 항목 추가 studentGrades["Alice"] = 95 studentGrades["Bob"] = 88 studentGrades["Charlie"] = 72 fmt.Println("추가 후:", studentGrades) // 출력: 추가 후: map[Alice:95 Bob:88 Charlie:72] // 기존 항목 업데이트 studentGrades["Bob"] = 90 fmt.Println("Bob 업데이트 후:", studentGrades) // 출력: Bob 업데이트 후: map[Alice:95 Bob:90 Charlie:72] // 다른 항목 추가 studentGrades["David"] = 85 fmt.Println("David 추가 후:", studentGrades) // 출력: David 추가 후: map[Alice:95 Bob:90 Charlie:72 David:85] }
2. 요소 검색 (읽기)
대괄호([]
)에 키를 제공하여 값에 액세스합니다. Go의 맵 액세스에는 고유한 기능이 있습니다. 첫 번째는 키와 연관된 값이고, 두 번째는 키가 맵에 있었는지 여부를 나타내는 부울입니다. 이 "comma ok" 관용구는 누락된 키와 해당 유형의 제로 값이 값인 키를 구별하는 데 중요합니다.
package main import "fmt" func main() { settings := map[string]string{ "theme": "dark", "language": "en-US", "font_size": "14px", } // 기존 값 검색 theme := settings["theme"] fmt.Println("현재 테마:", theme) // 출력: 현재 테마: dark // 존재하지 않는 값 검색 - 제로 값(빈 문자열(""))을 반환합니다. userName := settings["username"] fmt.Println("사용자 이름:", userName) // 출력: 사용자 이름: // "comma ok" 관용구를 사용하여 존재 여부 확인 fontSize, ok := settings["font_size"] if ok { fmt.Println("글꼴 크기:", fontSize) // 출력: 글꼴 크기: 14px } else { fmt.Println("글꼴 크기가 설정되지 않았습니다.") } // 존재하지 않는 키 확인 debugMode, ok := settings["debug_mode"] if ok { fmt.Println("디버그 모드:", debugMode) } else { fmt.Println("디버그 모드가 설정되지 않았습니다 (기본값은 false일 가능성이 높습니다).") // 출력: 디버그 모드가 설정되지 않았습니다 (기본값은 false일 가능성이 높습니다). } }
3. 요소 삭제 (Delete)
내장 delete()
함수는 맵에서 키-값 쌍을 제거합니다. 키가 존재하지 않으면 delete()
는 아무 작업도 수행하지 않으며 오류도 보고되지 않습니다.
package main import "fmt" func main() { userSessions := map[string]string{ "user123": "sessionABC", "user456": "sessionDEF", "user789": "sessionGHI", } fmt.Println("초기 세션:", userSessions) // 기존 항목 삭제 delete(userSessions, "user456") fmt.Println("user456 삭제 후:", userSessions) // 출력: user456 삭제 후: map[user123:sessionABC user789:sessionGHI] // 존재하지 않는 항목 삭제 시도 - 오류 없음 delete(userSessions, "user999") fmt.Println("존재하지 않는 user999 삭제 후:", userSessions) // 출력: 존재하지 않는 user999 삭제 후: map[user123:sessionABC user789:sessionGHI] }
4. 맵 길이
len()
함수는 맵의 키-값 쌍 수를 반환합니다.
package main import "fmt" func main() { items := map[string]int{ "apple": 10, "banana": 5, "cherry": 20, } fmt.Println("항목 수:", len(items)) // 출력: 항목 수: 3 delete(items, "banana") fmt.Println("삭제 후 항목 수:", len(items)) // 출력: 삭제 후 항목 수: 2 }
맵 순회 (반복)
Go의 for...range
루프는 맵의 키-값 쌍을 반복하는 관용적인 방법입니다. 각 요소에 대해 키와 값을 모두 반환합니다. 중요한 것은 맵 반복 순서는 보장되지 않으며 실행마다 다를 수 있습니다. 이는 더 효율적인 맵 구현을 허용하기 위한 설계 결정입니다. 특정 순서가 필요한 경우 키를 별도로 정렬해야 합니다.
package main import ( "fmt" "sort" ) func main() { // 예제 맵 planetGravities := map[string]float64{ "Earth": 9.81, "Mars": 3.71, "Jupiter": 24.79, "Venus": 8.87, "Moon": 1.62, } fmt.Println("--- 임의 순서로 반복 (기본값) ---") for planet, gravity := range planetGravities { fmt.Printf("행성: %s, 중력: %.2f m/s^2\n", planet, gravity) } fmt.Println("\n--- 키만 반복 ---") for planet := range planetGravities { fmt.Println("행성:", planet) } fmt.Println("\n--- 값만 반복 (덜 일반적이지만 가능) ---") for _, gravity := range planetGravities { fmt.Printf("중력: %.2f m/s^2\n", gravity) } fmt.Println("\n--- 정렬된 키 순서로 반복 ---") // 1. 모든 키 추출 keys := make([]string, 0, len(planetGravities)) for planet := range planetGravities { keys = append(keys, planet) } // 2. 키 정렬 sort.Strings(keys) // 3. 정렬된 키를 통해 반복하여 맵 값에 액세스 for _, planet := range keys { fmt.Printf("행성: %s, 중력: %.2f m/s^2\n", planet, planetGravities[planet]) } }
'임의 순서로 반복' 섹션의 출력은 프로그램을 실행할 때마다 다를 가능성이 높으며, 보장되지 않는 순서를 보여줍니다.
함수 인수로 맵
맵은 슬라이스와 유사하게 참조 유형입니다. 맵을 함수에 전달하면 맵 헤더 복사본(내부 데이터 구조에 대한 포인터 포함)을 전달하는 것입니다. 이는 함수 내에서 맵에 대한 수정이 원본 맵에 영향을 미친다는 것을 의미합니다.
package main import "fmt" func updateScores(scores map[string]int) { scores["Alice"] = 100 // 원본 맵 수정 scores["Eve"] = 92 // 원본 맵에 새 항목 추가 delete(scores, "Bob") // 원본 맵에서 항목 삭제 } func main() { grades := map[string]int{ "Alice": 90, "Bob": 85, "Charlie": 78, } fmt.Println("함수 호출 전:", grades) // 출력: 함수 호출 전: map[Alice:90 Bob:85 Charlie:78] updateScores(grades) fmt.Println("함수 호출 후:", grades) // 출력: 함수 호출 후: map[Alice:100 Charlie:78 Eve:92] }
맵에 대한 중요 고려 사항
-
Nil 맵 vs. 빈 맵:
nil
맵에는 쓸 수 없습니다. 빈 맵(make(map[KeyType]ValueType)
또는{}
로 생성됨)에는 쓰고 읽을 수 있습니다. -
키 유형의 비교 가능성: 맵 키는 반드시 비교 가능해야 합니다. 즉,
==
및!=
연산자를 지원해야 합니다.- 비교 가능: 부울, 숫자 유형, 문자열, 배열, 모든 필드가 비교 가능한 구조체 유형.
- 비교 불가능: 슬라이스, 맵, 함수. 슬라이스/맵을 포함하는 구조체를 키로 사용해야 하는 경우, 비교 가능한 유형으로 변환해야 합니다(예: 슬라이스를 문자열 또는 바이트 배열로 해싱하거나, 가능한 경우 사용자 지정 비교 함수를 사용하지만 이는 Go의 내장 맵 기능을 넘어서는 것입니다).
-
동시성: Go의 내장 맵은 동시 사용에 안전하지 않습니다. 여러 고루틴이 동기화 없이 맵에 동시에 액세스하고 수정하면 데이터 경합 및 정의되지 않은 동작(대부분 패닉)이 발생합니다. 동시 맵 액세스의 경우
sync.RWMutex
를 사용하여 맵을 보호하거나,sync.Map
을 특정 사용 사례(예: 키가 대부분 한 번 작성되고 여러 번 읽히는 경우 또는 몇 개의 핫 키가 자주 업데이트되는 경우)에 사용하십시오.// sync.RWMutex를 사용한 동시성 안전 맵 예제 package main import ( "fmt" "sync" "time" ) type SafeCounter struct { mu sync.RWMutex m map[string]int } func NewSafeCounter() *SafeCounter { return &SafeCounter{m: make(map[string]int)} } func (sc *SafeCounter) Inc(key string) { sc.mu.Lock() // 쓰기 잠금 획득 sc.m[key]++ sc.mu.Unlock() // 쓰기 잠금 해제 } func (sc *SafeCounter) Value(key string) int { sc.mu.RLock() // 읽기 잠금 획득 defer sc.mu.RUnlock() // 읽기 잠금 해제 보증 return sc.m[key] } func main() { counter := NewSafeCounter() var wg sync.WaitGroup // "test"를 1000번 동시적으로 증가 for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Inc("test") }() } wg.Wait() fmt.Println("'test'에 대한 카운터 값:", counter.Value("test")) // 출력: 'test'에 대한 카운터 값: 1000 // 다른 키 읽기 fmt.Println("'another'에 대한 카운터 값:", counter.Value("another")) // 출력: 'another'에 대한 카운터 값: 0 }
-
참조 유형 동작: 맵은 참조 유형이라는 것을 기억하십시오. 한 맵 변수를 다른 맵 변수에 할당하면 둘 다 동일한 내부 데이터 구조를 가리키게 됩니다.
package main import "fmt" func main() { originalMap := map[string]int{"A": 1, "B": 2} duplicateMap := originalMap // duplicateMap은 이제 originalMap과 동일한 맵을 참조합니다. duplicateMap["C"] = 3 fmt.Println("원본 맵:", originalMap) // 출력: 원본 맵: map[A:1 B:2 C:3] fmt.Println("중복 맵:", duplicateMap) // 출력: 중복 맵: map[A:1 B:2 C:3] }
맵의 독립적인 복사본이 필요한 경우, 원본을 반복하여 각 요소를 새 맵으로 복사해야 합니다.
package main import "fmt" func main() { originalMap := map[string]int{"A": 1, "B": 2} // 독립적인 복사본 생성 copiedMap := make(map[string]int, len(originalMap)) for k, v := range originalMap { copiedMap[k] = v } copiedMap["C"] = 3 fmt.Println("원본 맵:", originalMap) // 출력: 원본 맵: map[A:1 B:2] fmt.Println("복사된 맵:", copiedMap) // 출력: 복사된 맵: map[A:1 B:2 C:3] }
결론
Go의 map
유형은 효율적이고 확장 가능한 애플리케이션 구축에 필수적인 강력하고 다재다능한 데이터 구조입니다. 생성, 조작 및 반복에 대한 직관적인 구문과 안정적인 존재 확인을 위한 "comma ok" 관용구는 작업하기에 즐겁습니다. nil 맵, 키 비교 가능성 및 동시성과 같은 기본 동작을 이해함으로써 개발자는 맵을 효과적으로 활용하여 광범위한 프로그래밍 과제를 해결할 수 있습니다. 맵을 숙달하는 것은 능숙한 Go 개발자가 되기 위한 중요한 단계입니다.