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 값을 참고해 조정하고 있다.