Go 팬을 활용한 외부 API 호출의 효율적인 오케스트레이션
Olivia Novak
Dev Intern · Leapcell

소개: 멀티 API 데이터 격차 탐색
오늘날 상호 연결된 소프트웨어 환경에서 애플리케이션은 거의 독립적으로 존재하지 않습니다. 대부분의 경우, 기능에 필수적인 다양한 데이터를 가져오기 위해 수많은 외부 API에 의존합니다. 인증 서비스에서 사용자 프로필을 가져오고, 전자상거래 플랫폼에서 주문 내역을 가져오고, 금융 데이터 제공업체에서 실시간 주가를 가져오는 대시보드를 구축한다고 상상해 보세요. 각 API 호출은 필요하지만 지연 시간을 발생시킵니다. 이러한 호출을 순차적으로 하면 애플리케이션 속도가 크게 느려져 응답성이 떨어지는 사용자 경험과 비효율적인 리소스 활용으로 이어집니다. 바로 여기서 동시성의 힘이 중요해집니다. Go는 내장된 고루틴(goroutine)과 채널(channel)을 통해 이러한 문제를 해결하는 우아한 솔루션을 제공합니다. 이 글에서는 "팬인, 팬아웃" 패턴에 대해 자세히 알아보고, 이를 활용하여 여러 외부 API에서 데이터를 동시에 처리하여 성능과 응답성을 크게 향상시키는 방법을 시연할 것입니다.
우리의 여정은 이 강력한 패턴의 핵심 개념을 탐구하고, 실용적인 Go 코드 예제를 제공하며, 강력하고 확장 가능한 시스템을 구축하는 데 있어 실제 적용 가능성을 강조할 것입니다.
팬인, 팬아웃 패턴 해독
구현 세부 사항을 살펴보기 전에 팬인, 팬아웃 패턴에 관련된 핵심 개념에 대한 공통된 이해를 정립해 보겠습니다.
- 고루틴(Goroutines): Go에서 경량의 독립적인 실행 단위입니다. 함수를 동시에 실행할 수 있게 하여 기존 스레드의 오버헤드 없이 여러 작업을 동시에 수행할 수 있습니다.
- 채널(Channels): 고루틴이 값을 주고받을 수 있는 타입 지정된 통로입니다. 고루틴 간의 안전하고 동기화된 통신을 제공하여 경쟁 조건을 방지하고 동시 프로그래밍을 단순화합니다.
- 팬아웃(Fan-Out): 단일 작업 또는 입력을 여러 작업자 고루틴으로 분배하는 기술입니다. 우리 맥락에서는 각기 독립적인 API 호출을 담당하는 여러 고루틴을 시작하는 것을 의미합니다.
- 팬인(Fan-In): 팬아웃의 반대입니다. 여러 작업자 고루틴의 결과를 단일 채널로 집계하는 것을 포함합니다. 여기서는 모든 API 호출 고루틴의 응답을 추가 처리를 위해 통합된 스트림으로 수집하는 것을 의미합니다.
순차적 API 호출의 문제점
세 개의 외부 API를 호출해야 하는 시나리오를 생각해 보세요. 각 호출이 1초 걸린다면, 순차적인 접근 방식은 총 3초가 걸릴 것입니다.
package main import ( "fmt" time "time" ) func fetchDataFromAPI1() string { time.Sleep(1 * time.Second) // API 호출 지연 시간 시뮬레이션 return "Data from API 1" } func fetchDataFromAPI2() string { time.Sleep(1 * time.Second) // API 호출 지연 시간 시뮬레이션 return "Data from API 2" } func fetchDataFromAPI3() string { time.Sleep(1 * time.Second) // API 호출 지연 시간 시뮬레이션 return "Data from API 3" } func sequentialAPICalls() { start := time.Now() res1 := fetchDataFromAPI1() res2 := fetchDataFromAPI2() res3 := fetchDataFromAPI3() fmt.Println(res1) fmt.Println(res2) fmt.Println(res3) fmt.Printf("Sequential calls took: %v\n", time.Since(start)) } func main() { fmt.Println("Running sequential API calls:") sequentialAPICalls() }
이 출력은 총 실행 시간이 약 3초임을 보여줄 것입니다. API 호출이 독립적일 때 명확히 비효율적입니다.
팬아웃(Fan-Out) 구현: 동시 API 요청
"팬아웃" 단계는 각 외부 API 호출을 담당하는 여러 고루틴을 시작하는 것을 포함합니다. 각 고루틴은 전용 출력 채널로 결과를 보냅니다.
package main import ( "fmt" "sync" time "time" ) // 외부 API 호출 시뮬레이션 func callAPI(apiName string, delay time.Duration) <-chan string { out := make(chan string) go func() { defer close(out) // 고루틴 완료 시 채널이 닫혔는지 확인 fmt.Printf("Calling %s...\n", apiName) time.Sleep(delay) // 네트워크 지연 시간 시뮬레이션 out <- fmt.Sprintf("Data from %s (took %v)", apiName, delay) }() return out } func main() { fmt.Println("Starting Fan-Out stage...") // 팬아웃: 여러 고루틴을 시작하여 다른 API를 호출합니다. // 각 API 호출 함수는 결과를 받기 위한 채널을 반환합니다. api1Channel := callAPI("API Service A", 2*time.Second) api2Channel := callAPI("API Service B", 1*time.Second) api3Channel := callAPI("API Service C", 3*time.Second) fmt.Println("\nAPI calls fanned out. Now waiting for results (Fan-In)...") // 팬인 부분은 다음에 구현될 것입니다. // 현재로서는 개별 채널을 비워서 동시 효과를 보겠습니다. // 이것은 아직 진정한 팬인이 아니지만, 독립적인 실행을 시연합니다. fmt.Println(<-api1Channel) fmt.Println(<-api2Channel) fmt.Println(<-api3Channel) fmt.Println("\nAll API results received individually.") }
이 main 함수를 실행하면 "Calling API Service A...", "Calling API Service B...", "Calling API Service C..."가 거의 동시에 나타나는 것을 볼 수 있습니다. 프로그램은 결과를 기다리고, 각 시뮬레이션된 API 호출이 완료될 때마다 결과가 도착하여 동시성을 보여줍니다.
팬인(Fan-In) 구현: 결과 집계
"팬인" 단계는 모든 개별 API 호출의 결과를 단일 채널로 통합하는 곳입니다. 이를 통해 단일 소비자가 사용 가능한 대로 모든 결과를 처리할 수 있습니다.
package main import ( "fmt" "sync" time "time" ) // 외부 API 호출 시뮬레이션 func callAPI(apiName string, delay time.Duration) <-chan string { out := make(chan string) go func() { defer close(out) fmt.Printf("Calling %s...\n", apiName) time.Sleep(delay) out <- fmt.Sprintf("Data from %s (took %v)", apiName, delay) }() return out } // 여러 입력 채널을 받아 단일 출력 채널로 다중화합니다. func fanIn(inputChans ...<-chan string) <-chan string { var wg sync.WaitGroup multiplexedChan := make(chan string) // 입력 채널에서 읽고 다중화된 채널로 보내는 함수 multiplex := func(c <-chan string) { defer wg.Done() for val := range c { multiplexedChan <- val } } // 각 입력 채널에 대해 WaitGroup에 고루틴을 추가합니다. wg.Add(len(inputChans)) for _, c := range inputChans { go multiplex(c) } // 모든 입력 채널이 닫힌 후 다중화된 채널을 닫기 위한 고루틴을 시작합니다. go func() { wg.Wait() // 모든 다중화 고루틴이 완료될 때까지 기다립니다. close(multiplexedChan) }() return multiplexedChan } func main() { start := time.Now() fmt.Println("Starting concurrent API calls with Fan-Out and Fan-In...") // 팬아웃: 각 API 호출에 대한 고루틴을 시작합니다. api1Chan := callAPI("API Service A", 2*time.Second) api2Chan := callAPI("API Service B", 1*time.Second) api3Chan := callAPI("API Service C", 3*time.Second) // 팬인: 모든 API 채널의 결과를 단일 채널로 집계합니다. unifiedResults := fanIn(api1Chan, api2Chan, api3Chan) // 통합 채널에서 오는 대로 결과를 처리합니다. for result := range unifiedResults { fmt.Printf("Received: %s\n", result) } fmt.Printf("All concurrent calls completed in: %v\n", time.Since(start)) }
이 향상된 예제를 실행하면 순차적인 접근 방식에 비해 상당한 성능 향상을 볼 수 있습니다. 총 실행 시간은 모든 호출의 합이 아니라 가장 느린 API 호출(API 서비스 C, 3초 소요)에 의해 결정됩니다. 출력에서는 API 호출이 완료되는 대로 메시지가 도착함을 보여주며, 동시 처리를 시연합니다.
fanIn 함수는 팬인 단계의 핵심입니다. 여러 입력 채널(API 호출 결과)을 받아 단일 multiplexedChan을 생성합니다. 각 입력 채널에 대해 입력 채널에서 지속적으로 읽고 받은 값을 multiplexedChan으로 보내는 multiplex 고루틴을 생성합니다. sync.WaitGroup은 모든 입력 채널이 완전히 비워지고 해당 multiplex 고루틴이 완료된 후에만 multiplexedChan이 닫히도록 보장합니다.
실제 적용 사례 및 이점
팬인, 팬아웃 패턴은 다양한 시나리오에 매우 유용하며 적용 가능합니다.
- 데이터 집계: 여러 마이크로서비스 또는 외부 데이터 소스에서 데이터를 결합하여 복합 보기를 구축합니다.
- 병렬 처리: 대규모 계산 작업을 작고 독립적인 하위 작업으로 분산하여 동시에 실행할 수 있습니다. 예를 들어, 대규모 파일의 세그먼트 처리 또는 다른 데이터 세트에 대한 별도 분석 수행.
- 워크플로우 오케스트레이션: 다음 단계로 진행하기 전에 여러 작업의 결과를 수집해야 하는 비동기 작업을 조정합니다.
- 실시간 대시보드: 다양한 실시간 피드(예: 주식 시장, 센서 데이터)에서 지속적으로 데이터를 가져오고 업데이트하여 단일 인터페이스에 표시합니다.
- 검색 엔진: 여러 인덱스 또는 데이터 소스를 병렬로 쿼리하여 포괄적인 결과를 신속하게 수집합니다.
이 패턴의 주요 이점은 다음과 같습니다.
- 성능 향상: 독립적인 작업을 동시에 실행함으로써 전체 실행 시간이 크게 단축됩니다.
- 확장성 증가: 이 패턴은 더 많은 API 호출이나 처리 작업을 쉽게 수용할 수 있으며, 단순히 더 많은 고루틴을 시작하면 됩니다.
- 디커플링: 각 API 호출 또는 처리 단위는 독립적이므로 시스템이 더욱 모듈화되고 유지 관리하기 쉬워집니다.
- 탄력성: 한 API 호출의 실패는 격리될 수 있으며, 재시도 또는 대체 메커니즘과 같은 전략은 다른 API를 차단하지 않고 각 API별로 구현할 수 있습니다.
결론: Go의 동시성을 활용한 확장 가능한 데이터 처리
Go의 고루틴과 채널로 지원되는 팬인, 팬아웃 패턴은 여러 외부 API에서 데이터를 동시 처리하는 우아하고 매우 효과적인 접근 방식을 제공합니다. 독립적인 작업을 전략적으로 팬아웃하고 결과를 팬인함으로써 개발자는 애플리케이션 성능을 극적으로 향상시키고, 확장성을 향상시키며, 더욱 강력하고 반응성이 뛰어난 시스템을 구축할 수 있습니다. 이 패턴은 Go의 간단하고 강력한 동시성 철학을 구현하여 개발자가 복잡한 데이터 흐름을 쉽게 오케스트레이션할 수 있도록 합니다.
API 중심 세계에서 Go 애플리케이션의 잠재력을 최대한 활용하려면 이 패턴을 채택하세요.

