13 minute read

개요

맵(map)은 키-값 쌍을 저장하는 해시 테이블 기반 자료구조입니다.

주요 특징:

  • 키-값 쌍: 고유한 키로 값에 접근
  • 참조 타입: 함수 인자로 전달 시 참조 전달
  • 제로값은 nil: nil 맵에 쓰기 시도는 패닉 발생
  • 순서 없음: 순회 순서가 무작위 (의도적 설계)
  • 동적 크기: 자동으로 확장
  • make() 또는 맵 리터럴로 생성
  • Comparable 키: 키 타입은 == 비교 가능해야 함

맵의 내부 구조

// 맵의 개념적 구조
// map[KeyType]ValueType
// 내부적으로 해시 테이블로 구현
// 키의 해시값으로 버킷 위치 결정
해시 테이블 구조:
┌─────────────────────────────────┐
│  Key Hash → Bucket Index        │
├─────────────────────────────────┤
│ Bucket 0: [key1:val1, key2:val2]│
│ Bucket 1: [key3:val3]           │
│ Bucket 2: []                    │
│ Bucket 3: [key4:val4, key5:val5]│
└─────────────────────────────────┘

맵 생성 방법

1. nil 맵

package main

import "fmt"

func nilMap() {
    var m map[string]int
    
    fmt.Println(m)         // map[]
    fmt.Println(m == nil)  // true
    fmt.Println(len(m))    // 0
    
    // 읽기는 가능 (제로값 반환)
    value := m["key"]
    fmt.Println(value)     // 0
    
    // ❌ nil 맵에 쓰기는 패닉!
    // m["key"] = 1        // panic: assignment to entry in nil map
}

2. make로 생성

func makeMap() {
    // 기본 생성
    m1 := make(map[string]int)
    fmt.Println(m1 == nil) // false
    
    // 초기 용량 힌트 (성능 최적화)
    m2 := make(map[string]int, 100)
    
    // 쓰기 가능
    m1["apple"] = 1
    m2["banana"] = 2
    
    fmt.Println(m1) // map[apple:1]
    fmt.Println(m2) // map[banana:2]
}

3. 맵 리터럴

func mapLiteral() {
    // 빈 맵
    m1 := map[string]int{}
    fmt.Println(m1 == nil) // false
    
    // 초기값과 함께 생성
    m2 := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 복잡한 타입
    m3 := map[int][]string{
        1: {"a", "b"},
        2: {"c", "d"},
    }
    
    // 구조체를 값으로
    type Person struct {
        Name string
        Age  int
    }
    m4 := map[string]Person{
        "alice": {"Alice", 25},
        "bob":   {"Bob", 30},
    }
    
    fmt.Println(m2, m3, m4)
}

맵 접근 및 수정

1. 기본 접근

func mapAccess() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
    }
    
    // 값 읽기
    fmt.Println(m["apple"])   // 1
    
    // 존재하지 않는 키 (제로값 반환)
    fmt.Println(m["cherry"])  // 0
    
    // 값 쓰기
    m["cherry"] = 3
    fmt.Println(m["cherry"])  // 3
    
    // 값 수정
    m["apple"] = 10
    fmt.Println(m["apple"])   // 10
}

2. Comma Ok Idiom (존재 여부 확인)

func commaOk() {
    m := map[string]int{
        "apple":  1,
        "banana": 0,  // 제로값 저장
    }
    
    // 단순 접근 (존재 여부 불확실)
    value := m["banana"]
    fmt.Println(value) // 0 (존재하지만 제로값)
    
    value = m["cherry"]
    fmt.Println(value) // 0 (존재하지 않음)
    
    // Comma Ok Idiom (존재 여부 확인)
    value, ok := m["banana"]
    fmt.Printf("banana: value=%d, exists=%v\n", value, ok)
    // banana: value=0, exists=true
    
    value, ok = m["cherry"]
    fmt.Printf("cherry: value=%d, exists=%v\n", value, ok)
    // cherry: value=0, exists=false
    
    // 일반적인 패턴
    if value, ok := m["apple"]; ok {
        fmt.Println("Found:", value)
    } else {
        fmt.Println("Not found")
    }
}

3. 맵 삭제

