본문 바로가기

프로그래밍/golang

[Go] 고루틴 워커 풀(Worker Pool)

 

Go언어에서 고루틴은 아주 편리하고 강력한 동시성 도구이지만,

제어되지 않은 고루틴은 급격한 리소스 소모와 메모리 누수의 원인이 되기도 합니다.

이에 고루틴은 항상 예측 가능한 범위 내에서 관리가 되어야 하며,

이를 위한 가장 대표적인 설계 패턴 "워커 풀(Worker Pool)"이 있습니다.
이 설계 패턴에 대해서 함께 알아보겠습니다.

 

1. 워커 풀 (Worker Pool)이 필요한 이유

  1. 무분별한 리소스 점유 및 오버헤드
    - 고루틴은 가볍긴 하지만 독립적인 스택 메모리 공간을 차지하게 됩니다.
    매 작업마다 고루틴을 생성해서 기능을 수행하게 되면, 메모리 사용량이 계속해서 증가하고,
    Go 런타임 스케줄러가 관리해야 할 대상도 계속 증가해 스케줄링 오버헤드도 발생하게 됩니다.
    이러한 메모리 사용량 증가와 스케줄링 대상 증가로 인해 전체적인 성능이 저하되는 문제가 발생하게 됩니다.

  2. BackPressure 부재
    - 제어되지 않는 동시성은 시스템이 감당할 수 있는 범위를 쉽게 넘겨버릴 수 있습니다.

  3. 시스템 안정성
    각 작업의 비용(CPU,메모리 등)을 어느정도 예측할 수 있는 환경에서 워커 풀을 통해서 최적의 성능을 이끌어낼 수 있습니다.
    안정적이고 효과적인 워커 수를 설정해서 현재의 환경에서 최적의 안정성과 성능을 뽑아 낼 수 있습니다.

 

2. 예제 코드

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 900*time.Millisecond)
	defer cancel()

	jobs := make(chan int)
	results := make(chan int)

	// 1. Producer: 작업을 생성하여 채널에 삽입
	go func() {
		defer close(jobs)
		for i := 1; i <= 10; i++ {
			jobs <- i
		}
	}()

    // 2. Workers: 3개의 고정된 워커가 병렬 처리
	const workers = 3
	var wg sync.WaitGroup
	wg.Add(workers)

	for w := 0; w < workers; w++ {
		go func(workerID int) {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					return
				case job, ok := <-jobs:
					if !ok {
						return
					}
					// simulate work
					time.Sleep(80 * time.Millisecond) // 작업 시뮬레이션
					select {
					case <-ctx.Done():
						return
					case results <- job * job:
					}
				}
			}
		}(w + 1)
	}

	// closer
	go func() {
		wg.Wait()
		close(results)
	}()

	// consumer
	for v := range results {
		fmt.Println("result:", v)
	}
	fmt.Println("done:", ctx.Err())
}

 

3. 벤치마크 테스트

환경 :  Apple M2 Pro

매번 고루틴을 생성하는 방식과 워커 풀을 사용하는 방식이 차이가 나는지 측정했을 때 결과 입니다.

테스트 케이스 처리 속도  비고
Unbounded Goroutines 760.1 ns 매 작업마다 고루틴 생성
Worker Pool (Workers=8) 231.1 ns 8개의 워커 생성

 

 

4. 핵심 정리

  • 워커 풀을 써야할 경우
    • 처리해야할 작업이 고정되지 않고 대량으로 데이터가 들어오는 경우
    • 메모리 사용량을 일정하게 유지해 시스템 안정성을 높여야 할 경우
    • DB 커넥션 등 제한된 공유 리소스 및 네트워크를 효율적으로 배분해야할 경우

  • 쓰지 않아야 할 경우
    • 사용자의 요청이 즉각적으로 반응해야하는 경우
      (실시간 채팅 및 알림 서비스, 금융 거래 게이트 웨이 등)
       
    • 작업량이 작아서 풀로 관리하는게 오히려 오버헤드일 경우

5. 전체 코드

(예제코드)

https://github.com/reochoi109/go-handbook/blob/main/patterns/workerpool/basic/main.go

 

go-handbook/patterns/workerpool/basic/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com


(밴치마크 테스트 코드)
https://github.com/reochoi109/go-handbook/blob/main/patterns/workerpool/basic/workerpool_benchmark_test.go

 

go-handbook/patterns/workerpool/basic/workerpool_benchmark_test.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com

 

5. 참고자료

https://goperf.dev/01-common-patterns/worker-pool/

 

Goroutine Worker Pools - Go Optimization Guide

Goroutine Worker Pools in Go Go’s concurrency model makes it deceptively easy to spin up thousands of goroutines—but that ease can come at a cost. Each goroutine starts small, but under load, unbounded concurrency can cause memory usage to spike, conte

goperf.dev

 

'프로그래밍 > golang' 카테고리의 다른 글

[Golang] 고정 메모리 기반 Rate Limit  (0) 2026.03.21
[Golang] Rate Limit 패턴  (0) 2026.03.21
[Golang] flag로 서브커맨드 만들기  (0) 2026.03.21
[Golang] Context란?  (0) 2026.03.21
[Golang] 채널(Channel)이란?  (0) 2026.03.18