[Golang] go-routine을 이용한 URL 컨텐츠 가져오기

Go 언어는 2009년 말에 공개된 언어로, 다른 언어에 비하면 매우 최신의 언어입니다. 최근 컴퓨터 프로그래밍을 보면 동시성 프로그래밍 방법론이 매우 중요한데요. 네트워크를 통한 데이터 송수신처리나 대용량의 데이터를 분할하여 처리하여 그 결과를 요약하고, 이렇게 요약된 데이터를 하나로 합쳐 최종결과를 내는 과정에서.. 데이터를 분할하여 동시에 처리할 수 있는 자연스럽고, 안정적이며 빠른 처리가 필요하기 때문입니다. Go 언어는 언어 차원에서 go-module이라는 기존의 스레드보다 최적화된 동시성 기능과 이러한 고모듈 간의 데이터 교환을 위한 목적으로 도입된 channel을 제공합니다. 물론 mutex 등과 같은 기존의 일반적인 데이터 공유를 위한 기법도 제공합니다.

최근 동시성 프로그래밍에 관심이 많았고.. 동시성에 강점을 보이는 몇가지 언어 중 Go 언어에 강한 이끌림을 받았는데요. 몇 일전부터 에이콘 출판사의 The Go Programming Language라는 책을 짬짬이 살펴보고 있습니다. 1장의 튜토리얼에서 소개된 코드 중 fetchall이라는 예제에 대해 이해한 바를 복습차원에서 정리해 봅니다. 최대한 상세히 설명하고 있으므로 Go 언어를 처음 접하시는 분들도 이해가 가능하시리라 믿습니다.

예제 프로그램으로써 fetchall은 실행시 파라메터로 다수의 URL을 넘겨주면 고루틴이라는 Go언어의 스레드를 통해 해당 URL의 컨텐츠를 가져오는 시간을 측정하여 출력해 줍니다. 먼저 패키지 이름 선언과 함께 프로그램에서 사용할 라이브러리를 import 합니다.

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)

fmt는 화면에 문자열을 출력하기 위해 필요하고, io와 io.ioutil은 URL에 대한 컨텐츠의 길이를 얻기 위해 필요하며, net/http는 URL에 대한 연결을 위해서, os는 실행시 Command 라인의 인자를 얻기 위해서, 끝으로 time은 URL의 컨텐츠를 가져오는데 소요되는 시간을 계산하기 위해서 필요한 라이브러리입니다.

다음은 프로그램이 시작되는 함수인 main에 대한 코드입니다.

