golang GO routine channel 테스트

2023. 1. 24. 22:35Development

요즘 핫하다는 golang, 과거에는 Android app개발을 주로 해왔던 터라, Java를 사용하다가 근래에 주로 사용하는 언어가 Python으로 변경이 되면서 초반에는 심플한 문법... 파이써닉하다. 라는 표현에 매료되어 찬양할 수 밖에 없는 경험을 해왔습니다. 하지만 사용하면 할수록 단점들도 보이기 시작했습니다. 물론 단점이 있다고 나쁘다는 건 아닙니다. 단점이 없는 언어는 없을 것이기에... 넘쳐나는 다양한 Module들과 이로인해 간단한 기능을 수행하기 위한 코드를 작성하는 데는 아직도 Python이 가장 먼저 떠오릅니다. 하지만 좀 더 복잡도 있고, 안정적인 프로그램을 만들어야하는 상황이 되면 Python을 택하는 데 조금 고민이 되기 시작했습니다.

 

Type... 이건 Python에게 양날의 검이라고 할 수 있는 것 같습니다.

 

최초 Python을 접했을 땐, Type에 대한 고민을 하지 않는 것이 너무도 편했어요. Function pointer 같은 복잡한 Type도 그냥 알아서 판단을 해주기에 코드양도 많이 줄어들고, 자잘한 실수로 컴파일 에러를 볼일이 없어지는 것에 굉장히 만족도가 높았습니다.

 

하지만 누가 그랬던 가... 고통은 축복이라고... 컴파일러가 미리 잡아주던 Syntax 문제를 런타임에 몇번 맞고 나면서... 미리 맞는 고통에 감사하게 되었습니다. 물론 Python도 컴파일을 할 수 있는 기능도 있고, Unit test로 커버리지를 많이 끌어올리면 런타임에 도달하기 전에 대부분의 관련 에러를 잡을 수는 있을 것이다.
그렇지만 이러한 경험들은 역설적으로 Type strict한 언어가 가끔은 그리워지는 사건이 되었다.

그러던 시기에 Backend를 개발할 기회가 생겼는 데, 언어에 대한 고민을 하게 되면서 새로운 언어를 또 접해보고자 하는 마음으로 GO에 관심을 갖게 되었습니다.

 

기본 문법을 부터 봐가면서 느낀건... 이 언어는 정말 체계적으로 만들어졌구나, 그리고 저는 감히 GO를 이렇게 표현하고 싶어졌습니다.

 

Programing language 계의 한글, Python을 닮고싶은 C... (쓸데 없는 소리는 이제 그만하고... )

GO 언어를 조금씩 사용해가며, 이해도를 높혀가며 좀 더 심도 있게 이 언어의 강점을 이용하려면 꼭 짚고 넘어가야할 기능 중 하나가 단연코 go routine일 것 입니다. 타 언어에서는 Thread라는 Common한 OS의 개념 용어을 그대로 사용하는 데.. GO언어에서는 해당 기능을 go routine이라는 걸 통해서 대체할 수 있도록 되어있었습니다. 심지어 다른 언어의 Thread보다 더 경량이라고 합니다.

 

여러가지 실험을 하면서 go routine에 대한 특성을 이해해보고자 노력했습니다.

1. chan

go routine을 사용하면서 chan을 사용하지 않는 경우는 드물 것 입니다. 물론 독자적으로 돌고 종료하는 로직도 있을 수는 있지만, 많은 경우 Main thread에서 Lock 되지 않도록 background로 특정 기능을 수행하기 때문에 background 기능이 끝나고나면 Noti를 받고 그 결과를 어딘가에 반영해야하는 경우가 많기 때문입니다. 이때 chan을 이용하면 go routine과 데이터 송수신이 가능하도록 되어있습니다.

type test struct {
    a int
    b string
}
func main() {
    stringCh := make(chan string)
    structCh := make(chan test)
    go func() {
        fmt.Println("GO routine")
        stringCh <- "done"
        structCh <- test{0, "done"}

    }()
    fmt.Println("result1:", <-stringCh)
    fmt.Println("result2:", <-structCh)
}

Channel을 원하는 자료형으로 선언하고 데이터를 전달하고 받는 샘플입니다.

 

2. Channel은 Make로 초기화한다.

Channel은 make 명령으로 최초에 생성합니다.
생성시 Type을 지정을 지정해야만 golang의 void pointer와 같은 interface 타입도 사용가능합니다.

type test struct {
    a int
    b string
}
func main() {
    interCh := make(chan interface{})
    go func() {
        fmt.Println("GO routine")
        interCh <- "done"
        interCh <- test{0, "done"}
        interCh <- 3
    }()
    //get first result
    fmt.Println("result1:", <-interCh)
    fmt.Println("result2:", <-interCh)
    fmt.Println("result3:", <-interCh)
}
GO routine
result1: done
result2: {0 done}
result3: 3

 

