Go 채널과 컨텍스트로 안전한 워커 풀 구현하기
문제 상황
이미지 리사이징 API에서 트래픽이 몰릴 때마다 메모리 사용량이 계속 증가했다. 프로파일링 결과 고루틴이 제대로 종료되지 않고 쌓이는 것이 원인이었다. 기존 코드는 무한정 고루틴을 생성하고 있었다.
워커 풀 패턴 적용
고루틴 수를 제한하고 재사용하는 워커 풀을 구현했다.
type WorkerPool struct {
workers int
jobs chan Job
results chan Result
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewWorkerPool(workers int) *WorkerPool {
ctx, cancel := context.WithCancel(context.Background())
return &WorkerPool{
workers: workers,
jobs: make(chan Job, 100),
results: make(chan Result, 100),
ctx: ctx,
cancel: cancel,
}
}
func (p *WorkerPool) Start() {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go p.worker(i)
}
}
func (p *WorkerPool) worker(id int) {
defer p.wg.Done()
for {
select {
case <-p.ctx.Done():
return
case job, ok := <-p.jobs:
if !ok {
return
}
result := processJob(job)
p.results <- result
}
}
}
func (p *WorkerPool) Stop() {
p.cancel()
close(p.jobs)
p.wg.Wait()
close(p.results)
}
개선 효과
- 메모리 사용량 안정화: 고루틴 수가 워커 수로 제한됨
- 우아한 종료: context와 WaitGroup으로 모든 작업 완료 보장
- 백프레셔 처리: 버퍼드 채널로 과부하 제어
CPU 사용률은 약간 증가했지만, 메모리 누수가 사라지고 서비스 안정성이 크게 향상되었다. 워커 수는 런타임에 GOMAXPROCS 값을 참고해 조정하고 있다.