func main() {
    start := time.Now()
    ch := make(chan string)

    for _, url := range os.Args[1:] {
        go fetch(url, ch)
    }

    for range os.Args[1:] {
        fmt.Println(<-ch)
    }

    fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

2번 코드에서 실행하기 앞서 현재의 시간을 저장해 둡니다. go는 변수의 타입을 명시적으로 지정할 수도 있고, 2번 코드처럼 := 를 통해 타입을 지정하지 않고 선언할 수도 있습니다. 3번 코는 채널(Channel)을 만드는 코드입니다. 채널은 스레드들 간에 데이터를 주고 받는 용도로 사용되는데요. Go는 고루틴이라는 기존의 스레드보다 최적화된 동시 실행을 위한 기능을 제공합니다. 3번 코드는 문자열 타입의 데이터를 주고 받을 수 있는 채널을 생성하고 있습니다. 다음은 5번~7번의 반복문인데요. 5번 코드의 반복문인 for 문에서 실행시 입력한 Command Line 인자를 얻기 위해 os.Args[1:]이라는 슬라이스 데이터를 사용하고 있습니다. 정확히 슬라이스는 배열(Array)과는 다르지만 n개의 요소를 갖는다고 가정한다면 배열처럼 0부터 n-1까지의 인덱스를 통해 각 요소의 값에 접근할 수 있는데요. os.Args[1:]에서 [1:]의 의미는 2번째 요소부터 마지막 요소를 의미합니다. 5번 코드의 for 문을 통해 이 각 요소의 값은 url이라는 변수로 접근이 가능합니다. 6번 코드는 fetch라는 뒤에서 살펴볼 사용자 정의 함수를 고루틴으로 동시적으로 실행하도록 합니다. fetch 함수는 2개의 인자를 받으며 첫번째는 컨텐츠를 가져올 url 문자열이고 두번째는 main 함수에 대한 고루틴과 fetch 실행시 생성할 고루틴간의 데이터 통신을 위한 채널 객체입니다. 주지할 부분은 5번~7번 코드의 반복문 호출을 통한 fetch 함수의 호출은 비동기적으로 실행되므로 바로 return 된다는 점입니다. 다시 9번~11번의 반복문이 나오는데요. 10번 코드의 <-ch는 앞서 fetch 함수를 통해 전달한 채널 객체인 ch로부터 데이터가 전달될때까지 블럭킹(Blocking)됩니다. fetch가 고루틴으로 실행된 횟수만큼 <-ch에 의해 블럭킹됩니다. <-ch는 고루틴에서 ch를 통해 전달된 데이터가 반환되므로 10번 코드는 fetch 함수에서 ch 채널로 전달된 문자열을 화면에 출력하게 됩니다. 끝으로 13번 코드는 처음 프로그램 시작에서 현재까지 실행되어 경과된 시간을 초(Second) 단위로 화면에 표시합니다. 여기서 출력되는 시간은, 만약 5개의 URL을 인자로 전달했다고 할때 이 URL에 대한 컨텐츠를 가져오는 fetch 함수의 호출 중 가장 시간이 많이 소요된 호출 시간이라는 점입니다. 이는 fetch 함수가 순차적으로 실행되는 것이 아니고 고루틴을 통해 동시적으로 실행되기 때문입니다. 이제 다음으로 fetch 함수입니다.

func fetch(url string, ch chan

이 fetch 함수는 인자로 주어진 URL에 대한 컨텐츠를 가져오고 그 컨텐츠의 길이와 컨텐츠를 가져오는데 소요되는 시간을 문자열로 구성하여 인자로 주어진 채널로 보내는 기능을 합니다. 코드를 좀더 자세히 살펴보면, 2번 코드는 함수를 실행하기 직접의 현재 시간을 얻습니다. 이렇게 얻은 시간은 17번 코드를 통해 fetch 함수 호출이 끝날때 소요된 시간을 계산할때 사용됩니다. 3번 코드는 url에 대한 컨텐츠를 얻기 위한 커넥션을 맺는 것입니다. Go 언어는 함수가 2개 이상의 값을 반환할 수 있는데요. http.Get 함수는 만약 해당 url을 얻을때 에러가 발생하면 두번째 반환값에 해당 에러에 대한 객체를 전달해 주게 됩니다. 그러므로 4번~7번 코드에서 먄약 에러 객체가 nil이 아닐 경우 해당 에러의 내용을 문자열로 만들어 채널에 전달하고 fetch 함수를 종료합니다. 9번 코드는 해당 url에 대한 컨텐츠의 길이를 얻어오기 위한 코드인데요. io.Copy 함수는 스트림에서 또 다른 스트림으로 데이터를 복사하는 함수인데요. 첫번째 인자가 복사되어 저장될 출력 스트림이고 두번째 인자가 복사될 입력 스트림입니다. 출력 스트림을 ioutil.Discard로 지정하면 복사되지 않고 폐기되며, 입력 스트림은 앞서 url을 통해 맺은 커넥션의 Body 객체로 지정합니다. 이 io.Copy 함수 역시 2개의 값을 반환하며 두번째가 nil이 아니라면 에러이므로 12번~14번 코드에서 이를 확인하고, 만약 에러가 발생했다면 채널에 에러에 대한 정보를 문자열로 만들어 전달하고 함수를 종료합니다. 17번은 성공적으로 fetch 함수가 실행되어 해당 url의 컨텐츠의 길이를 잘 가져왔다면, 소요된 시간을 초단위로 계산하여, 18번 코드에서 이 계산된 시간과 url 그리고 컨텐츠의 길이를 문자열로 구성하여 채널에 전달합니다.

실행하여 그 결과의 한 예로 아래의 화면을 볼 수 있습니다.

사용자 삽입 이미지

위의 결과를 보면 총 5개의 url을 Command-Line의 인자로 전달했으며 이 중 http://www.gisdeveloper.co.kr의 컨텐츠를 가져오는 시간이 0.62초로 안타깝지만.. 가장 느렸습니다. 이 5개의 url을 모두 가져오는 시간 역시 0.62s초로 가장 느린 url 1개를 가져오는 시간과 동일하다는 것을 알 수 있습니다. 이는 각 url의 컨텐츠를 가져오기 위해 각 url에 대한 처리를 순차적으로 하지 않고 동시에 실행하였기 때문에 나오는 결과입니다. 이는 우리가 예상했던 것과 매우 정확히 일치합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다