ONE QUESTION AT A TIME.

천천히 생각하고, 분명하게.

TODAY’S QUESTION606 / 1500
언어·런타임난이도 하5분 답변 목표 · 보강본

Go 슬라이스를 다른 변수에 대입한 뒤 복사본의 원소를 바꿨더니 원본도 바뀝니다. 어떤 저장 공간을 공유하며 독립된 복사본은 어떻게 만드나요?

잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?

답변과 점검 포인트 펼치기답변 접기
01

구두 답변

정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.

여기서 ‘복사’가 슬라이스 변수의 대입인지, 원소를 새 배열에 복사한 것인지 구분해야 합니다. 다음 코드에서는 변수는 두 개지만 실제 원소를 담은 배열은 하나입니다.

슬라이스 헤더와 배열

a := []int{10, 20, 30}
b := a
b[0] = 99

fmt.Println(a) // [99 20 30]
fmt.Println(b) // [99 20 30]

슬라이스는 원소를 직접 담는 배열 자체가 아닙니다. 기반 배열을 가리키는 정보, 길이인 len, 사용할 수 있는 용량인 cap을 가진 값으로 이해하면 됩니다. b := a는 이 정보를 복사하므로 두 슬라이스가 같은 배열을 가리킵니다. b[0]을 바꾸면 a[0]으로 보는 원소도 바뀌는 이유입니다.

원소를 독립적으로 바꾸려면 새 공간을 만들고 복사해야 합니다.

a := []int{10, 20, 30}
b := make([]int, len(a))
copy(b, a)
b[0] = 99

fmt.Println(a) // [10 20 30]
fmt.Println(b) // [99 20 30]

이 예제의 원소는 정수라서 이렇게 하면 독립됩니다. 다만 원소가 포인터나 다른 슬라이스라면 그 안의 참조 대상까지 재귀적으로 복제하는 것은 아닙니다. 새 바깥 배열을 만든 것과 깊은 복사는 구분해야 합니다.

append에서도 같은 공유 문제가 나타날 수 있습니다. 아래처럼 일부만 잘라 만든 슬라이스는 길이 뒤에 사용할 용량이 남아 있습니다.

a := []int{10, 20, 30}
b := a[:1]          // len(b) == 1, cap(b) == 3
b = append(b, 99)    // 기존 배열의 두 번째 칸을 사용

fmt.Println(a) // [10 99 30]
fmt.Println(b) // [10 99]

append가 항상 새 배열을 만드는 것은 아닙니다. 기존 용량이 충분하면 재사용하고, 부족하면 새 기반 배열을 할당합니다. 위에서는 재사용했으므로 원본의 두 번째 값이 바뀝니다. 그렇다고 a의 길이까지 2로 바뀌지는 않습니다. 원소 배열은 공유하지만 각 슬라이스의 길이 정보는 별개이기 때문입니다.

append·보관·소유권

실무에서는 수신 버퍼의 일부를 슬라이스로 잘라 비동기 작업에 넘기는 경우를 조심하겠습니다. 작업이 읽기도 전에 수신 버퍼를 재사용하면 내용이 바뀔 수 있습니다. 함수 실행 중에만 빌리는 데이터라면 공유할 수 있지만, 나중에 사용할 데이터라면 필요한 부분을 복사하거나 완료까지 버퍼의 소유권을 넘기는 계약을 두겠습니다. 값으로 전달했다는 이유만으로 데이터도 독립됐다고 판단하지 않는 것이 핵심입니다.

선택 기준과 검증

함수 인자로 슬라이스를 값으로 넘겨도 backing array 공유는 사라지지 않습니다. a[:1:1]은 append가 기존 뒤쪽 칸을 쓰지 않게 만들지만, 이미 노출된 b[0]의 직접 수정은 여전히 공유됩니다. 작은 부분을 장기 보관하면 큰 배열 전체가 살아 있을 수 있으므로 보관 경계에서 필요한 범위만 복사하고, 비동기 소비와 풀 반환 사이에는 명시적인 소유권 전환을 두겠습니다.

함수에서 append한 새 슬라이스를 반환하지 않으면 호출자의 길이는 바뀌지 않습니다. 기반 배열을 재사용해 일부 데이터는 바뀌었는데 호출자는 예전 len만 보거나, 새 배열이 생겨 추가 원소 자체가 호출자에게 보이지 않을 수 있습니다. 따라서 원소 변경과 슬라이스 헤더 변경을 API 계약에서 나누고 확장 결과는 명시적으로 반환해야 합니다.

copy는 원소 참조를 옮기는 얕은 복사이며 겹치는 슬라이스 사이 복사도 지원합니다. 그러나 다른 고루틴이 그 배열을 동시에 수정해도 된다는 의미는 아닙니다. nil 슬라이스와 길이 0인 비nil 슬라이스는 len이나 range에서 비슷해도 JSON 표현이나 nil 검사에서 다를 수 있으므로 외부 응답 계약도 확인하겠습니다. 용량 증가 배율은 런타임 구현 세부이므로 실제 cap 변화를 측정할 수는 있어도 영구 규칙으로 의존하지 않겠습니다.

02

득점 포인트

이해가 드러나는 지점을 확인해 보세요.

  • 슬라이스 헤더와 기반 배열의 공유를 구분한다.
  • append의 cap·재할당과 full slice expression을 설명한다.
  • 버퍼 풀·비동기 수명·깊은 복사의 경계를 확인한다.
03

감점 포인트

놓치기 쉬운 조건과 오해를 짚어 보세요.

  • Go가 값 전달이므로 슬라이스 원소도 항상 독립된다고 말한다.
  • append가 언제나 새 backing array를 만든다고 단정한다.
  • copy가 포인터·중첩 슬라이스까지 깊게 복제한다고 설명한다.
04

더 파고들 거리

하나 더 질문하며 이해의 범위를 넓혀 보세요.

득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.

원리부터 공부하고 싶다면

답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.

← 전체 질문 보기