func deleteFromMap() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    fmt.Println("Before:", m) // map[apple:1 banana:2 cherry:3]
    
    // 키 삭제
    delete(m, "banana")
    fmt.Println("After:", m)  // map[apple:1 cherry:3]
    
    // 존재하지 않는 키 삭제 (안전, 아무 일도 안 일어남)
    delete(m, "orange")
    
    // 모든 요소 삭제
    for key := range m {
        delete(m, key)
    }
    fmt.Println("Cleared:", m) // map[]
    
    // 또는 새 맵 할당
    m = make(map[string]int)
}

맵 순회

func iterateMap() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 키와 값 모두 순회
    for key, value := range m {
        fmt.Printf("%s: %d\n", key, value)
    }
    
    // 키만 순회
    for key := range m {
        fmt.Println(key)
    }
    
    // 값만 순회 (비권장, 키 없이는 의미 없음)
    for _, value := range m {
        fmt.Println(value)
    }
    
    // ⚠️ 순회 순서는 무작위!
    // 같은 맵을 여러 번 순회해도 순서가 다를 수 있음
}

정렬된 순서로 순회

import "sort"

func sortedIterate() {
    m := map[string]int{
        "charlie": 3,
        "alice":   1,
        "bob":     2,
    }
    
    // 키를 슬라이스로 추출
    keys := make([]string, 0, len(m))
    for key := range m {
        keys = append(keys, key)
    }
    
    // 키 정렬
    sort.Strings(keys)
    
    // 정렬된 순서로 접근
    for _, key := range keys {
        fmt.Printf("%s: %d\n", key, m[key])
    }
    // alice: 1
    // bob: 2
    // charlie: 3
}

맵은 참조 타입

func mapReference() {
    m1 := map[string]int{"apple": 1}
    
    // 맵 할당은 참조 복사
    m2 := m1
    m2["banana"] = 2
    
    fmt.Println(m1) // map[apple:1 banana:2] (변경됨!)
    fmt.Println(m2) // map[apple:1 banana:2]
    
    // 함수 인자로 전달
    modifyMap(m1)
    fmt.Println(m1) // map[apple:1 banana:2 cherry:3]
}

func modifyMap(m map[string]int) {
    m["cherry"] = 3 // 원본 맵 수정됨
}

맵 복사

func copyMap() {
    original := map[string]int{
        "apple":  1,
        "banana": 2,
    }
    
    // 얕은 복사 (수동)
    copied := make(map[string]int)
    for key, value := range original {
        copied[key] = value
    }
    
    copied["cherry"] = 3
    fmt.Println("Original:", original) // map[apple:1 banana:2]
    fmt.Println("Copied:", copied)     // map[apple:1 banana:2 cherry:3]
}

// 제네릭 복사 함수 (Go 1.18+)
func CopyMap[K comparable, V any](m map[K]V) map[K]V {
    result := make(map[K]V, len(m))
    for k, v := range m {
        result[k] = v
    }
    return result
}

키 타입 제약

func keyTypes() {
    // ✅ Comparable 타입은 키로 사용 가능
    m1 := map[int]string{}
    m2 := map[string]int{}
    m3 := map[bool]int{}
    m4 := map[float64]string{} // 부동소수점은 가능하지만 권장 안 함
    
    type Point struct {
        X, Y int
    }
    m5 := map[Point]string{} // 구조체 (모든 필드가 comparable)
    
    // ❌ 슬라이스는 키로 사용 불가
    // m6 := map[[]int]string{} // 컴파일 에러
    
    // ❌ 맵은 키로 사용 불가
    // m7 := map[map[string]int]string{} // 컴파일 에러
    
    // ❌ 함수는 키로 사용 불가
    // m8 := map[func()]string{} // 컴파일 에러
    
    // ✅ 포인터는 키로 사용 가능
    m9 := map[*Point]string{}
    
    // ✅ 인터페이스는 키로 사용 가능 (런타임 값이 comparable이어야 함)
    m10 := map[interface{}]string{}
    
    _, _, _, _, _, _, _, _ = m1, m2, m3, m4, m5, m9, m10
}

동시성과 sync.Map

import (
    "sync"
)

// ❌ 일반 맵은 동시성 안전하지 않음
func unsafeMap() {
    m := make(map[int]int)
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m[n] = n * 2 // 경쟁 상태 발생 가능
        }(i)
    }
    
    wg.Wait()
    // 패닉 또는 데이터 손실 가능
}

// ✅ Mutex로 보호
func safeMapWithMutex() {
    m := make(map[int]int)
    var mu sync.Mutex
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            mu.Lock()
            m[n] = n * 2
            mu.Unlock()
        }(i)
    }
    
    wg.Wait()
}

