학습
Go 비동기 프로그래밍 (Async Programming)
Go 비동기 프로그래밍 (Async Programming) — 고급
비동기 프로그래밍 (Async Programming)
1. 도입 (왜 배워야 하는가)
비동기 프로그래밍은 현대 소프트웨어 개발에서 필수적인 기술입니다. 특히 서버 애플리케이션이나 고성능 시스템을 개발할 때, 비동기 방식은 동시성을 높이고 시스템의 응답성을 개선하는 데 큰 도움이 됩니다. 예를 들어, 웹 서버가 여러 클라이언트의 요청을 동시에 처리해야 할 때, 비동기 프로그래밍을 사용하지 않으면 하나의 요청이 처리되는 동안 다른 요청이 대기하게 되어 전체 시스템이 느려질 수 있습니다.
이러한 비동기 프로그래밍을 모르면, 개발자는 시스템의 성능을 극대화하는 데 필요한 도구를 잃게 되고, 사용자 경험이 나빠질 수 있습니다. 특히 대규모 애플리케이션에서는 비동기 처리를 통해 자원 소모를 줄이고, 더 많은 사용자 요청을 처리할 수 있게 됩니다.
2. 핵심 개념 설명
비동기 프로그래밍은 작업을 동시에 수행할 수 있도록 하는 프로그래밍 스타일입니다. 이를 통해 프로그램은 특정 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속할 수 있습니다.
관련 용어 정리
| 용어 | 설명 |
|---|---|
| Goroutine | Go 언어에서 비동기 작업을 수행하는 경량 스레드 |
| Channel | Goroutine 간의 데이터 통신을 위한 메커니즘 |
| 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("안녕하세요!") // 메시지 출력
}
실행 결과
안녕하세요!
메인 함수 종료
코드 한 줄씩 동작 설명
package main: 프로그램의 메인 패키지를 정의합니다.import ...: 필요한 패키지를 가져옵니다. 여기서는fmt와time을 사용합니다.func main(): 프로그램의 진입점인 메인 함수를 정의합니다.go sayHello():sayHello함수를 비동기적으로 호출합니다. 이때Goroutine이 생성됩니다.time.Sleep(1 * time.Second): 메인 함수가 종료되지 않도록 1초간 대기합니다.fmt.Println("메인 함수 종료"): 메인 함수에서 종료 메시지를 출력합니다.func sayHello():sayHello함수를 정의합니다.time.Sleep(500 * time.Millisecond): 0.5초 동안 대기합니다.fmt.Println("안녕하세요!"): 인사 메시지를 출력합니다.- 잘못된 코드:
- 올바른 코드:
- 잘못된 코드:
- 올바른 코드:
- 비동기 프로그래밍은 현대 애플리케이션에서 필수적인 기술이며, 시스템의 성능과 응답성을 개선합니다.
- Goroutine과 채널을 통해 비동기 작업을 쉽게 처리할 수 있습니다.
- 동기적 작업에 비해 비동기 작업은 자원 소모를 줄이고, 더 많은 요청을 처리할 수 있습니다.
- 다음에 학습하면 좋은 주제: Go에서의 채널 사용법, 에러 처리 및 복구 패턴, 컨텍스트(Context) 패턴.
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)
}