2025-12-15
Go 언어에서 make는 단순한 “초기화 함수”가 아닙니다. 이 함수는 Slice, Map, Channel이라는 세 종류의 참조 타입이 실제로 작동하기 위한 내부 구조를 메모리에 구성하는 역할을 합니다. 이전 글에서 깊이 다루었던 nil의 문제를 기억한다면, make는 바로 그 nil 상태를 해결하는 열쇠입니다.
이 글에서는 make의 문법과 각 타입별 사용법을 단순히 나열하는 것을 넘어, 내부적으로 무엇이 일어나는지, 그리고 리터럴과 make 중 실무에서 언제 무엇을 선택해야 하는지까지 실전적으로 알아보겠습니다.
make가 존재하는 이유: nil과의 관계이전 글에서 우리는 Go 언어의 참조 타입(Slice, Map, Channel)은 초기화되지 않으면 nil 상태로 존재하며, 이 상태에서는 실제 사용이 불가능하다는 것을 배웠습니다.
var s []int // s는 nil — append 이외의 연산은 불가
var m map[string]int // m은 nil — 키-값 저장 시 panic 발생
var c chan int // c는 nil — 전송·수신 모두 영구 블록
그러면 이 참조 타입들을 “사용할 수 있는 상태”로 만드는 방법은 두 가지입니다.
| 방법 | 예시 | 특징 |
|---|---|---|
| 리터럴 | s := []int{1, 2, 3} | 초기 값이 명확할 때 적합 |
| make 함수 | s := make([]int, 3, 5) | 크기·구조만 정해놓고 값을 나중에 채울 때 적합 |
make는 단순히 “빈 변수를 만드는 것”이 아니라, Go 런타임에게 “이 타입의 내부 구조를 메모리에 준비하라”는 지시를 내리는 것입니다.
make의 기본 문법은 다음과 같습니다.
make(type, length, capacity)
각 파라미터의 의미는 타입에 따라 달라집니다. 이를 한 테이블로 정리하면 다음과 같습니다.
| 파라미터 | Slice | Map | Channel |
|---|---|---|---|
| type | []T | map[K]V | chan T |
| length | 초기 원소 수 | 사용 안 됨 | 버퍼 크기 |
| capacity | 내부 배열 크기 (선택) | 초기 해시테이블 크기 힌트 (선택) | 사용 안 됨 |
💡 핵심 포인트:
capacity는 선택적 파라미터입니다. 지정하지 않으면Slice의 경우length와 동일하게 설정됩니다.
Slice는 Go 언어에서 가장 빈번하게 사용되는 참조 타입이기 때문에, make의 두 번째·세 번째 파라미터의 차이를 반드시 이해하는 것이 중요합니다.
s := make([]int, 3, 5)
fmt.Println(len(s)) // 3 — 현재 사용 중인 원소 수
fmt.Println(cap(s)) // 5 — 내부 배열의 총 크기
fmt.Println(s) // [0 0 0] — 타입의 Zero Value로 초기화됨
여기서 length는 “지금 당장 사용할 원소 수”를, capacity는 “앞으로 추가할 원소가 들어갈 수 있는 공간의 크기”를 의미합니다.
🛠️ 실무 Best Practice
capacity를 적절히 설정하면append로 원소를 추가할 때 불필요한 메모리 재할당(reallocation)을 방지할 수 있습니다. 예상하는 원소 수를 미리 알고 있다면, 반드시capacity를 지정하세요.// ❌ 매번 capacity가 부족하면 내부 배열을 새로 복사 s := make([]int, 0) for i := 0; i < 1000; i++ { s = append(s, i) } // ✅ 처음부터 충분한 capacity 확보 s := make([]int, 0, 1000) for i := 0; i < 1000; i++ { s = append(s, i) }
Map은 키-값 쌍을 저장하는 해시테이블 기반 자료구조입니다. nil 상태의 Map에 키-값을 저장하려고 하면 runtime panic이 발생하기 때문에, make로 반드시 초기화해야 합니다.
m := make(map[string]int)
m["article_number"] = 8
fmt.Println(m) // map[article_number:8]
make의 두 번째 파라미터로 초기 크기 힌트를 넘길 수 있습니다. 이는 Go 런타임이 해시테이블의 초기 버킷 수를 결정하는 데 활용됩니다.
// 약 1000개의 키-값을 저장할 예정이므로 힌트 제공
m := make(map[string]int, 1000)
💡 이 값은 어디까지나 힌트입니다. 실제 저장되는 키-값 수가 이를 초과해도 자동으로 확장됩니다.
Channel은 Go 언어의 고루틴(Goroutine) 간 데이터를 전달하는 파이프입니다. make의 두 번째 파라미터는 버퍼 크기를 설정하며, 이 값이 동시성 패턴의 작동 방식을 결정합니다.
c := make(chan int, 2) // 버퍼 크기 2인 채널
c <- 1 // 버퍼에 저장 (블록 안 됨)
c <- 2 // 버퍼에 저장 (블록 안 됨)
// c <- 3 // ⚠️ 여기서 블록 발생 — 버퍼가 가득침
fmt.Println(<-c) // 1
fmt.Println(<-c) // 2
| 버퍼 크기 | 이름 | 특징 |
|---|---|---|
0 | Unbuffered Channel | 전송자와 수신자가 동시에 준비되어야 함 |
> 0 | Buffered Channel | 버퍼가 비어있는 동안 수신자 없이도 전송 가능 |
⚠️ Channel은 고루틴과 깊은 연관이 있는 주제입니다. 이 글에서는 초기화 방법까지만 다루며, Channel과 Goroutine의 실무 패턴은 추후 시리즈에서 상세히 다룰 예정입니다.
make와 리터럴은 둘 다 참조 타입을 초기화하는 수단이지만, 적절한 사용 상황이 다릅니다.
| 상황 | 권장 방법 | 이유 |
|---|---|---|
| 초기 값이 명확하고 고정되어 있을 때 | 리터럴 | 코드가 간결하고 의도가 한눈에 보임 |
| 크기만 정해놓고 값을 나중에 채울 때 | make | 유연성과 가독성 우선 |
| 동적으로 데이터가 들어오는 경우 | make | 리터럴의 초기값이 의미 없음 |
| 특정 capacity·버퍼 크기가 필요한 경우 | make | 리터럴로는 capacity 지정 불가 |
🛠️ 실무 관점
실제 업무에서 데이터는 “상수가 아닌 경우”가 압도적으로 많습니다. API에서 받은 응답, 데이터베이스 조회 결과 등에서 초기 값이 미리 정해져 있는 경우는 드물기 때문에,
make의 사용 빈도가 리터럴보다 높을 수 있습니다.
make의 강력함은 동시에 책임감도 가져옵니다. 크기를 잘못 설정하면 직접적으로 성능과 메모리에 영향을 줍니다.
// ❌ length를 너무 크게 설정
s := make([]int, 1000000) // 백만 개의 Zero Value가 즉시 메모리에 할당됨
// 실제로 사용하는 것이 100개뿐이면, 나머지는 낭비
// ✅ length는 0, capacity로 최대 예상 크기 지정
s := make([]int, 0, 1000)
| 잘못된 패턴 | 발생하는 문제 |
|---|---|
length를 과대 설정 | 불필요한 Zero Value 메모리 할당 |
capacity를 과소 설정 | append 시 반복적인 메모리 재할당 |
| Map 크기 힌트 과대 설정 | 초기 해시테이블이 불필요하게 크게 생성됨 |
| Channel 버퍼를 0으로 설정 | 고루틴 간 동기화 부담 증가 |
make 함수는 Go 언어에서 참조 타입을 올바르게 사용하기 위한 준비 단계입니다. 단순히 “빈 변수를 만드는 것”이 아니라, Go 런타임에게 타입에 맞는 내부 구조를 구성하라는 지시입니다.
오늘 글의 핵심을 요약하면:
nil → panic).make의 파라미터 의미는 타입에 따라 다르다 — Slice는 length/capacity, Map은 크기 힌트, Channel은 버퍼 크기.capacity와 크기 힌트는 성능의 열쇠 — 적절한 설정이 불필요한 메모리 재할당을 방지한다.make가 적합하다.다음 글에서는 이번까지 배운 참조 타입과 메모리 개념을 기반으로, Go 언어의 고루틴(Goroutine)과 채널(Channel)이 만드는 동시성 세계로 발걸음을 옮기겠습니다.