본문 바로가기

Programming/Go

Go의 동시성 패턴

『Go in Action』의 내용을 기반으로 학습 및 정리했습니다.


 

동시성 패턴

Go에서는 고루틴과 채널을 이용해 다양한 동시성 패턴을 구현할 수 있다. 이번 장에서는 채널을 활용한 대표적인 패턴인 Runner, Pool, Work 패턴을 정리한다.


Runner

패키지 runner

채널을 이용하여 프로그램의 실행 시간을 관찰하고 프로그램이 너무 오래 실행되면 프로그램을 종료한다. 또한 운영체제로부터 인터럽트 신호를 수신하여 실행 중인 작업을 중단할 수도 있다.

배치 프로그램이나 백그라운드 작업처럼 일정 시간 안에 종료되어야 하는 프로세스를 작성할 때 유용한 패턴이다.

 
package runner
 
 
type Runner struct {
    interrupt chan os.Signal

    complete chan error

    timeout <-chan time.Time

    tasks []func(int)
}
 

채널

Runner는 세 개의 채널을 사용한다.

interrupt

os.Signal 타입의 값을 전달받는 채널이다.

운영체제로부터 전달되는 인터럽트 신호(SIGINT, SIGTERM 등)를 수신하기 위해 사용된다. 사용자가 Ctrl + C를 입력하거나 프로세스 종료 요청이 발생했을 때 해당 채널을 통해 이벤트를 전달받는다.

complete

작업이 모두 종료되었음을 알리기 위한 채널이다.

Runner는 별도의 고루틴에서 작업을 수행한 뒤 결과를 complete 채널로 전달한다. 메인 루틴은 이 채널을 감시하면서 작업 종료 여부를 확인한다.

timeout

지정된 시간 초과를 알리기 위한 채널이다.

일반적으로 time.After()를 통해 생성되며, 설정된 시간이 지나면 채널로 값이 전달된다. Runner는 이를 통해 작업이 허용된 시간을 초과했는지 판단한다.

tasks

순차적으로 실행할 작업 목록이다.

 
tasks []func(int)
 

각 함수는 자신의 실행 순서를 인자로 전달받는다.

Runner는 여러 작업을 동시에 실행하는 것이 아니라 등록된 작업을 순서대로 수행하며, 실행 중 인터럽트나 타임아웃이 발생하는지 지속적으로 감시한다.

특징

Runner 패턴의 핵심은 여러 이벤트를 동시에 기다리는 것이다.

  • 작업 정상 종료
  • 타임아웃 발생
  • 운영체제 인터럽트 발생

Go에서는 주로 select 문을 이용하여 이러한 이벤트를 처리한다.

 


Pool

패키지 pool

 
package pool
 

버퍼가 있는 채널을 이용하여 공유 가능한 리소스의 풀(Pool)을 생성하고, 여러 고루틴이 해당 리소스를 재사용할 수 있도록 한다.

실제 서비스에서는 다음과 같은 객체를 풀링하는 경우가 많다.

  • 데이터베이스 연결
  • HTTP 연결
  • 파일 핸들
  • 메모리 버퍼

고루틴이 공유 리소스를 사용해야 하는 경우, 새로운 객체를 생성하는 대신 풀에서 리소스를 할당받아 사용하고 다시 반환하는 방식으로 동작한다.

매번 생성과 해제를 반복하는 비용을 줄일 수 있기 때문에 성능과 자원 효율성 측면에서 유리하다.

 
import (
    "io"
    "log"
    "sync"
)

type Pool struct {
    m sync.Mutex

    resources chan io.Closer

    factory func() (io.Closer, error)

    closed bool
}
 

m

sync.Mutex 타입의 필드이다.

여러 고루틴이 동시에 Pool에 접근할 수 있기 때문에 동기화가 필요하다. Pool의 상태를 변경하거나 종료 여부를 확인하는 과정에서 Race Condition이 발생하지 않도록 보호한다.

