Go 언어 make 함수와 참조 타입 초기화 심층 분석

Go 언어 make 함수: Slice, Map, Channel 초기화

Go 언어에서 make는 단순한 “초기화 함수”가 아닙니다. 이 함수는 Slice, Map, Channel이라는 세 종류의 참조 타입이 실제로 작동하기 위한 내부 구조를 메모리에 구성하는 역할을 합니다. 이전 글에서 깊이 다루었던 nil의 문제를 기억한다면, make는 바로 그 nil 상태를 해결하는 열쇠입니다.

이 글에서는 make의 문법과 각 타입별 사용법을 단순히 나열하는 것을 넘어, 내부적으로 무엇이 일어나는지, 그리고 리터럴과 make 중 실무에서 언제 무엇을 선택해야 하는지까지 실전적으로 알아보겠습니다.


1. 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 런타임에게 “이 타입의 내부 구조를 메모리에 준비하라”는 지시를 내리는 것입니다.


2. make 함수의 문법과 파라미터 해부

make의 기본 문법은 다음과 같습니다.

make(type, length, capacity)

각 파라미터의 의미는 타입에 따라 달라집니다. 이를 한 테이블로 정리하면 다음과 같습니다.

파라미터SliceMapChannel
type[]Tmap[K]Vchan T
length초기 원소 수사용 안 됨버퍼 크기
capacity내부 배열 크기 (선택)초기 해시테이블 크기 힌트 (선택)사용 안 됨

💡 핵심 포인트: capacity는 선택적 파라미터입니다. 지정하지 않으면 Slice의 경우 length와 동일하게 설정됩니다.


3. 타입별 make 사용법과 내부 구조

3-1. Slice: length와 capacity의 차이

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)
}

3-2. Map: 해시테이블의 준비

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)

💡 이 값은 어디까지나 힌트입니다. 실제 저장되는 키-값 수가 이를 초과해도 자동으로 확장됩니다.

3-3. Channel: 버퍼와 동시성

Channel은 Go 언어의 고루틴(Goroutine) 간 데이터를 전달하는 파이프입니다. make의 두 번째 파라미터는 버퍼 크기를 설정하며, 이 값이 동시성 패턴의 작동 방식을 결정합니다.

c := make(chan int, 2) // 버퍼 크기 2인 채널

c <- 1 // 버퍼에 저장 (블록 안 됨)
c <- 2 // 버퍼에 저장 (블록 안 됨)
// c <- 3 // ⚠️ 여기서 블록 발생 — 버퍼가 가득침

fmt.Println(<-c) // 1
fmt.Println(<-c) // 2
버퍼 크기이름특징
0Unbuffered Channel전송자와 수신자가 동시에 준비되어야 함
> 0Buffered Channel버퍼가 비어있는 동안 수신자 없이도 전송 가능

⚠️ Channel은 고루틴과 깊은 연관이 있는 주제입니다. 이 글에서는 초기화 방법까지만 다루며, Channel과 Goroutine의 실무 패턴은 추후 시리즈에서 상세히 다룰 예정입니다.


4. make vs 리터럴: 실무에서의 선택 기준

make와 리터럴은 둘 다 참조 타입을 초기화하는 수단이지만, 적절한 사용 상황이 다릅니다.

상황권장 방법이유
초기 값이 명확하고 고정되어 있을 때리터럴코드가 간결하고 의도가 한눈에 보임
크기만 정해놓고 값을 나중에 채울 때make유연성과 가독성 우선
동적으로 데이터가 들어오는 경우make리터럴의 초기값이 의미 없음
특정 capacity·버퍼 크기가 필요한 경우make리터럴로는 capacity 지정 불가

🛠️ 실무 관점

실제 업무에서 데이터는 “상수가 아닌 경우”가 압도적으로 많습니다. API에서 받은 응답, 데이터베이스 조회 결과 등에서 초기 값이 미리 정해져 있는 경우는 드물기 때문에, make의 사용 빈도가 리터럴보다 높을 수 있습니다.


5. ⚠️ 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는 준비의 행위

make 함수는 Go 언어에서 참조 타입을 올바르게 사용하기 위한 준비 단계입니다. 단순히 “빈 변수를 만드는 것”이 아니라, Go 런타임에게 타입에 맞는 내부 구조를 구성하라는 지시입니다.

오늘 글의 핵심을 요약하면:

  1. 참조 타입은 반드시 초기화되어야 사용 가능하다 (nil → panic).
  2. make의 파라미터 의미는 타입에 따라 다르다 — Slice는 length/capacity, Map은 크기 힌트, Channel은 버퍼 크기.
  3. capacity와 크기 힌트는 성능의 열쇠 — 적절한 설정이 불필요한 메모리 재할당을 방지한다.
  4. 리터럴과 make는 상황에 따라 선택하며, 동적 데이터가 많을수록 make가 적합하다.

다음 글에서는 이번까지 배운 참조 타입과 메모리 개념을 기반으로, Go 언어의 고루틴(Goroutine)채널(Channel)이 만드는 동시성 세계로 발걸음을 옮기겠습니다.