고랭(Golang) 리플렉션, 정말 느린가요? 알아볼까요?
리플렉션이 필요한 이유는 무엇인가요?
먼저 리플렉션이 어떤 이점을 가져는지 이해할 필요가 있습니다.
만약 어떤 장점도 없다면, 사실 우리는 그것을 사용할 필요가 없으며 성능에 미치는 영향에 대해 걱정할 필요도 없습니다.
고랭에서 리플렉션의 구현 원리
고랭은 구문 요소가 적고 디자인이 단순하기 때문에 특별히 강력한 표현력을 가지고 있지 않습니다.
그러나 고랭의 reflect
패키지는 일부분 구문적 단점을 보완할 수 있습니다.
리플렉션은 반복적인 코딩 작업을 줄일 수 있으며, 툴킷은 리플렉션을 사용하여 다양한 구조체 입력 매개변수를 처리합니다.
리플렉션을 사용하여 구조체가 비어 있는지 판단하기
비즈니스 시나리오
이런 방식으로 들어오는 구조체가 비어 있을 때 우리는 SQL을 연결하지 않고 바로 반환할 수 있으며, 따라서 전체 테이블 스캔과 느린 SQL을 피할 수 있습니다.
리플렉션을 사용하지 않은 구현
리플렉션을 사용하지 않으면, 구조체가 비어 있는지 판단할 때 각 필드를 하나씩 확인해야 합니다.
구현은 다음과 같습니다:
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
}
이때 새 구조체를 추가하여 비어 있는지 판단해야 한다면, 각 필드를 확인하는 해당 메서드를 구현해야 합니다.
리플렉션을 사용한 구현
리플렉션을 사용하여 구현하면, 다음을 참조할 수 있습니다: Golang 빈 구조체 판단. 이때 해당 구조체를 전달하기만 하면 해당 데이터가 비어 있는지 여부를 얻을 수 있으며, 반복 구현이 필요 없습니다.
성능 비교
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()
}
}
성능 테스트 실행
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
: 테스트 함수의 이름이며,- 16
은 GOMAXPROCS(스레드 수) 값이 16임을 나타냅니다.2899022
: 총 2899022번의 실행이 수행되었습니다.401 ns/op
: 작업당 평균 401 나노초가 소요되었음을 나타냅니다.112 B/op
: 작업당 112 바이트의 메모리가 할당되었음을 나타냅니다.7 allocs/op
: 메모리가 일곱 번 할당되었음을 나타냅니다.
리플렉션으로 판단한 각 작업의 시간 소모는 직접 판단의 약 1000배이며, 또한 일곱 번의 추가 메모리 할당이 발생하여 매번 112 바이트가 증가됩니다.
전반적으로 성능은 직접 작업과 비교하여 여전히 크게 떨어집니다.
리플렉션을 사용하여 동일한 이름의 구조체 필드 복사
리플렉션을 사용하지 않은 구현
실제 비즈니스 인터페이스에서 우리는 종종 DTO와 VO 사이에서 데이터를 변환해야 하며, 대부분의 경우 동일한 이름의 필드를 복사하는 것입니다.
이때 리플렉션을 사용하지 않으면 각 필드를 복사해야 하며, 새 구조체를 복사해야 할 때 새 메서드의 작성을 반복해야 하므로 많은 반복 작업이 발생합니다.
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,
}
}
리플렉션을 사용한 구현
리플렉션을 사용하여 구조체를 복사하면, 새 구조체를 복사해야 할 때 구조체 포인터를 전달하기만 하면 동일한 이름의 필드를 복사할 수 있습니다. 구현은 다음과 같습니다:
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
위의 첫 번째 실행 결과와 유사하게, 리플렉션의 시간 소모는 여전히 리플렉션을 사용하지 않은 경우의 약 1000배이며, 메모리 할당도 매번 64 바이트가 증가됩니다.
실제 비즈니스 시나리오에서는 여러 리플렉션이 결합될 수 있습니다.
실제 성능을 테스트하려면 자신만의 BenchmarkTest를 작성할 수 있습니다. 플레임 그래프를 비교하면 실행 시간의 비율을 더 명확하게 보여줄니다.
결론
비즈니스 인터페이스에서 인터페이스 응답이 10ms라고 가정하고, 리플렉션 메서드의 평균 작업이 400 나노초라면 약 64 - 112 바이트의 추가 메모리 할당이 발생합니다.
1ms [밀리초] = 1000μs [마이크로초] = 1000 * 1000ns [나노초]
1MB = 1024KB = 1024 * 1024 B
인터페이스가 링크에서 1000번의 리플렉션 작업을 수행하면, 단일 작업이 인터페이스 지연시를 약 0.4ms 증가시킵니다.
일반적으로 단일 요청에서 미들웨어 및 비즈니스 작업의 수는 거의 이 수에 도달하지 않으므로 응답 시간에 미치는 영향은 기본적으로 무시할 수 있습니다.
실제 비즈니스에서 더 많은 손실은 메모리 복사 및 네트워크 IO에서 발생합니다.
그러나 리플렉션은 코딩에서도 실제 문제가 있습니다.
일반적인 비즈니스 코드보다 유지 및 이해가 더 어렵습니다.
따라서 과도하게 사용하지 않도록 신중히 고려해야 합니다.
과도하게 사용하면 코드의 복잡성이 지속 증가될 것입니다.
리플렉션은 고랭에서 강력한 도구이지만, 성능 저하 및 코드 복잡성 증가 문제가 있습니다.
실제 비즈니스 시나리오에서는 리플렉션의 사용을 신중히 고려해야 하며, 필요한 경우에만 사용해야 합니다.
성능 테스트를 통해 리플렉션의 영향을 확인하고, 적절한 최적화를 진행하는 것이 중요합니다.
'Go' 카테고리의 다른 글
[Go 언어 탐구] 슬라이스 용량(Capacity)은 어떻게 늘어날까? append의 비밀 파헤치기 (Go 1.23 기준) (0) | 2025.04.27 |
---|---|
우버(Uber)가 만든 고성능 Go 로깅! Zap(자프) 사용법 완벽 정리 (설치부터 파일 분리, 색상 출력까지) (0) | 2025.04.26 |
Go 1.22 버전, `http.ServeMux` 하나면 충분할까요? (0) | 2025.04.25 |
Golang 웹 프레임워크 7종 비교분석 (Gin, Echo, Beego, Revel, Fiber, Gorilla Mux, go-zero/rest) (0) | 2025.03.29 |
Go 언어의 난수, 왜 예측 가능할까요? (math/rand vs crypto/rand 깊이 파헤치기) (0) | 2025.03.24 |