본문 바로가기

IT/Go 언어 (Golang)

[Golang] Go Routine (고 루틴) - Golang의 꽃 [심화]

반응형

이  론

어느 프로그래밍 언어나 해당 언어만의 강점, 혹은 "꽃"이라고 불릴 만한 것이 있다.

 

예를 들어 C언어의 꽃이라고 한다면 누가 물어도 "포인터"일 것이며,

Python 같은 경우 "Life is too short, you need Python."으로 대표되는 "초고도의 생산성"일 것이다. (필자의 경우 이 초고도의 생산성이 파이썬의 <모든 변수&상수의 객체화>에 따라 Call by Assignment로 자료형을 다루기 때문이 아닐까 조심스레 추측해본다.)

 

그렇다면, Go언어에서는 "Go Routine"이 있다. (이하 "고루틴")

 

고루틴은 가벼운 스레드와 같은 것으로, 현재 수행 흐름과 별개의 흐름을 만들어준다.

스레드는 으레 SW직군 면접 질문에 나오는 단골 질문인 "프로세서와 스레드의 차이점이 뭔가요?"에서 단 한 줄로 설명될 수 있는데,

 

 

"프로세스는 운영체제로부터 자원을 할당받는 작업의 단위이며, 스레드는 프로세스가 할당받는 자원을 이용하는 실행의 단위이다."

이 설명대로, 고루틴은 스레드와 본질적으로는 같은 것이다. (할당받는 자원을 이용하는 실행의 단위라는 관점에서)

그러나 스레드와 고루틴을 가르는 결정적인 구분선은 무엇인가? 라고 한다면 아래와 같은 답이 나올 수 있다.

 

  • 고루틴은 하나의 스레드 내에서 여러 고루틴으로 나뉠 수 있다. (1개의 스레드에 1+개의 고루틴이 있을 수 있음)
  • 고루틴 하나가 Wait() 등의 함수나 channel input에 의해 막힌다면(Block) 곧바로 다른 하나의 고루틴으로 스위치된다. 이는 같은 스레드에서 진행된다.
  • 이렇게 스위치하는 연산에는 고작 3개의 레지스터밖에 들어가지 않는다. 통째로 스레드를 스위치하는 것이 아니기 때문이다.
  • 그렇기에 고루틴은 일반적인 OS 스레드에 비해 굉장히 가볍고 빠를 수 있는 것이다.

소스 코드

그렇다면 실제로 고루틴을 사용해보는 예제를 다뤄보자.

// Go Routine Control

package main

import (
	"archive/zip"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"sync"
)

func download(url string) (string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return "", err
	}
	filename, err := urlToFilename(url)
	if err != nil {
		return "", err
	}
	f, err := os.Create(filename)
	if err != nil {
		return "", err
	}
	defer f.Close()
	_, err = io.Copy(f, resp.Body)
	return filename, err
}

func urlToFilename(rawurl string) (string, error) {
	url, err := url.Parse(rawurl)
	if err != nil {
		return "", err
	}
	return filepath.Base(url.Path), nil
}

func writeZip(outFilename string, filenames []string) error {
	outf, err := os.Create(outFilename)
	if err != nil {
		return err
	}
	zw := zip.NewWriter(outf)
	for _, filename := range filenames {
		w, err := zw.Create(filename)
		if err != nil {
			return err
		}
		f, err := os.Open(filename)
		if err != nil {
			return err
		}
		defer f.Close()
		_, err = io.Copy(w, f)
		if err != nil {
			return err
		}
	}
	return zw.Close()
}

func main() {
	runtime.GOMAXPROCS(4)
	var wait sync.WaitGroup
	var urls = []string{
		"http://xkxqjlzvieat874751.gcdn.ntruss.com/2/2019/d265/2d2651001bb575d64812b398661b39589500a9084c38a772f4b409035f74bf4e5_o_st.jpg",
		"https://file.mk.co.kr/meet/neds/2019/06/image_readtop_2019_441884_15610734753796599.jpg",
		"https://t1.daumcdn.net/news/201806/15/seouleconomy/20180615151617982vtvw.jpg",
	}

	for _, url := range urls {
		wait.Add(1)
		go func(url string) {
			defer wait.Done()
			if _, err := download(url); err != nil {
				log.Fatal(err)
			}
		}(url)
	}
	wait.Wait()

	filenames, err := filepath.Glob("*.jpg")
	if err != nil {
		log.Fatal(err)
	}
	err = writeZip("kei_img.zip", filenames)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("DONE!")
}

위의 코드는 필자가 좋아하는 국내 아이돌 러블리즈의 Kei 님의 사진을 3장 다운로드 받아와서, 압축하는 예제이다.

 

위 코드에서 집중할 것은 main 함수 내부에 정의되어 있는 익명함수의 고루틴과, wait.Wait() 함수이다.

func main() {
	runtime.GOMAXPROCS(4)
	var wait sync.WaitGroup
	var urls = []string{
		"http://xkxqjlzvieat874751.gcdn.ntruss.com/2/2019/d265/2d2651001bb575d64812b398661b39589500a9084c38a772f4b409035f74bf4e5_o_st.jpg",
		"https://file.mk.co.kr/meet/neds/2019/06/image_readtop_2019_441884_15610734753796599.jpg",
		"https://t1.daumcdn.net/news/201806/15/seouleconomy/20180615151617982vtvw.jpg",
	}

	for _, url := range urls {
		wait.Add(1)
		go func(url string) {
			defer wait.Done()
			if _, err := download(url); err != nil {
				log.Fatal(err)
			}
		}(url)
	}
	wait.Wait()

	filenames, err := filepath.Glob("*.jpg")
	if err != nil {
		log.Fatal(err)
	}
	err = writeZip("kei_img.zip", filenames)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("DONE!")
}

main 함수의 첫 줄은 runtime.GOMAXPROCS() 함수로, 최대 사용 프로세서를 4개로 설정해두었다.

그 후, go 예약어를 통해 고루틴을 이어지는 익명함수로 만들어낸다.

 

해당 for문 바로 아래에 있는 wait.Wait()을 통해서 고루틴이 끝나기 전까지는 이후의 main 함수가 실행되지 않도록 하는 게 포인트이다.

 

만약 해당 함수를 쓰지 않는다면, 다운로드가 끝나기 전에, 혹은 다운로드를 하려는 고루틴에 들어가기도 전에 main 함수의 아래쪽에 있는 압축함수에 다다를 것이고, 다운로드를 하는 와중에 압축을 하려고 할 것이기에 필히 중대한 에러가 일어날 것이다.

 

소스 코드의 실행 결과는 아래와 같다.

오오 Kei님의 이미지 오오

보다시피, 3개의 jpg 파일이 다운로드 되었고, "kei_img.zip" 파일로 압축되었다.

해당 프로그램 소스 코드의 실행 속도는 굉장히 빠르다.

 

파이썬으로 똑같은 내용의 소스 코드를 짠다면 더욱 짧은 줄로 빠르게 짤 수 있겠지만,

동시성이 없기 때문에 많은 파일을 다운로드 받을수록 더욱 느려질 것이다.

 

이런 면에서 Golang은 확실히 동시성과 병렬성에서 매우 뛰어난 언어라고 볼 수 있다.

그러면서도 C나 C++에 비해서는 확실히 더 나은 생산성도 보이니,

배울만한 가치가 충분히 높은 언어라고 생각된다.

반응형