프로그래밍 언어

chan chan 활용법 - 3. 시그널 채널의 채널로 이용하기

해당 시리즈는 Golang Korea 페이스북 그룹 최흥배님의 번역 글에서 영감을 얻었으며 Go언어와 Go언어를 이용한 동시성 프로그래밍에 관해 어느정도 알고 있는 독자를 대상으로 한다.

이전 글에서는 순서가 보장되는 워커풀 패턴으로 이용하는 채널의 채널 활용법에 대해 알아보았다. 해당 활용법은 성능에 초점을 두었다. 이번 활용법은 성능 보다는 구조에 초점을 둔다. 다른 프로젝트에서 이따금씩 쓰이기 때문에 알아두는 것이 좋다.

시그널 채널

시그널 채널은 닫힐 때까지 코드의 실행을 멈추게 하는 채널이다.

원리는 다음과 같다. 채널은 데이터를 보낼 때 해당 채널로 데이터를 받거나 닫힐 때까지 다음 코드로 넘어가지 않고 기다리는 특성이 있다. 그리고 시그널 채널은 기본적으로 데이터를 받지 않도록 한다. 두 문장을 연결해보면 시그널 채널에서 데이터를 보내면 닫힐 때 까지 다음 코드로 넘어가지 않고 기다린다. 시그널 채널을 닫으면 다음 코드로 진행할 수 있게 된다.

시그널 채널은 주로 동시성 프로그래밍에서 병렬로 실행 중인 다른 함수가 끝날 때까지 대기시키는 데에 주로 쓰인다. 또한 시그널 채널은 관례상 struct{}형 데이터를 받게 하는데, 이는 struct{}형이 Go언어의 자료형 중 가장 공간을 적게 차지하기 때문이다.

임의의 수 출력 서비스

임의의 수 출력 서비스는 사용자가 요청하면 0.5초 후에 0에서 99까지의 임의의 수를 출력하는 서비스이다. 서비스를 처리하는 데몬은 고루틴으로 실행되며 데몬이 사용자의 모든 요청을 완료하기 전까지 프로그램이 종료되어서는 안된다.

daemon

package daemon

import (
	"fmt"
	"math/rand"
	"time"
)

type daemon struct {
	req chan chan struct{}
}

func New() *daemon {
	d := new(daemon)
	d.req = make(chan chan struct{})
	return d
}

func (d *daemon) Start() {
	go func() {
		for {
			select {
			case ch := <-d.req:
				rand.Seed(time.Now().UnixNano())
				time.Sleep(500 * time.Millisecond)
				fmt.Println(rand.Intn(100))
				close(ch)
			}
		}
	}()
}

func (d *daemon) Do() {
	ch := make(chan struct{})
	d.req <- ch
	<-ch
}

daemon 구조체 내에는 chan chan struct{}형의 req만 있는데, 해당 채널이 이번 활용법의 핵심이다.

Start는 daemon을 실행하는 함수로, 고루틴으로 호출된다. 내부의 for-select 문에서는 해당 구조체의 req에서 시그널 채널을 받을 때마다 호출되며 0.5초 후에 임의의 수를 출력한다. 그 후 받은 시그널 채널을 닫는다.

마지막으로 Do는 사용자가 호출하면 시그널 채널을 만들어서 req에 보내는 함수이다. 이후 해당 시그널 채널에서 데이터를 보내게 함으로써 위에서 언급했던 중단점의 역할을 하게 된다. 이 시그널 채널은 Start 함수에서 요청을 처리한 후 시그널 채널을 닫을 때까지 Do함수를 종료시키지 않는다.

결과

실행 코드는 매우 직관적이라 따로 설명하지 않겠다.

package main

import (
	"fmt"
	"myProject/daemon"
)

func main() {
	d := daemon.New()
	d.Start()
	d.Do()
	d.Do()
	d.Do()
	fmt.Println("Disconnected.")
}

실행해 보면 랜덤한 변수 각각은 0.5초의 간격을 두고 출력될 것이다.

89
25
48
Disconnected.

결론

해당 패턴을 위처럼 for-select 문에 이용할 때  단점이 하나 있다. 해당 패턴 자체에서는 고루틴을 종료시키는 방법이 없다는 것이다. 이는 더 이상 해당 서비스를 이용하지 않더라도 그 서비스의 고루틴이 계속 돌아갈 수 있다는 것을 의미하며, 메모리 누수가 발생할 수 있다는 것을 의미한다. 당장 위의 코드만 하더라도 프로그램이 종료되는 시점에 고루틴이 종료되지 않았다.

바람직한 대안으로는 서비스를 종료할 시그널 채널을 하나 더 만드는 것이 있다. select 문에 해당 시그널 채널의 경우를 추가하고 해당 채널이 호출되면 return을 통해 고루틴을 종료하도록 만든다. 그리고 서비스를 더 이상 받지 않을 때에 함수 등을 통해 시그널 채널을 닫게 하면 된다. 다만 이렇게까지 하면 코드가 길어지고 시그널 채널로써의 역할이 중복되는 감이 있다.

채널의 채널은 Go언어만의 재미있는 분야이다. 필자는 독자가 해당 시리즈를 읽고 채널의 채널이 생각보다 쓸만하다고 느꼈으면 좋겠다.

참고 자료