benchmark와 test
Golang에서 benchmark와 test는 코드의 정확성과 성능을 보장하는 핵심 요소입니다. 특히, 대규모 시스템이나 고성능이 중요한 애플리케이션을 개발하는데 매우 중요한 기능입니다. 이번 문서에서는 benchmark와 test에 대한 사용법을 알아보겠습니다.
benchmark란
Golang에서 benchmark란 특정 코드, 함수, 알고리즘 또는 시스템의 성능을 측정하여 실행 속도, 처리량, 리소스 사용량(CPU, 메모리 등)을 분석하는 과정입니다. 이를 통해 코드의 최적화 가능성을 평가하고 여러 구현 방식 간의 성능 차이를 비교하며 병목 현상을 식별하여 효율적인 개선 방향을 찾을 수 있습니다. 특히 Golang에서는 go test -bench 명령어를 활용하여 특정 함수의 실행 시간을 반복적으로 측정하고 평균 실행 시간을 ns/op(나노초/연산) 단위로 확인하고 코드 변경 전후의 성능을 비교하여 보다 빠르고 최적화된 코드 구조를 설계하는 데 중요한 역할을 합니다. 이를 통해 애플리케이션의 전반적인 성능을 향상하고, 최적의 알고리즘을 선택하여 개발하는데 많은 도움이 됩니다.
benchmark의 중요성
- 성능 최적화: 코드 실행 시간을 측정하여 병목 현상을 발견하고 개선할 수 있습니다.
- 효율적인 리소스 사용: CPU, 메모리 사용량을 줄여 더 빠른 애플리케이션을 만들 수 있습니다.
- 다양한 구현 방식 비교: 두 가지 이상의 알고리즘을 벤치마크하여 더 빠른 방법을 선택할 수 있습니다.
- 최적화 후 성능 검증: 코드 변경 후 실제로 성능이 개선되었는지 수치적으로 확인할 수 있습니다.
test란
go test는 Go 언어에서 제공하는 기본 테스트 실행 도구로, 개발자가 작성한 테스트 코드(*_test.go 파일)를 자동으로 찾아 실행하며, 코드의 올바른 동작 여부를 검증하는 유닛 테스트(Unit Test), 기능 테스트(Functional Test), 통합 테스트(Integration Test) 등을 수행할 수 있는 기능입니다. 이를 통해 코드 변경 시 발생할 수 있는 예기치 않은 오류를 사전에 감지하고, 소프트웨어의 신뢰성을 유지하며, CI/CD 파이프라인과 연동하여 자동화된 테스트 환경을 구축하는 데 필수적인 역할을 수행하는 강력한 도구입니다.
test의 중요성
- 버그 방지: 코드 수정 시, 기존 기능이 깨지지 않았는지 확인할 수 있습니다.
- 안정성 확보: 배포 전에 문제를 발견하여 안정적인 소프트웨어를 제공할 수 있습니다.
- 코드 유지보수성 향상: 테스트 코드가 있으면, 코드 변경 후에도 정상 동작 여부를 쉽게 확인할 수 있습니다.
- TDD (Test-Driven Development) 가능: 테스트를 먼저 작성한 후 기능을 구현하는 방식으로, 안정적인 개발이 가능합니다.
예제
gin 프레임워크를 이용하여 간단한 프로젝트를 만들었습니다. 숫자를 받으면 1부터 해당 숫자까지 더하는 간단한 API를 구성하였고 API의 v1은 루프를 이용하여 1부터 하나씩 증가하여 덧셈을 수행하였으며 v2는 (n * (n + 1)) / 2 공식을 이용하여 해당 덧셈을 수행하는 방식으로 개발을 진행하였습니다. 이제 benchmark를 이용하여 각 버전의 API 성능을 측정하여 비교하는 방식에 대해 알아보겠습니다.
소스코드 : https://github.com/stdhsw/go-benchmark
프로젝트 구조
├── README.md
├── benchmarks
│ ├── benchmark_result.txt
│ └── benchmark_test.go
├── cmd
│ └── server
│ └── main.go
├── go.mod
├── go.sum
├── internal
│ ├── handlers
│ │ ├── v1
│ │ │ ├── parity_handler.go
│ │ │ └── sum_handler.go
│ │ └── v2
│ │ ├── parity_handler.go
│ │ └── sum_handler.go
│ └── services
│ ├── v1
│ │ ├── parity_service.go
│ │ └── sum_service.go
│ └── v2
│ ├── parity_service.go
│ └── sum_service.go
└── tests
├── v1
│ ├── parity_handler_test.go
│ └── sum_handler_test.go
└── v2
├── parity_handler_test.go
└── sum_handler_test.go
internal/service/v1/sum_service.go
package v1
// SumService - 1부터 입력된 숫자까지 합산
func SumService(n int) int {
sum := 0
for i := 1; i <= n; i++ {
sum += i
}
return sum
}
internal/service/v2/sum_service.go
package v2
// SumService - 1부터 n까지의 합을 수학 공식을 이용해 계산
func SumService(n int) int {
return (n * (n + 1)) / 2
}
Test 예제
tests/v1/sum_handler_test.go
tests/v2/sum_handler_test.go
func TestSumHandlerV1(t *testing.T) {
router := gin.Default()
router.GET("/v1/sum", v1.SumHandler)
tests := []struct {
query string
expected int
}{
{"10", 55}, // 1부터 10까지의 합: 55
{"100", 5050}, // 1부터 100까지의 합: 5050
{"1", 1}, // 1부터 1까지의 합: 1
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", "/v1/sum?number="+tt.query, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), fmt.Sprintf(`"sum":%d`, tt.expected))
}
}
func TestSumHandlerV2(t *testing.T) {
router := gin.Default()
router.GET("/v2/sum", v2.SumHandler)
tests := []struct {
query string
expected int
}{
{"10", 55},
{"100", 5050},
{"1", 1},
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", "/v2/sum?number="+tt.query, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), fmt.Sprintf(`"sum":%d`, tt.expected))
}
}
test 수행하는 방법
go test를 이용하여 현재 API의 결과에는 문제가 없는 것을 확인할 수 있습니다.
# 전체 테스트 실행
go test ./tests/...
# 출력 결과
ok github.com/stdhsw/go-benchmark/tests/v1 1.049s
ok github.com/stdhsw/go-benchmark/tests/v2 0.707s
Benchmark 예제
benchmarks/benchmakr_test.go
// BenchmarkSumV1 - v1의 Sum API 성능 측정
func BenchmarkSumV1(b *testing.B) {
router := gin.Default()
router.GET("/v1/sum", v1.SumHandler)
req, _ := http.NewRequest("GET", "/v1/sum?number=10000", nil)
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
}
// BenchmarkSumV2 - v2의 Sum API 성능 측정
func BenchmarkSumV2(b *testing.B) {
router := gin.Default()
router.GET("/v2/sum", v2.SumHandler)
req, _ := http.NewRequest("GET", "/v2/sum?number=10000", nil)
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
}
benchmark 수행하는 방법
프로젝트는 위와 같이 구성되어 있으며 benchmark 기능을 통해 v1과 v2의 성능을 측정하는 방법에 대해 알아보겠습니다.
# 벤치마크는 여러번의 수행을 통해 결과를 측정하기 때문에 출력 결과가 매우 많습니다.
# 파일로 저장하여 출력 결과 확인
go test -bench=. benchmarks/benchmark_test.go > benchmark_result.txt
benchmark 결과
[GIN-debug] GET /v1/sum --> example.com/myproject/internal/handlers/v1.SumHandler (3 handlers)
[GIN] 2025/03/19 - 17:50:11 | 200 | 57.334µs | | GET "/v1/sum?number=10000"
BenchmarkSumV1-12 [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
(생략) ...
[GIN-debug] GET /v2/sum --> example.com/myproject/internal/handlers/v2.SumHandler (3 handlers)
[GIN] 2025/03/19 - 17:50:13 | 200 | 7.75µs | | GET "/v2/sum?number=10000"
BenchmarkSumV2-12 [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
(생략) ...
Benchmark 결과를 보면 BenchmarkSumV1은 평균 57.334µs가 소요된 반면, BenchmarkSumV2는 7.75µs로 실행되어 훨씬 더 빠른 성능을 보여주었습니다. 이처럼 벤치마크를 활용하면 코드의 실행 시간을 정량적으로 비교하며, 보다 최적화된 성능을 유지하면서 개발을 진행할 수 있도록 도와줍니다.
benchmark 옵션
benchmark에서는 여러 옵션을 이용하여 상세한 설정으로 benchmark를 수행할 수 있습니다.
옵션 |
설명 |
-benchmem |
메모리 사용량 확인 |
-benchtime |
벤치마크 반복 횟수 조정 |
-cpuprofile |
CPU 프로파일링 |
-memprofile |
메모리 프로파일링 |
마지막으로
Golang 개발에서 Test는 코드의 정확성을 보장하여 버그를 방지하고 유지보수성을 높이며, Benchmark는 코드의 성능을 측정하여 최적화된 알고리즘을 선택하고 리소스를 효율적으로 사용하게 해 주므로, 이 두 가지를 함께 활용하면 안정적이고 빠른 애플리케이션을 개발할 수 있으며, CI/CD와 결합하여 지속적인 코드 품질 관리를 가능하게 하고, 다양한 코드 변경에도 신뢰할 수 있는 결과를 유지할 수 있기 때문에, 테스트와 벤치마크는 필수적인 개발 요소라 할 수 있습니다.