IMCAT.DEV
로그인 가입
Go 고급 비동기

Go 비동기 프로그래밍 (Async Programming)

Go 비동기 프로그래밍 (Async Programming) — 고급

비동기 프로그래밍 (Async Programming)

1. 도입 (왜 배워야 하는가)

비동기 프로그래밍은 현대 소프트웨어 개발에서 필수적인 기술입니다. 특히 서버 애플리케이션이나 고성능 시스템을 개발할 때, 비동기 방식은 동시성을 높이고 시스템의 응답성을 개선하는 데 큰 도움이 됩니다. 예를 들어, 웹 서버가 여러 클라이언트의 요청을 동시에 처리해야 할 때, 비동기 프로그래밍을 사용하지 않으면 하나의 요청이 처리되는 동안 다른 요청이 대기하게 되어 전체 시스템이 느려질 수 있습니다.

이러한 비동기 프로그래밍을 모르면, 개발자는 시스템의 성능을 극대화하는 데 필요한 도구를 잃게 되고, 사용자 경험이 나빠질 수 있습니다. 특히 대규모 애플리케이션에서는 비동기 처리를 통해 자원 소모를 줄이고, 더 많은 사용자 요청을 처리할 수 있게 됩니다.

2. 핵심 개념 설명

비동기 프로그래밍은 작업을 동시에 수행할 수 있도록 하는 프로그래밍 스타일입니다. 이를 통해 프로그램은 특정 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속할 수 있습니다.

관련 용어 정리

용어설명
GoroutineGo 언어에서 비동기 작업을 수행하는 경량 스레드
ChannelGoroutine 간의 데이터 통신을 위한 메커니즘
Blocking특정 작업이 완료될 때까지 다른 작업이 멈추는 것
Non-blocking다른 작업을 계속 수행하면서 특정 작업이 완료되기를 기다리는 것

비유하자면, 비동기 프로그래밍은 요리사와 같습니다. 요리사가 한 가지 요리를 만드는 동안, 다른 요리를 동시에 준비하는 것은 비동기 작업입니다. 반면, 모든 요리를 차례대로 만들면 (동기 작업), 시간이 더 오래 걸릴 것입니다.

3. 기본 코드 예제

아래 코드 예제는 Goroutine을 사용하여 비동기 작업을 수행하는 기본적인 사용법을 보여줍니다.

package main

import (
    "fmt"
    "time"
)

func main() {
    go sayHello() // 비동기 호출
    time.Sleep(1  time.Second) // 메인 함수가 종료되지 않도록 대기
    fmt.Println("메인 함수 종료")
}

func sayHello() {
    time.Sleep(500  time.Millisecond) // 0.5초 대기
    fmt.Println("안녕하세요!") // 메시지 출력
}

실행 결과

안녕하세요!
메인 함수 종료

코드 한 줄씩 동작 설명

  1. package main: 프로그램의 메인 패키지를 정의합니다.
  2. import ...: 필요한 패키지를 가져옵니다. 여기서는 fmt와 time을 사용합니다.
  3. func main(): 프로그램의 진입점인 메인 함수를 정의합니다.
  4. go sayHello(): sayHello 함수를 비동기적으로 호출합니다. 이때 Goroutine이 생성됩니다.
  5. time.Sleep(1 * time.Second): 메인 함수가 종료되지 않도록 1초간 대기합니다.
  6. fmt.Println("메인 함수 종료"): 메인 함수에서 종료 메시지를 출력합니다.
  7. func sayHello(): sayHello 함수를 정의합니다.
  8. time.Sleep(500 * time.Millisecond): 0.5초 동안 대기합니다.
  9. fmt.Println("안녕하세요!"): 인사 메시지를 출력합니다.
  10. 4. 실전 활용 예제

    예제 1: 웹 서버에서의 비동기 요청 처리

    쇼핑몰 웹 서버를 구현할 때, 사용자가 상품을 검색하는 요청을 비동기로 처리할 수 있습니다.

    package main
    
    import (
        "fmt"
        "net/http"
    )
    
    func searchProduct(w http.ResponseWriter, r http.Request) {
        go func() {
            // 상품 검색 로직 (가상의 대기 시간)
            time.Sleep(2  time.Second)
            fmt.Fprintln(w, "상품 검색 완료")
        }()
    }
    
    func main() {
        http.HandleFunc("/search", searchProduct)
        http.ListenAndServe(":8080", nil)
    }

    위 코드에서 /search 엔드포인트에 대한 요청이 들어오면, searchProduct 함수가 비동기로 실행되어 웹 서버가 다른 요청을 계속 처리할 수 있습니다.

    예제 2: 데이터베이스 쿼리 비동기 처리

    데이터베이스에서 여러 쿼리를 동시에 실행하여 결과를 처리할 수 있습니다.

    package main
    
    import (
        "database/sql"
        "fmt"
        "log"
        "sync"
    )
    
    func queryDB(wg sync.WaitGroup, query string) {
        defer wg.Done()
        // DB 쿼리 로직 (생략)
        fmt.Println(query, "쿼리 실행 완료")
    }
    
    func main() {
        var wg sync.WaitGroup
        queries := []string{"SELECT  FROM users", "SELECT * FROM orders"}
    
        for _, query := range queries {
            wg.Add(1)
            go queryDB(&wg, query) // 비동기 쿼리 실행
        }
    
        wg.Wait() // 모든 Goroutine이 완료될 때까지 대기
    }

    이 코드는 여러 쿼리를 동시에 실행하여 데이터베이스 성능을 개선합니다.

    5. 흔한 실수와 주의사항

    실수 1: Goroutine 종료 시 메인 함수 종료

    Goroutine이 실행 중에도 메인 함수가 종료되면, Goroutine이 실행되지 않을 수 있습니다.

    • 잘못된 코드:
    • package main
      
      func main() {
          go func() {
              fmt.Println("Goroutine 실행")
          } // 메인 함수 종료
      }
    • 올바른 코드:
    • package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          go func() {
              fmt.Println("Goroutine 실행")
          }()
          time.Sleep(1 * time.Second) // 메인 함수 대기
      }

      실수 2: 채널을 통한 Goroutine 통신 간과

      Goroutine 간의 데이터 통신을 채널 없이 간과하면 데이터 경쟁이 발생할 수 있습니다.

    • 잘못된 코드:
    • package main
      
      import "fmt"
      
      var counter int
      
      func increment() {
          counter++
      }
      
      func main() {
          go increment()
          go increment()
          time.Sleep(1 * time.Second)
          fmt.Println("Counter:", counter)
      }
    • 올바른 코드:
    • package main
      
      import (
          "fmt"
          "sync"
      )
      
      var counter int
      var mu sync.Mutex // 뮤텍스 사용
      
      func increment() {
          mu.Lock()
          counter++
          mu.Unlock()
      }
      
      func main() {
          var wg sync.WaitGroup
          wg.Add(2)
          go func() {
              defer wg.Done()
              increment()
          }()
          go func() {
              defer wg.Done()
              increment()
          }()
          wg.Wait()
          fmt.Println("Counter:", counter)
      }

      6. 정리

    • 비동기 프로그래밍은 현대 애플리케이션에서 필수적인 기술이며, 시스템의 성능과 응답성을 개선합니다.
    • Goroutine과 채널을 통해 비동기 작업을 쉽게 처리할 수 있습니다.
    • 동기적 작업에 비해 비동기 작업은 자원 소모를 줄이고, 더 많은 요청을 처리할 수 있습니다.
    • 다음에 학습하면 좋은 주제: Go에서의 채널 사용법, 에러 처리 및 복구 패턴, 컨텍스트(Context) 패턴.