프로그래밍Go 개발자, 백엔드 개발자

큰 구조체를 함수에서 전달하고 반환하는 Go의 특징과 이것이 성능 및 프로그램 동작에 미치는 영향에 대해 설명하세요.

Hintsage AI 어시스턴트로 면접 통과

답변.

Go에서는 구조체(struct)가 기본적으로 값에 의해 전달되고 반환됩니다. 이는 함수 호출이나 반환 시 전체 구조체가 복사된다는 것을 의미합니다. 작은 구조체의 경우 이는 투명하지만, 큰 구조체의 경우에는 문제가 됩니다.

문제의 역사

처음에 Go는 적은 수의 할당으로 효율적으로 작동하도록 설계되었습니다. 그러나 많은 필드와 중첩된 객체를 사용하는 구조체가 있을 때 큰 데이터를 무의식적으로 복사하는 위험이 발생했습니다. 이러한 작업의 성능은 저하될 수 있으며, 때때로 차이는 프로파일링이나 GC의 고통에서만 드러납니다.

문제

구조체가 큰 크기를 가지면, 매 호출 시 함수, 반환 또는 할당 시 복사하는 것이 비용이 많이 듭니다. 이는 다음과 같은 결과를 초래합니다:

  • 실행 시간 증가;
  • GC 부하 증가 (큰 필드를 위한 copy-on-write, 메모리 정리 지연);
  • 복사본에서의 변경 사항이 원본에 반영되지 않는 오류.

해결책

큰 구조체의 경우 구조체의 포인터(*T)를 전달하고 반환하는 것이 좋습니다. 이는 비용을 줄이고 단일 데이터 인스턴스를 활용할 수 있게 합니다.

코드 예시:

package main import "fmt" type Large struct { Data [1024]int } // 값에 의한 전달 (큰 객체에 대해 부적합) func ValueProcess(l Large) { l.Data[0] = 123 // 복사본만 변경됨 } // 포인터에 의한 전달 func PointerProcess(l *Large) { l.Data[0] = 456 // 원본 변경 } func main() { a := Large{} ValueProcess(a) fmt.Println("ValueProcess 이후:", a.Data[0]) // 0 PointerProcess(&a) fmt.Println("PointerProcess 이후:", a.Data[0]) // 456 }

주요 특징:

  • 모든 구조체는 기본적으로 값에 의해 복사됨;
  • 주소(포인터)의 전달은 복사를 피할 수 있게 함;
  • 작은 구조체에 대해서는 값에 의한 반환이 컴파일러에 의해 효율적으로 최적화될 수 있지만, 큰 구조체에는 그렇지 않음.

심리적 질문들.

1. Go에서 함수에서 구조체의 지역 변수에 대한 포인터를 반환할 수 있나요?

네. Go는 포인터가 유효하도록 보장하며, 반환된 포인터가 가리키는 값들을 자동으로 힙으로 이동시킵니다 (escape to heap).

func NewLarge() *Large { l := Large{} return &l }

2. 구조체를 값으로 전달하고 내부 필드를 변경하면 원본이 변경되나요?

아니요: 복사본만 변경되고, 함수 밖의 원본은 그대로 유지됩니다.

3. 항상 구조체에 포인터를 사용해야 하나요?

아니요. 필드 수가 적은 작은 구조체의 경우 값으로 전달하는 것이 안전하고 종종 더 바람직합니다 (immutable/value-semantic), 할당을 절약하고 GC의 부하를 줄입니다.

일반적인 오류 및 안티 패턴

  • 필요 없이 큰 구조체를 반환하고 함수에 값을 전달하는 것;
  • 사소한 struct에 대한 포인터를 불필요하게 사용하는 것;
  • 데이터 변경 가능성에 대한 오류: 의도치 않게 복사본만 업데이트하고 원본은 변경하지 않음.

실생활 예시

부정적인 케이스

로깅 서비스에서 각 이벤트는 큰 구조체로 표현되었으며, 함수에서 값으로 반환되었습니다 — 각 변경 사항이 전체 구조체를 복사했습니다.

장점:

  • 코드는 간단하고 작은 구조체를 위한 안전함.

단점:

  • 메모리 소비가 증가했으며, GC가 자주 작동하여 서비스가 느려지기 시작했습니다.

긍정적인 케이스

구조체를 포인터로 전달하고 반환하는 것으로 전환하여 func(l *Large)func() *Large와 같은 시그니처를 통해 데이터를 변경했습니다.

장점:

  • 복사가 최소화되고, GC의 부하가 줄어들며, 처리 속도가 빨라졌습니다.

단점:

  • 변경 가능성을 제어해야 했으며, 하나의 객체를 다룰 때 의도치 않은 부작용을 피해야 했습니다.