// ✅ sync.Map 사용 (특정 상황에서 유용)
func syncMap() {
    var m sync.Map
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(n, n*2)
        }(i)
    }
    
    wg.Wait()
    
    // 값 읽기
    if value, ok := m.Load(42); ok {
        fmt.Println("Found:", value)
    }
    
    // 순회
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true // false 반환 시 순회 중단
    })
}

실전 활용 패턴

1. 카운터 (빈도수 계산)

func wordCount(text string) map[string]int {
    words := strings.Fields(text)
    counts := make(map[string]int)
    
    for _, word := range words {
        counts[word]++
    }
    
    return counts
}

func main() {
    text := "go go go python java go python"
    counts := wordCount(text)
    fmt.Println(counts) // map[go:4 java:1 python:2]
}

2. 집합 (Set) 구현

type Set map[string]bool

func NewSet() Set {
    return make(Set)
}

func (s Set) Add(item string) {
    s[item] = true
}

func (s Set) Remove(item string) {
    delete(s, item)
}

func (s Set) Contains(item string) bool {
    return s[item]
}

func (s Set) Size() int {
    return len(s)
}

func (s Set) Items() []string {
    items := make([]string, 0, len(s))
    for item := range s {
        items = append(items, item)
    }
    return items
}

func main() {
    s := NewSet()
    s.Add("apple")
    s.Add("banana")
    s.Add("apple") // 중복 추가 (무시됨)
    
    fmt.Println(s.Contains("apple"))  // true
    fmt.Println(s.Size())             // 2
    fmt.Println(s.Items())            // [apple banana] (순서 무작위)
}

3. 그룹화

type Student struct {
    Name  string
    Grade int
}

func groupByGrade(students []Student) map[int][]Student {
    groups := make(map[int][]Student)
    
    for _, student := range students {
        groups[student.Grade] = append(groups[student.Grade], student)
    }
    
    return groups
}

func main() {
    students := []Student{
        {"Alice", 1},
        {"Bob", 2},
        {"Charlie", 1},
        {"David", 2},
        {"Eve", 3},
    }
    
    groups := groupByGrade(students)
    for grade, students := range groups {
        fmt.Printf("Grade %d: %v\n", grade, students)
    }
}

4. 캐시 구현

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

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

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    value, ok := c.data[key]
    return value, ok
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[key] = value
}

func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    delete(c.data, key)
}

func (c *Cache) Clear() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data = make(map[string]interface{})
}

5. 인덱스 구현

type Book struct {
    ISBN   string
    Title  string
    Author string
}

type Library struct {
    booksByISBN   map[string]*Book
    booksByAuthor map[string][]*Book
}

func NewLibrary() *Library {
    return &Library{
        booksByISBN:   make(map[string]*Book),
        booksByAuthor: make(map[string][]*Book),
    }
}

func (l *Library) AddBook(book *Book) {
    l.booksByISBN[book.ISBN] = book
    l.booksByAuthor[book.Author] = append(l.booksByAuthor[book.Author], book)
}

func (l *Library) FindByISBN(isbn string) *Book {
    return l.booksByISBN[isbn]
}

func (l *Library) FindByAuthor(author string) []*Book {
    return l.booksByAuthor[author]
}

6. 메모이제이션

func fibonacci() func(int) int {
    cache := make(map[int]int)
    
    var fib func(int) int
    fib = func(n int) int {
        if n <= 1 {
            return n
        }
        
        if val, ok := cache[n]; ok {
            return val
        }
        
        result := fib(n-1) + fib(n-2)
        cache[n] = result
        return result
    }
    
    return fib
}

func main() {
    fib := fibonacci()
    fmt.Println(fib(10)) // 55
    fmt.Println(fib(40)) // 102334155 (캐시로 빠름)
}

7. 그래프 표현

type Graph map[string][]string

func NewGraph() Graph {
    return make(Graph)
}

func (g Graph) AddEdge(from, to string) {
    g[from] = append(g[from], to)
}

func (g Graph) Neighbors(node string) []string {
    return g[node]
}

func main() {
    graph := NewGraph()
    graph.AddEdge("A", "B")
    graph.AddEdge("A", "C")
    graph.AddEdge("B", "D")
    graph.AddEdge("C", "D")
    
    fmt.Println("Neighbors of A:", graph.Neighbors("A"))
    // [B C]
}

