Golang Reflection: 느린가?
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Reflection이 필요한 이유?
먼저 리플렉션이 가져다주는 이점이 무엇인지 이해해야 합니다. 아무런 이점이 없다면 사실 사용할 필요도 없고 성능에 미치는 영향에 대해 걱정할 필요도 없습니다.
Go 언어에서 Reflection의 구현 원리
Go 언어는 구문 요소가 적고 디자인이 단순하여 표현력이 특별히 강하지 않습니다. 그러나 Go 언어의 reflect
패키지는 구문상의 단점을 보완할 수 있습니다. Reflection은 반복적인 코딩 작업을 줄일 수 있으며, 툴킷은 리플렉션을 사용하여 다양한 구조체 입력 매개변수를 처리합니다.
Reflection을 사용하여 구조체가 비어 있는지 판단하기
비즈니스 시나리오
이런 식으로 들어오는 구조체가 비어 있으면 SQL 연결 없이 바로 반환하여 전체 테이블 스캔 및 느린 SQL을 방지할 수 있습니다.
Reflection을 사용하지 않는 구현
Reflection을 사용하지 않으면 구조체가 비어 있는지 확인해야 할 때 각 필드를 하나씩 확인해야 합니다. 구현 방법은 다음과 같습니다.
type aStruct struct { Name string Male string } func (s *aStruct) IsEmpty() bool { return s.Male == "" && s.Name == "" } type complexSt struct { A aStruct S []string IntValue int } func (c *complexSt) IsEmpty() bool { return c.A.IsEmpty() && len(c.S) == 0 && c.IntValue == 0 }
이때 비어 있는지 판단하기 위해 새로운 구조체를 추가해야 하는 경우 각 필드를 확인하기 위해 해당 메서드를 구현해야 합니다.
Reflection을 사용하는 구현
Reflection을 사용하여 구현하는 경우 Golang Empty Struct Judgment
를 참조할 수 있습니다. 이때 해당 구조체를 전달하기만 하면 반복적인 구현 없이 해당 데이터가 비어 있는지 여부를 알 수 있습니다.
성능 비교
func BenchmarkReflectIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { IsStructEmpty(s) } } func BenchmarkNormalIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { s.IsEmpty() } }
성능 테스트 실행
# -benchmem: 작업당 메모리 할당 횟수를 봅니다. # -benchtime=3s: 실행 시간을 3초로 지정합니다. 일반적으로 1초, 3초, 5초에 얻은 결과는 유사합니다. 성능이 좋지 않으면 실행 시간이 길수록 평균 성능 값이 더 정확합니다. # -count=3: 실행 횟수를 지정합니다. 여러 번 실행하면 정확성을 확인할 수 있습니다. # -cpu n: CPU 코어 수를 지정합니다. 일반적으로 CPU 코어 수를 늘리면 성능이 향상되지만 긍정적인 상관 관계는 아닙니다. 코어가 많을 때 컨텍스트 전환이 영향을 미치기 때문에 IO 집약적인 애플리케이션인지 CPU 집약적인 애플리케이션인지에 따라 다릅니다. 다중 - 고루틴 테스트에서 비교할 수 있습니다. go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
실행 결과
BenchmarkReflectIsStructEmpty-16 8127797 493 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 6139068 540 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 7282296 465 ns/op 112 B/op 7 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.272 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.285 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.260 ns/op 0 B/op 0 allocs/op
결과 분석
결과 필드의 의미:
결과 항목 | 의미 |
---|---|
BenchmarkReflectIsStructEmpty - 16 | BenchmarkReflectIsStructEmpty는 테스트 함수 이름이고 - 16은 GOMAXPROCS(스레드 수) 값이 16임을 나타냅니다. |
2899022 | 총 2899022번의 실행이 수행되었습니다. |
401 ns/op | 작업당 평균 401나노초가 소요되었음을 나타냅니다. |
112 B/op | 작업당 112바이트의 메모리가 할당되었음을 나타냅니다. |
7 allocs/op | 메모리가 7번 할당되었음을 나타냅니다. |
Reflection으로 판단한 각 작업의 시간 소비는 직접 판단하는 것보다 약 1000배 더 많고, 7개의 추가 메모리 할당도 발생하여 매번 112바이트씩 증가합니다. 전체적으로 볼 때 성능은 직접 작업에 비해 여전히 크게 떨어집니다.
Reflection을 사용하여 이름이 같은 구조체 필드 복사
Reflection을 사용하지 않는 구현
실제 비즈니스 인터페이스에서는 DTO
와 VO
간에 데이터를 변환해야 하는 경우가 많으며, 대부분 이름이 같은 필드를 복사하는 것입니다. 이때 리플렉션을 사용하지 않으면 각 필드를 복사해야 하고, 새 구조체를 복사해야 할 때마다 다음과 같이 new
메서드 작성을 반복해야 하므로 반복적인 작업이 많이 발생합니다.
type aStruct struct { Name string Male string } type aStructCopy struct { Name string Male string } func newAStructCopyFromAStruct(a *aStruct) *aStructCopy { return &aStructCopy{ Name: a.Name, Male: a.Male, } }
Reflection을 사용하는 구현
리플렉션을 사용하여 구조체를 복사할 때 복사해야 하는 새 구조가 있는 경우 동일한 이름의 필드를 복사하기 위해 구조체 포인터를 전달하기만 하면 됩니다. 구현 방법은 다음과 같습니다.
func CopyIntersectionStruct(src, dst interface{}) { sElement := reflect.ValueOf(src).Elem() dElement := reflect.ValueOf(dst).Elem() for i := 0; i < dElement.NumField(); i++ { dField := dElement.Type().Field(i) sValue := sElement.FieldByName(dField.Name) if!sValue.IsValid() { continue } value := dElement.Field(i) value.Set(sValue) } }
성능 비교
func BenchmarkCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { var ac aStructCopy CopyIntersectionStruct(a, &ac) } } func BenchmarkNormalCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { newAStructCopyFromAStruct(a) } }
성능 테스트 실행
go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
실행 결과
BenchmarkCopyIntersectionStruct-16 10789202 352 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10877558 304 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10167404 322 ns/op 64 B/op 5 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.277 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.270 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.259 ns/op 0 B/op 0 allocs/op
위의 첫 번째 실행 결과와 유사하게 reflection의 시간 소비는 여전히 reflection을 사용하지 않는 경우의 1000배이고 메모리 할당도 매번 64바이트씩 증가합니다. 실제 비즈니스 시나리오에서는 여러 reflection이 결합될 수 있습니다. 실제 성능을 테스트해야 하는 경우 자신의 BenchmarkTest를 작성할 수 있습니다. 플레임 그래프를 비교하면 실행 시간 비율을 더 명확하게 알 수 있습니다.
결론
비즈니스 인터페이스에서 인터페이스 응답이 10ms라고 가정하고 reflection 메서드의 평균 작업이 400나노초이면 약 64 - 112바이트의 추가 메모리 할당이 발생합니다.
1ms [밀리초] = 1000μs [마이크로초]=1000 * 1000ns [나노초] 1MB = 1024KB = 1024 * 1024 B
인터페이스가 링크에서 1000개의 reflection 작업을 수행하는 경우 단일 작업으로 인해 인터페이스 대기 시간이 약 0.4ms 증가합니다. 일반적으로 단일 요청에서 미들웨어 및 비즈니스 작업의 수는 이 수에 도달하는 경우가 드물므로 응답 시간에 미치는 영향은 기본적으로 무시할 수 있습니다. 실제 비즈니스에서는 메모리 복사 및 네트워크 IO에서 더 많은 손실이 발생합니다.
그러나 reflection은 코딩에도 실제적인 문제가 있습니다. 일반 비즈니스 코드보다 유지 관리 및 이해가 더 어렵습니다. 따라서 과도한 사용을 피하기 위해 사용할 때 신중하게 고려해야 하며, 이는 코드의 복잡성을 지속적으로 증가시킵니다.
Leapcell: Golang 앱 호스팅을 위한 최고의 서버리스 플랫폼
마지막으로 Golang 서비스를 배포하기 위한 최고의 플랫폼을 추천하고 싶습니다: Leapcell
1. 다중 언어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오 — 요청 없음, 요금 없음.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 쉬운 동시성 처리를 위한 자동 확장.
- 운영 오버헤드 제로 — 구축에만 집중하십시오.
Leapcell Twitter: https://x.com/LeapcellHQ