resources

io.Closer 타입의 버퍼가 있는 채널이다.

공유할 리소스를 저장하는 공간으로 사용된다.

채널의 버퍼 크기가 곧 Pool이 보관할 수 있는 최대 리소스 수가 된다.

 
resources chan io.Closer
 

Pool 패턴에서 채널은 단순한 통신 수단이 아니라 리소스 저장소 역할도 수행한다.

factory

리소스를 생성하기 위한 함수이다.

 
factory func() (io.Closer, error)
 

Pool 내부에 사용 가능한 리소스가 없을 경우 해당 함수를 호출하여 새로운 리소스를 생성한다.

예를 들어 데이터베이스 커넥션 풀이라면 새로운 DB 연결을 생성하는 함수가 된다.

closed

Pool이 종료되었는지 여부를 나타내는 플래그이다.

Pool이 종료된 이후에는 새로운 리소스 요청이나 반환이 발생하지 않도록 제어하기 위해 사용한다.

특징

Pool 패턴의 핵심은 생성 비용이 큰 객체를 재사용한다는 점이다.

예를 들어 데이터베이스 연결은 생성 비용이 비싸기 때문에 요청마다 생성하는 대신 미리 생성된 연결을 재사용하는 것이 일반적이다.

실제로 Go의 database/sql 패키지 역시 내부적으로 커넥션 풀을 사용한다.

 


Work

패키지 work

 
package work
 

버퍼가 없는 채널을 이용하여 원하는 개수만큼의 작업을 동시적으로 실행할 수 있는 고루틴 풀(Worker Pool)을 생성한다.

작업이 많이 발생한다고 해서 그 수만큼 고루틴을 생성하는 것은 좋은 방법이 아니다.

예를 들어 10만 개의 작업이 발생했을 때 10만 개의 고루틴을 생성하면 메모리 사용량 증가, 스케줄링 비용 증가 등의 문제가 발생할 수 있다.

Work 패턴은 미리 정해진 수의 Worker를 생성하고 작업을 분배하여 처리한다.

Worker Pool

구조를 단순화하면 다음과 같다.

Job Queue
    │
    ▼
 Worker 1
 Worker 2
 Worker 3
 Worker 4
 

작업은 큐에 들어가고, 대기 중인 Worker가 하나씩 가져가 처리한다.

이 방식은 동시에 실행되는 작업 수를 제한할 수 있다는 장점이 있다.

버퍼가 없는 채널을 사용하는 이유

작업 큐를 생성할 때는 임의 크기의 버퍼 채널보다 버퍼가 없는 채널을 사용하는 편이 더 적합할 수 있다.

버퍼가 없는 채널은 송신자와 수신자가 동시에 준비되어 있어야 데이터 전달이 이루어진다.

즉, Worker가 실제로 작업을 가져가는 시점을 정확하게 알 수 있다.

반대로 버퍼 채널은 작업이 큐에 계속 쌓일 수 있기 때문에 실제 처리량보다 더 많은 작업이 대기 상태로 저장될 수 있다.

특징

Work 패턴의 핵심은 작업 수를 제한하는 것이 아니라 동시에 실행되는 작업 수를 제한하는 것이다.

예를 들어 Worker를 10개 생성했다면,

  • 작업이 100개 들어와도
  • 작업이 1000개 들어와도

동시에 실행되는 작업은 최대 10개로 유지된다.

이러한 특성 덕분에 서버, 크롤러, 메시지 처리기, 이벤트 소비자(Consumer) 등에서 널리 사용되는 패턴이다.

'Programming > Go' 카테고리의 다른 글

Go 런타임 스케줄러와 동시성 모델  (1) 2026.06.07
Go의 타입 시스템  (0) 2026.06.07
Package와 Go의 데이터 컬렉션  (0) 2026.05.31
Go의 특징과 핵심 살펴보기  (0) 2026.05.24