Go 언어 - 동시성 프로그래밍
.
Background
필자는 작년 SW마에스트로 활동을 하면서 처음으로 Terraform을 접하게 되었다. Terraform이란 클라우드 인프라 관리 도구로 이는 Go 언어로 작성되어 있어, 늘 흥미를 가지고 있던 언어 중 하나였다. 관심만 가지고 있다가 최근에 졸업 작품을 진행하면서 Go 언어에 대한 관심이 더욱 깊어졌고, 그 활용 범위가 클라우드 인프라 관리뿐만 아니라 마이크로서비스, 네트워크 프로그래밍, DevOps 도구 개발 등 다양한 분야로 확장되어 있음을 알게 되었다. 이에 Go 언어의 기초부터 실제 활용까지 체계적으로 학습하고 공유하고자 본 포스트를 작성하게 되었다.
Goroutine
고루틴(goroutine)
은 Go 런타임이 관리하는 경량 스레드다. 고루틴은 OS 스레드보다 훨씬 가볍고, 생성과 관리가 쉽다.
func printNumbers() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func printLetters() {
for char := 'a'; char < 'e'; char++ {
time.Sleep(150 * time.Millisecond)
fmt.Printf("%c ", char)
}
}
func main() {
go printNumbers()
go printLetters()
time.Sleep(2 * time.Second)
fmt.Println("\n완료")
}
이 예제에서는 두 개의 고루틴을 생성하여 숫자와 문자를 동시에 출력한다. ‘go’ 키워드를 사용하여 함수를 고루틴으로 실행할 수 있다.
Channel
채널(Channel)
은 고루틴 간의 통신과 동기화를 위한 메커니즘이다. 채널을 통해 데이터를 주고받을 수 있으며, 이는 “통신을 통한 메모리 공유”라는 Go의 철학을 반영한다.
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 채널에 데이터 전송
}
close(ch)
}
func consumer(ch <-chan int) {
for num := range ch {
fmt.Printf("받은 숫자: %d\n", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
이 예제에서는 producer 고루틴이 채널을 통해 숫자를 전송하고, consumer 고루틴이 이를 받아 출력한다.
Concurrency Pattern
Go에서 자주 사용되는 몇 가지 동시성 패턴을 살펴보자.
Fan-out, Fan-in Pattern
여러 고루틴에서 작업을 분산 처리하고 결과를 모으는 패턴이다.
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(cs))
for _, c := range cs {
go func(ch <-chan int) {
for n := range ch {
out <- n
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
in := generator(1, 2, 3, 4)
c1 := square(in)
c2 := square(in)
for n := range merge(c1, c2) {
fmt.Println(n)
}
}
Worker Pool 패턴
작업을 여러 워커에 분산하여 처리하는 패턴이다.
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
<-results
}
}
Context를 이용한 취소 패턴
작업을 취소하거나 타임아웃을 구현할 때 사용하는 패턴이다.
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("작업 취소됨")
return
default:
fmt.Println("작업 중...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go doWork(ctx)
time.Sleep(3 * time.Second)
fmt.Println("메인 함수 종료")
}
Summary
Go의 동시성 프로그래밍 모델은 간단하면서도 강력하다. 고루틴과 채널을 이용하여 복잡한 동시성 문제를 효과적으로 해결할 수 있으며, 다양한 동시성 패턴을 통해 효율적이고 확장 가능한 프로그램을 작성할 수 있다.
다만, 동시성 프로그래밍에는 항상 주의가 필요하며, 데드락이나 레이스 컨디션과 같은 문제를 방지하기 위해 신중하게 설계해야 한다. Go는 이러한 문제를 해결하기 위한 도구(예: -race 플래그)도 제공하므로, 이를 적극 활용하는 것이 좋다.