Go의 Fan-Out/Fan-In 패턴: 유연한 병렬 데이터 처리
Go에서 Fan-Out/Fan-In 패턴은 작업을 여러 고루틴(goroutine)에 분산한 후 결과를 모으는 강력한 방법을 제공합니다. Worker Pool 패턴과 달리 각 워커마다 개별 처리 파이프라인을 사용할 수 있어 복잡한 데이터 변환 체인에 특히 유용합니다.
패턴의 핵심 원리
이 패턴은 두 가지 주요 단계로 나뉩니다. Fan-Out 단계에서는 입력 데이터를 여러 워커(고루틴)에 분산합니다. 각 워커는 독립적으로 작업을 처리한 후 전용 채널로 결과를 전송합니다. Fan-In 단계에서는 모든 워커 채널의 결과를 하나의 출력 채널로 수집해 후속 처리를 진행합니다.
Worker Pool과의 주요 차이점은 채널 구조에 있습니다:
- Worker Pool: 모든 워커가 공유 채널에서 작업을 읽고 결과를 공유 채널에 씁니다.
- Fan-Out/Fan-In: 각 워커마다 전용 출력 채널이 있어 추가 병합(merge) 단계가 필요합니다.
이 접근법은 병합을 위한 추가 고루틴과 채널로 약간의 오버헤드가 발생하지만, 후속 단계에서 별도의 데이터 스트림이 필요한 파이프라인 구조에서 빛을 발합니다.
실제 구현
Worker Pool과 의도적으로 차별화된 실습 구현을 통해 핵심 특징을 살펴보겠습니다.
워커 함수
이 구조에서 워커는 고루틴을 생성하고 결과 채널을 반환하는 함수입니다. 채널을 매개변수로 전달하는 방식과 다릅니다.
func worker(x int, workerID int) chan int {
out := make(chan int)
go func() {
defer close(out)
fmt.Printf("worker %d, job %d\n", workerID, x)
out <- x * x
}()
return out
}
주요 구현 포인트:
- 고루틴은 결과를 전송한 후
defer로out채널을 닫습니다. - 데모 목적으로 간단한 작업(제곱 계산)을 사용했습니다.
- 실제 코드에서는 컨텍스트를 활용해 graceful shutdown과 타임아웃을 관리해야 합니다.
Fan-Out 단계 (분산)
분배자(프로듀서)는 입력 데이터 항목당 하나의 워커를 시작합니다. 워커가 반환한 채널들은 나중 병합을 위해 슬라이스에 저장됩니다.
for i, x := range data {
chnls = append(chnls, worker(x, i+1))
}
주요 특징: 작업 수에 따라 워커 수가 동적으로 확장되며, 이는 필수가 아니지만 패턴의 유연성을 보여줍니다. 실제 앱에서는 동시 워커 수를 제한합니다.
Fan-In 단계와 병합 함수
merge 함수는 여러 채널의 데이터를 하나로 결합하는 핵심 역할을 합니다. 다음을 수행합니다:
- 단일 비버퍼 출력 채널
out을 생성합니다. - 각 입력 채널마다 데이터를 읽어
out으로 전달하는 고루틴을 시작합니다. - 별도 고루틴에서
sync.WaitGroup으로 모든 리더 고루틴 완료를 기다린 후out을 닫습니다.
func merge(chnls ...chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range chnls {
wg.Add(1)
go func(c chan int) {
defer wg.Done()
for x := range c {
out <- x
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
기술 노트: 대기(wg.Wait())는 별도 고루틴에서 실행됩니다. 메인 흐름에서 하면 소비자가 out 읽기를 시작하지 않아 데드락이 발생할 수 있습니다.
소비자
소비자는 단일 병합 채널에서 결과를 읽습니다. for range 루프는 merge가 out을 닫으면 자동 종료됩니다.
for x := range out {
results = append(results, x)
}
주요 요점
- 아키텍처 유연성: 워커별 개별 파이프라인으로 추가 데이터 처리 단계를 쉽게 삽입할 수 있습니다.
- 동적 스케일링: 이론적으로 작업당 워커 하나를 지원하지만, 실제로는 동시성을 제어합니다.
- 오버헤드: 병합을 위한 추가 채널과 고루틴으로 자원이 더 소모됩니다.
- 라이프사이클 관리: 데모에서는 컨텍스트를 생략했지만, 실제로는 취소와 타임아웃 처리가 필수입니다.
- 코드 가독성: 비버퍼 채널과
for range루프로 코드가 간결하고 명확합니다.
프로덕션 추천
이 데모 코드는 학습용입니다. 실제 사용 시 다음을 업그레이드하세요:
- 컨텍스트 추가 (context.Context): 워커와
merge에 전달해 취소, 데드라인, 타임아웃을 처리합니다. - 동시성 제한: 작업당 무제한 고루틴은 자원을 고갈시킬 수 있습니다. 세마포어나 고정 크기 워커 풀을 사용하세요.
- 에러 처리: 워커 에러를 소비자로 전달하는 전용 에러 채널이나 결과 구조체를 도입합니다.
- 버퍼 채널: 일부 경우 블로킹을 줄여 성능을 높일 수 있지만, 신중히 테스트하세요.
Fan-Out/Fan-In 패턴은 Go 개발자의 필수 도구로, 효율적이고 확장 가능한 데이터 처리 파이프라인을 구축하는 데 강력합니다—특히 단계 간 데이터 흐름 관리가 유연해야 할 때요.
— Editorial Team
아직 댓글이 없습니다.