# Go 동시성: 고루틴과 채널 - 완벽 가이드 > 고루틴과 채널을 사용해 Go 동시성을 마스터하세요. 고급 패턴, 동기화, select 문, 모범 사례를 자세한 코드 예제와 함께 다룹니다. - Published: 2026-02-21 - Updated: 2026-04-29 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- 동시성은 Go의 가장 큰 강점 중 하나입니다. 멀티스레딩이 복잡하게 남아있는 다른 언어와 달리, Go는 고루틴과 채널을 기반으로 한 우아한 모델을 제공하여 동시성 애플리케이션 개발을 크게 단순화합니다. > **Go 철학** > > "메모리를 공유하여 통신하지 말고, 통신하여 메모리를 공유하세요." 이 근본 원칙은 Go의 모든 동시성 설계를 안내합니다. ## 고루틴 이해하기 고루틴은 Go 런타임이 관리하는 경량 스레드입니다. 약 2 KB의 스택을 소비하며(OS 스레드의 수 MB와 비교) 시스템 과부하 없이 수천 개의 동시 작업을 실행할 수 있습니다. 고루틴을 시작하려면 함수 호출 앞에 `go` 키워드를 두기만 하면 됩니다. 런타임이 사용 가능한 스레드 간의 스케줄링과 분배를 처리합니다. ```go // goroutines_basic.go package main import ( "fmt" "time" ) // fetchData simulates a network request func fetchData(id int) { // Simulates network delay time.Sleep(100 * time.Millisecond) fmt.Printf("Data %d fetched\n", id) } func main() { // Sequential execution - 500ms total start := time.Now() for i := 1; i <= 5; i++ { fetchData(i) } fmt.Printf("Sequential: %v\n", time.Since(start)) // Concurrent execution - ~100ms total start = time.Now() for i := 1; i <= 5; i++ { go fetchData(i) // Execute as goroutine } time.Sleep(150 * time.Millisecond) // Wait for completion fmt.Printf("Concurrent: %v\n", time.Since(start)) } ``` 동시 실행은 총 시간을 500ms에서 약 100ms로 줄입니다. 그러나 고루틴 동기화에 `time.Sleep`을 사용하는 것은 모범 사례가 아닙니다. 채널은 우아한 해결책을 제공합니다. ## 채널: 고루틴 간 통신 채널은 고루틴 간에 값을 보내고 받기 위한 타입이 지정된 도관입니다. 채널은 동기화를 보장합니다. 보내는 고루틴은 다른 고루틴이 받을 때까지 기다리며, 그 반대도 마찬가지입니다. 채널 생성에는 `make` 함수를 사용합니다. `<-` 연산자는 채널에 대한 위치에 따라 데이터를 보내고 받습니다. ```go // channels_basic.go package main import "fmt" // worker performs computation and returns result via channel func worker(id int, jobs <-chan int, results chan<- int) { // Receives jobs until channel closes for job := range jobs { result := job * 2 // Processing results <- result // Send result } } func main() { // Create channels jobs := make(chan int, 10) // Buffered channel results := make(chan int, 10) // Start 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Send 5 jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // Signal end of jobs // Collect results for r := 1; r <= 5; r++ { result := <-results fmt.Printf("Result: %d\n", result) } } ``` 방향성 채널(수신용 `<-chan`, 송신용 `chan<-`)은 가능한 작업을 제한하여 코드 안전성을 강화합니다. ## 버퍼드 vs 언버퍼드 채널 이 채널 유형 간의 구분은 고루틴 간의 동기화 동작에 직접적인 영향을 미칩니다. 언버퍼드 채널은 수신자가 준비될 때까지 송신자를 차단합니다. 버퍼드 채널은 N이 버퍼 용량을 나타내는 경우 차단 없이 최대 N개의 값을 보낼 수 있게 합니다. ```go // buffered_channels.go package main import "fmt" func main() { // Unbuffered channel - strict synchronization unbuffered := make(chan string) go func() { unbuffered <- "message" // Blocks until received }() msg := <-unbuffered // Unblocks the send fmt.Println(msg) // Buffered channel - capacity of 2 buffered := make(chan int, 2) // These sends don't block buffered <- 1 buffered <- 2 // buffered <- 3 // Would block because buffer is full fmt.Println(<-buffered) // 1 fmt.Println(<-buffered) // 2 // Check capacity fmt.Printf("Length: %d, Capacity: %d\n", len(buffered), cap(buffered)) } ``` 버퍼드 채널은 생산자와 소비자를 분리하는 반면, 언버퍼드는 점대점 동기화를 보장합니다. > **데드락 주의** > > 데드락은 모든 고루틴이 대기 상태로 차단될 때 발생합니다. Go 런타임은 이를 감지하고 명시적인 오류 메시지와 함께 프로그램을 종료합니다. ## Select: 채널 다중화 `select` 문은 여러 채널에 대한 동시 작업을 기다립니다. 동시 통신을 위한 switch 문과 유사합니다. 이 구조는 단일 채널에서 무기한 차단되지 않고 타임아웃, 취소 및 여러 통신을 관리하는 데 필수적입니다. ```go // select_example.go package main import ( "fmt" "time" ) // fetchAPI simulates an API call with variable delay func fetchAPI(name string, delay time.Duration, ch chan<- string) { time.Sleep(delay) ch <- fmt.Sprintf("%s: data received", name) } func main() { api1 := make(chan string) api2 := make(chan string) // Launch two API calls in parallel go fetchAPI("API-1", 100*time.Millisecond, api1) go fetchAPI("API-2", 200*time.Millisecond, api2) // Global timeout of 150ms timeout := time.After(150 * time.Millisecond) // Collect results with timeout for i := 0; i < 2; i++ { select { case result := <-api1: fmt.Println(result) case result := <-api2: fmt.Println(result) case <-timeout: fmt.Println("Timeout - operation cancelled") return } } } ``` `select`는 첫 번째로 준비된 채널을 선택합니다. 여러 개가 준비된 경우, 기아 상태를 방지하기 위해 선택은 의사 무작위입니다. ## Worker Pool 패턴 Worker pool 패턴은 여러 워커 간에 작업을 분배하여 동시성을 제한하고 리소스 사용을 최적화합니다. 이 패턴은 대량의 데이터를 처리하는 데 필수적임이 입증되었습니다. 구현은 워커 간에 공유되는 작업 채널과 수집을 위한 결과 채널에 의존합니다. ```go // worker_pool.go package main import ( "fmt" "sync" "time" ) // Task represents a unit of work type Task struct { ID int Data string } // Result contains the processing result type Result struct { TaskID int Output string } // worker processes received tasks func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for task := range tasks { // Simulate processing time.Sleep(50 * time.Millisecond) results <- Result{ TaskID: task.ID, Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data), } } } func main() { const numWorkers = 3 const numTasks = 10 tasks := make(chan Task, numTasks) results := make(chan Result, numTasks) var wg sync.WaitGroup // Start workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, tasks, results, &wg) } // Send tasks for i := 1; i <= numTasks; i++ { tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)} } close(tasks) // Close results channel after workers finish go func() { wg.Wait() close(results) }() // Collect results for result := range results { fmt.Printf("Task %d: %s\n", result.TaskID, result.Output) } } ``` `sync.WaitGroup`은 결과 채널을 닫기 전에 모든 워커가 완료되기를 기다리는 것을 조정합니다. ## Fan-Out/Fan-In 패턴 이 패턴은 여러 고루틴에 작업을 분배하고(fan-out) 결과를 집계합니다(fan-in). 결과 수집을 단순화하면서 병렬성을 최대화합니다. ```go // fan_out_fan_in.go package main import ( "fmt" "sync" ) // generate produces numbers on a channel func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square computes the square of received numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // merge combines multiple channels into one (fan-in) func merge(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup // Output function for each channel output := func(c <-chan int) { defer wg.Done() for n := range c { out <- n } } // Launch a goroutine per channel wg.Add(len(channels)) for _, c := range channels { go output(c) } // Close after all goroutines finish go func() { wg.Wait() close(out) }() return out } func main() { // Generate data numbers := generate(1, 2, 3, 4, 5, 6, 7, 8) // Fan-out: distribute to 3 workers sq1 := square(numbers) sq2 := square(numbers) sq3 := square(numbers) // Fan-in: aggregate results for result := range merge(sq1, sq2, sq3) { fmt.Println(result) } } ``` 이 패턴은 분배 가능한 CPU 바인딩 작업과 데이터 처리 파이프라인에 탁월합니다. ## 취소 및 데드라인을 위한 Context `context` 패키지는 고루틴 간의 취소, 데드라인 및 값 관리를 표준화합니다. 장기 실행 고루틴은 첫 번째 매개변수로 context를 받아들여야 합니다. ```go // context_example.go package main import ( "context" "fmt" "time" ) // fetchWithContext simulates a cancellable request func fetchWithContext(ctx context.Context, url string) (string, error) { // Simulates a long operation select { case <-time.After(2 * time.Second): return fmt.Sprintf("Data from %s", url), nil case <-ctx.Done(): return "", ctx.Err() // context.Canceled or context.DeadlineExceeded } } func main() { // Context with 500ms timeout ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // Release resources result, err := fetchWithContext(ctx, "https://api.example.com") if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Println(result) // Context with manual cancellation ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(100 * time.Millisecond) cancel2() // Explicit cancellation }() result, err = fetchWithContext(ctx2, "https://api2.example.com") if err != nil { fmt.Printf("Request cancelled: %v\n", err) } } ``` > **모범 사례** > > 리소스 누수를 방지하기 위해 context를 생성한 직후에 항상 `defer cancel()`을 호출하십시오. ## sync.Mutex를 사용한 동기화 채널이 통신에 선호되지만, 공유 데이터 구조에 대한 동시 접근을 보호하기 위해 `sync` 패키지가 여전히 필요합니다. ```go // mutex_example.go package main import ( "fmt" "sync" ) // SafeCounter is a thread-safe counter type SafeCounter struct { mu sync.Mutex value map[string]int } // Increment increments the value for a given key func (c *SafeCounter) Increment(key string) { c.mu.Lock() // Exclusive lock defer c.mu.Unlock() // Guaranteed unlock c.value[key]++ } // Value returns the current value func (c *SafeCounter) Value(key string) int { c.mu.Lock() defer c.mu.Unlock() return c.value[key] } func main() { counter := SafeCounter{value: make(map[string]int)} var wg sync.WaitGroup // 1000 concurrent increments for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment("visits") }() } wg.Wait() fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000 } ``` `sync.RWMutex`는 읽기 전용 작업을 위한 `RLock()`/`RUnlock()`로 동시 읽기를 최적화합니다. ## 일반적인 실수와 해결책 Go 동시성에는 고전적인 함정이 있습니다. 가장 일반적인 오류와 이를 피하는 방법은 다음과 같습니다. ```go // common_mistakes.go package main import ( "fmt" "sync" ) func main() { // ERROR: Loop variable capture // All goroutines would print the same value for i := 0; i < 3; i++ { go func() { fmt.Println(i) // Capture by reference - BUG }() } // SOLUTION: Pass value as parameter var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println(n) // Local copy - CORRECT }(i) } wg.Wait() // ERROR: Send on nil channel var ch chan int // ch <- 1 // Blocks forever // SOLUTION: Always initialize with make ch = make(chan int, 1) ch <- 1 fmt.Println(<-ch) // ERROR: Send on closed channel done := make(chan bool) close(done) // done <- true // Panic! // SOLUTION: Check before send or use sync.Once select { case done <- true: fmt.Println("Sent") default: fmt.Println("Channel closed or full") } } ``` 데이터 레이스 감지는 컴파일이나 테스트 중에 `-race` 플래그를 사용합니다: `go test -race ./...`. ## 결론 Go 동시성을 마스터하는 것은 잘 이해되면 고성능 애플리케이션을 구축할 수 있는 몇 가지 핵심 개념에 의존합니다. **핵심 사항:** ✅ 고루틴은 가볍고 저렴합니다 - 수천 개를 생성해도 허용 가능합니다 ✅ 채널은 고루틴 간에 데이터를 동기화하고 전송합니다 ✅ `select` 문은 여러 통신과 타임아웃을 관리합니다 ✅ Worker pool 패턴은 동시성을 제한하고 리소스를 최적화합니다 ✅ `context` 패키지는 취소와 데드라인을 표준화합니다 ✅ 채널이 부족할 때 뮤텍스는 공유 데이터를 보호합니다 ✅ `-race` 플래그는 테스트 중에 데이터 레이스를 감지합니다 "통신하여 메모리를 공유하라"는 철학은 잠금이 있는 전통적인 멀티스레딩보다 더 안전하고 유지 관리하기 쉬운 설계로 이끕니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/go/go-concurrency-goroutines-channels