-
golang: 루프 변수의 스코프 이슈(Fixing For Loops in Go 1.22)Back-End/Golang 2024. 8. 5. 12:46반응형
이 글은 https://go.dev/blog/loopvar-preview 주제를 다룹니다.
문제 설명
루프 변수의 스코프 문제
Go에서 for 루프는 반복문 내에서 루프 변수를 사용합니다. 하지만 기존의 Go 버전에서는 루프 변수의 스코프가 루프 전체에 걸쳐 있기 때문에, 개발자가 의도하지 않게 루프 변수가 변경되는 상황이 발생할 수 있습니다. 이 문제는 특히 **고루틴(goroutine)**이나 **클로저(closure)**를 사용할 때 더욱 두드러지며, 예측하지 못한 동작을 초래할 수 있습니다.
예시 코드 및 문제점
2. 고루틴 사용 예시
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { go func() { fmt.Println(v) done <- true }() } // wait for all goroutines to complete before exiting for _ = range values { <-done } }
문제점: go func() 클로저에서 v 변수를 사용하고 있습니다. 루프가 끝난 후, v는 마지막 값 "c"로 설정되며, 모든 고루틴은 동일한 v를 참조하여 출력합니다. 따라서 예상과 달리 모든 고루틴이 "c"를 출력하게 됩니다.
2. 클로저 사용 예시
func main() { var prints []func() for i := 1; i <= 3; i++ { prints = append(prints, func() { fmt.Println(i) }) } for _, print := range prints { print() } }
문제점: prints 슬라이스에 추가된 각 함수는 i의 참조를 유지합니다. 루프가 종료된 후 i는 최종 값 4를 가지며, 모든 함수 호출이 4를 출력합니다.
실제 예시: Let’s Encrypt 문제
Let’s Encrypt 프로젝트에서도 이 문제는 발생했습니다. 그들은 아래와 같은 문제를 겪었습니다:
// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a // protobuf authorizations map func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) { resp := &sapb.Authorizations{} for k, v := range m { // Make a copy of k because it will be reassigned with each loop. kCopy := k authzPB, err := modelToAuthzPB(&v) if err != nil { return nil, err } resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{ Domain: &kCopy, Authz: authzPB, }) } return resp, nil }
문제점: 이 코드는 kCopy를 만들어 k의 변화를 방지했지만, modelToAuthzPB가 v의 포인터를 사용하여 결과를 생성합니다. 따라서 v도 복사해야 했습니다. v를 복사하지 않으면 modelToAuthzPB는 예상치 못한 값을 사용할 수 있습니다.
도구의 한계
- go vet 및 gopls: 루프 클로저 문제를 분석하지만, 정확한 문제를 감지하기 어렵습니다. 이 도구들은 정확성을 위해 false negatives(문제가 있는 것을 문제로 인식하지 않음)를 선택하거나, 과도한 경고(false positives)를 발생시킵니다.
- 개발자 대응: 많은 개발자가 문제가 없는 코드에 방어적으로 x := x와 같은 불필요한 코드를 추가하여 경고를 피하려고 합니다.
예시 차이
두 가지 비슷한 코드를 비교해보면 문제를 파악하기 어렵습니다.
1. informer 예시:
for _, informer := range c.informerMap { informer := informer go informer.Run(stopCh) }
2. alarm 예시:
for _, a := range alarms { a := a go a.Monitor(b) }
이 두 예시 중 하나는 실제 버그를 해결한 것이고, 다른 하나는 불필요한 변경입니다. 타입과 함수의 구체적인 동작을 모른다면 어떤 것이 어떤 것인지 알기 어렵습니다.
문제 해결: Go 1.22에서의 변화
새로운 루프 스코프
Go 1.22에서는 for 루프 변수가 각 반복(iteration)마다 독립적인 스코프를 가지게 됩니다. 이는 기존의 per-loop 스코프에서 per-iteration 스코프로 변경되는 것을 의미합니다.
- 변경 전: 루프 전체에 걸쳐 변수가 재사용됩니다.
- 변경 후: 각 반복마다 새로운 변수가 생성됩니다.
이전 버전의 예시 코드 수정 후
고루틴 예시 수정
func main() { done := make(chan bool) values := []string{"a", "b", "c"} for _, v := range values { v := v // 변수를 복사하여 고루틴 내에서 안전하게 사용 go func() { fmt.Println(v) done <- true }() } for _ = range values { <-done } }
v := v를 사용하여 각 고루틴이 고유한 v를 가지게 됩니다.
클로저 예시 수정
func main() { var prints []func() for i := 1; i <= 3; i++ { i := i // 변수 복사 prints = append(prints, func() { fmt.Println(i) }) } for _, print := range prints { print() } }
i := i로 복사하여 각 함수가 다른 i를 참조합니다.
변화의 영향
- 생산성 향상: 루프 스코핑 문제를 자동으로 해결하여 버그를 줄입니다.
- 코드 클린업: 불필요한 x := x와 같은 코드 제거 가능.
- 역방향 호환성: Go 1.22 이상으로 설정된 모듈에만 적용됩니다. 기존 모듈은 현재 스코핑 규칙을 유지합니다.
- 변경 방법: 모듈의 go.mod 파일에 go 1.22를 명시하여 새 스코핑 규칙을 적용합니다.
Preview and Backward Compatibility
- GOEXPERIMENT=loopvar: Go 1.21에서는 GOEXPERIMENT=loopvar 환경 변수를 설정하여 새로운 스코핑을 미리 체험할 수 있습니다.
- Old Code Compatibility: Go 1.21 이하 버전에서는 새로운 문법을 지원하지 않습니다. Go 1.22 이상에서만 새로운 스코핑 규칙을 적용할 수 있습니다.
결론
Go 1.22의 루프 스코핑 변화는 Go 언어의 타입 안정성과 코드 안정성을 크게 개선합니다. 개발자가 루프 클로저와 고루틴에서 흔히 저지르는 실수를 줄이며, 더 안전한 코드를 작성할 수 있게 됩니다. 기존 코드에 대한 역방향 호환성을 유지하면서도 새로운 코드에는 개선된 기능을 적용할 수 있어, 점진적인 마이그레이션이 가능합니다.
반응형'Back-End > Golang' 카테고리의 다른 글
golang: 그레이스풀 셧다운(Graceful Shutdown) (1) 2024.10.25 golang: Gin vs Chi (2) 2024.10.17 golang: 제네릭(Generic)이란? (0) 2024.08.03 golang: Tee 패턴이란? (1) 2024.05.15 golang: 파이프라인(pipeline)이란? (0) 2024.05.15