8. 설정 관리

type Config map[string]interface{}

func (c Config) GetString(key string, defaultValue string) string {
    if val, ok := c[key]; ok {
        if str, ok := val.(string); ok {
            return str
        }
    }
    return defaultValue
}

func (c Config) GetInt(key string, defaultValue int) int {
    if val, ok := c[key]; ok {
        if num, ok := val.(int); ok {
            return num
        }
    }
    return defaultValue
}

func (c Config) GetBool(key string, defaultValue bool) bool {
    if val, ok := c[key]; ok {
        if b, ok := val.(bool); ok {
            return b
        }
    }
    return defaultValue
}

func main() {
    config := Config{
        "host":    "localhost",
        "port":    8080,
        "debug":   true,
        "timeout": 30,
    }
    
    host := config.GetString("host", "0.0.0.0")
    port := config.GetInt("port", 3000)
    debug := config.GetBool("debug", false)
    
    fmt.Printf("Server: %s:%d (debug=%v)\n", host, port, debug)
}

제네릭 맵 함수 (Go 1.18+)

// 맵 키 추출
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

// 맵 값 추출
func Values[K comparable, V any](m map[K]V) []V {
    values := make([]V, 0, len(m))
    for _, v := range m {
        values = append(values, v)
    }
    return values
}

// 맵 필터
func Filter[K comparable, V any](m map[K]V, fn func(K, V) bool) map[K]V {
    result := make(map[K]V)
    for k, v := range m {
        if fn(k, v) {
            result[k] = v
        }
    }
    return result
}

// 맵 변환
func MapValues[K comparable, V, U any](m map[K]V, fn func(V) U) map[K]U {
    result := make(map[K]U, len(m))
    for k, v := range m {
        result[k] = fn(v)
    }
    return result
}

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 키 추출
    keys := Keys(m)
    fmt.Println(keys) // [apple banana cherry] (순서 무작위)
    
    // 값 추출
    values := Values(m)
    fmt.Println(values) // [1 2 3] (순서 무작위)
    
    // 필터: 값이 2 이상인 항목만
    filtered := Filter(m, func(k string, v int) bool {
        return v >= 2
    })
    fmt.Println(filtered) // map[banana:2 cherry:3]
    
    // 변환: 모든 값을 2배로
    doubled := MapValues(m, func(v int) int {
        return v * 2
    })
    fmt.Println(doubled) // map[apple:2 banana:4 cherry:6]
}

성능 고려사항

1. 초기 용량 설정

import "testing"

func BenchmarkMapWithoutCapacity(b *testing.B) {
    for i := 0; i < b.N; i++ {
        m := make(map[int]int)
        for j := 0; j < 1000; j++ {
            m[j] = j
        }
    }
}

func BenchmarkMapWithCapacity(b *testing.B) {
    for i := 0; i < b.N; i++ {
        m := make(map[int]int, 1000)
        for j := 0; j < 1000; j++ {
            m[j] = j
        }
    }
}

// 결과: 용량 힌트를 주면 약 20-30% 빠름

2. 맵 vs 슬라이스 (작은 데이터)

// 요소가 적으면 슬라이스가 더 빠를 수 있음
func findInMap(m map[string]int, key string) (int, bool) {
    val, ok := m[key]
    return val, ok
}

type KeyValue struct {
    Key   string
    Value int
}

func findInSlice(slice []KeyValue, key string) (int, bool) {
    for _, kv := range slice {
        if kv.Key == key {
            return kv.Value, true
        }
    }
    return 0, false
}

// 요소 < 10개: 슬라이스가 빠름
// 요소 > 100개: 맵이 빠름

3. 문자열 키 최적화

// ❌ 긴 문자열 키는 비효율적
m1 := make(map[string]int)
m1["very_long_key_name_that_takes_memory"] = 1

// ✅ 짧은 문자열 또는 정수 키 권장
m2 := make(map[int]int)
m2[1] = 1

// ✅ 또는 문자열 인턴 패턴 사용
type StringInterner struct {
    strings map[string]string
}

func (si *StringInterner) Intern(s string) string {
    if interned, ok := si.strings[s]; ok {
        return interned
    }
    si.strings[s] = s
    return s
}

일반적인 실수

1. nil 맵에 쓰기