3. 실행순서는 랜덤

Go routine을 빠른 순서로 순차 실행시 실제 Running순서는 보장되지 않습니다.

func main() {
    interCh := make(chan interface{})
    for i := 0; i < 5; i++ {
        j := i
        go func() {
            fmt.Println(j)
        }()
    }
    fmt.Print(<-interCh)
}

결과는 늘 다릅니다.

4
1
2
3
0

위의 코드는 일단 panic입니다. interCh로 부터 값을 건네받기 위해 대기를 시키지만, interCh쪽으로 값을 보내주는 코드는 없기 때문입니다.
이는 go routine들을 실행하고 바로 Main thread(?)가 종료되지 않도록 붙잡는 용도로 써넣은 코드이지만 똑똑하게도 Main thread가 무한정 기다리지 않고, 이를 깨워줄 다른 running코드가 없으니 go runtime에서 그냥 바로 panic을 뱉어줍니다.

 

4. 일반 chan 대기하고 있어야 보낼 수 있다.

그냥 만든 channel은 대기를 하고 있어야지 발송이 가능합니다.
아래의 코드는 int type channel을 만들고 go routine안에서 channel로 발송을 합니다. 하지만 수신을 하는 코드는 없으므로 무한 대기 상태에 빠지게 되고, Main thread측에서도 go routine이 끝나길 기대리게 되니... 위와 같이 deadlock에 빠지고 맙니다. go runtime은 panic을 표시하며 바로 종료시켜 버립니다.

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        ch <- 1
    }()
    fmt.Print(<-ch) // 해당 부분이 없으면 deadlock
    wg.Wait()
}

 

5. (4)번의 상황을 회피하기 위한 방법 중 하나는 buffer이다.

make시 두번째 파라메터로 버퍼사이즈 지정이 가능합니다.
버퍼가 있을 경우 chan은 수신측이 준비되지 않아도 바로 버퍼로 전송이 가능하기 때문에 4번과 같이 수신 코드가 없어도 발송을 할 수 있습니다. 다만 당연히 버퍼가 Full이면 (4)번 상황이 발생합니다.

func main() {
    ch := make(chan int, 1)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        ch <- 1
    }()
    wg.Wait()
}

 

5. 닫고 보내면 패닉

닫혀진 channel에 발신 시도를 할 경우 바로 panic...

func main() {
    ch := make(chan int, 1)
    var wg sync.WaitGroup
    wg.Add(1)
    close(ch)
    go func() {
        defer wg.Done()
        ch <- 1
    }()
    wg.Wait()
}
$ go run test.go
panic: send on closed channel

 

6. channel은 수신, 송신 용도 한정이 가능하다.

func server(ch <-chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    ch <- "result"
}

func main() {
    ch := make(<-chan string, 1)
    var wg sync.WaitGroup
    wg.Add(1)
    go server(ch, &wg)

    fmt.Print(<-ch)
    wg.Wait()
}

위 케이스는 chan을 파라메터로 넘길때 수신용으로 한정해서 넘겼는 데, 이를 내부에서 발신용으로 사용하는 코드가 있으므로 컴파일 에러입니다. 참고로"<-chan", "chan<-" 각각 수, 발신용 Type을 나타냅니다.

 

7. select 문

switch case와 문법은 유사하지만 용도는 조금 다릅니다. 여러 go routine에서 채널로 값이 전달되면 웨이팅을 하고 있다가 먼저 수신되는 channel에 해당하는 루틴이 수행됩니다.
select문이 수행될 때, 어느 case의 channel에도 데이터가 수신되지 않고, default가 선언되어있으면 default문이 수행됩니다. select문을 반복문에 포함시키게 되면 channel로 값이 수신되는 순서로 select문이 수행되는 Thread에서 처리하는 로직을 쉽게 구현할 수 있습니다.

func background1(ch chan string) {
    ch <- "1"
}
func background2(ch chan string) {
    ch <- "2"
}
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go background1(ch1)
    go background1(ch2)
    time.Sleep(10000)
    select {
    case <-ch1:
        fmt.Println("value is transfered 1")
    case <-ch2:
        fmt.Println("value is transfered 2")
    default:
        fmt.Println("default")
    }
}

위 코드는 sleep을 조절하여, default, ch1, ch2 수신이 적당히 랜덤으로 수행되는 테스트 코드입니다. 해당 케이스를 유발하는 sleep 수치는 환경마다 다를 수 있습니다.

'Development' 카테고리의 다른 글

GSON - Composite pattern class  (2) 2023.01.24
Design pattern - Composite pattern  (0) 2023.01.24
OIDC Login 구현해보기 Part-3  (1) 2023.01.24
OIDC Login 구현해보기 Part-2  (0) 2023.01.24
OIDC Login 구현해보기 Part-1  (0) 2023.01.24