Go 채널을 활용한 워커 풀 패턴 구현
문제 상황
사용자가 업로드한 이미지를 여러 해상도로 리사이징하는 작업을 Node.js로 처리하고 있었다. 트래픽이 늘면서 CPU 바운드 작업의 한계가 명확해졌고, 별도 서비스 분리를 결정했다.
Go의 동시성 모델이 이런 작업에 적합하다고 판단해 마이그레이션을 진행했다.
워커 풀 구현
type Job struct {
ImagePath string
Sizes []int
}
func worker(id int, jobs <-chan Job, results chan<- Result) {
for job := range jobs {
log.Printf("Worker %d processing %s", id, job.ImagePath)
result := processImage(job)
results <- result
}
}
func main() {
numWorkers := runtime.NumCPU()
jobs := make(chan Job, 100)
results := make(chan Result, 100)
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// Job 분배 로직
}
성능 개선
- 기존 Node.js: 평균 1.2초/이미지 (3개 해상도)
- Go 워커 풀: 평균 0.3초/이미지
- CPU 사용률: 40% → 85% (효율적 활용)
배운 점
채널은 단순히 데이터 전달이 아니라 동기화 메커니즘이다. sync.WaitGroup과 조합하면 고루틴 완료를 안전하게 관리할 수 있었다.
버퍼 크기 설정이 중요했다. 너무 작으면 블로킹이 잦고, 너무 크면 메모리 낭비였다. 프로파일링 후 100으로 설정했다.
Node.js에서 클러스터링보다 Go의 고루틴이 훨씬 가볍고 직관적이었다. CPU 바운드 작업에서는 확실히 유리하다는 걸 체감했다.