func mistake1() {
    var m map[string]int // nil 맵
    
    // ❌ 패닉 발생!
    // m["key"] = 1 // panic: assignment to entry in nil map
    
    // ✅ 초기화 후 사용
    m = make(map[string]int)
    m["key"] = 1
}

2. 맵 순회 순서 의존

func mistake2() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
    // ❌ 순서에 의존하는 코드
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    // keys 순서는 실행마다 다름!
    
    // ✅ 명시적으로 정렬
    sort.Strings(keys)
}

3. 맵 비교

func mistake3() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]int{"a": 1}
    
    // ❌ 컴파일 에러: 맵은 == 비교 불가
    // fmt.Println(m1 == m2)
    
    // ✅ nil 비교만 가능
    var m3 map[string]int
    fmt.Println(m3 == nil) // true
    
    // ✅ 수동 비교
    equal := len(m1) == len(m2)
    if equal {
        for k, v := range m1 {
            if m2[k] != v {
                equal = false
                break
            }
        }
    }
    
    // ✅ reflect 사용
    fmt.Println(reflect.DeepEqual(m1, m2)) // true
}

4. 맵 순회 중 수정

func mistake4() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
    // ⚠️ 순회 중 삭제는 안전하지만 예측 불가능
    for k, v := range m {
        if v%2 == 0 {
            delete(m, k) // 안전하지만 순회 결과 예측 어려움
        }
    }
    
    // ✅ 삭제할 키를 먼저 수집
    toDelete := []string{}
    for k, v := range m {
        if v%2 == 0 {
            toDelete = append(toDelete, k)
        }
    }
    for _, k := range toDelete {
        delete(m, k)
    }
}

5. 구조체 필드 수정 시도

type Person struct {
    Name string
    Age  int
}

func mistake5() {
    m := map[string]Person{
        "alice": {"Alice", 25},
    }
    
    // ❌ 컴파일 에러: 맵 요소는 addressable하지 않음
    // m["alice"].Age = 26
    
    // ✅ 전체 값 재할당
    p := m["alice"]
    p.Age = 26
    m["alice"] = p
    
    // ✅ 또는 포인터 사용
    m2 := map[string]*Person{
        "alice": {"Alice", 25},
    }
    m2["alice"].Age = 26 // OK
}

6. 동시성 문제

func mistake6() {
    m := make(map[int]int)
    
    // ❌ 데이터 레이스
    go func() {
        m[1] = 1
    }()
    
    go func() {
        m[2] = 2
    }()
    
    // ✅ Mutex 사용
    var mu sync.Mutex
    go func() {
        mu.Lock()
        m[1] = 1
        mu.Unlock()
    }()
}

맵 관련 팁

1. 존재하지 않는 키의 기본값

func getWithDefault(m map[string]int, key string, defaultValue int) int {
    if value, ok := m[key]; ok {
        return value
    }
    return defaultValue
}

// 또는 제로값 활용
func increment(m map[string]int, key string) {
    m[key]++ // 존재하지 않으면 0에서 시작
}

2. 맵을 슬라이스로 변환

type Entry struct {
    Key   string
    Value int
}

func mapToSlice(m map[string]int) []Entry {
    entries := make([]Entry, 0, len(m))
    for k, v := range m {
        entries = append(entries, Entry{k, v})
    }
    return entries
}

3. 조건부 초기화

func ensureKey(m map[string][]int, key string) {
    if _, ok := m[key]; !ok {
        m[key] = []int{}
    }
}

// 사용
m := make(map[string][]int)
ensureKey(m, "items")
m["items"] = append(m["items"], 1, 2, 3)

정리

  • 맵은 키-값 쌍을 저장하는 해시 테이블
  • 참조 타입: 함수 전달 시 원본 수정됨
  • nil 맵: 읽기는 가능하지만 쓰기는 패닉
  • make() 또는 리터럴로 초기화 필수
  • 순서 없음: 순회 순서는 무작위 (의도적)
  • Comma Ok Idiom: value, ok := m[key]로 존재 여부 확인
  • delete(m, key)로 삭제
  • 키는 comparable 타입만 가능
  • 동시성 안전 아님: Mutex 또는 sync.Map 사용
  • 초기 용량 힌트로 성능 개선 가능
  • 맵은 == 비교 불가 (nil 비교만 가능)
  • 구조체 필드 직접 수정 불가 (값 재할당 또는 포인터 사용)
  • 작은 데이터(<10개)는 슬라이스가 더 빠를 수 있음