GIS DEVELOPER, 공간정보시스템 개발 블로그 -
   GIS DEVELOPER, 공간정보시스템 개발 블로그  
김형준(Dip2K) GIS 기반기술 연구소
첫 페이지
공지 | E-Mail | 관리자 | 글쓰기   
2016/09/27 13:23 2016/09/27 13:23
[Golang] go-routine을 이용한 URL 컨텐츠 가져오기

몇일전 에이콘 출판사의 The Go Programming Language라는 책을 살펴보고 있는데요. 1장의 튜토리얼에서 소개된 코드 중 fetchall이라는 예제에 대해 이해한 바를 복습차원에서 정리해 봅니다.

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<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprint(err)
        return
    }

    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close()

    if err != nil {
        ch <- fmt.Sprintf("while reading %s: %v", url, err)
        return
    }

    secs := time.Since(start).Seconds()
    ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}

이 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 그리고 컨텐츠의 길이를 문자열로 구성하여 채널에 전달합니다.

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

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

사용자 삽입 이미지

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

Tag : , ,
2016/09/22 00:42 2016/09/22 00:42
[Java] Java8, 스트림(Streams)

A java.util.Stream 클래스는 동일한 타입의, 다수의 데이터에 대해 다양한 연산을 빠르게 적용할 수 있는 기능을 제공합니다. 스트림은 java.util.Collection 객체로부터 생성되는데, 리스트나 집합 등이 그 예인데요. 맵은 지원하지 않습니다. 다음과 같은 리스트가 있다고 하고 이 리스트를 통해 스트림을 만들어 다양한, 유용한 연산의 예제 코드를 언급하겠습니다.

List stringCollection = new ArrayList<>();
		
stringCollection.add("신숙주");
stringCollection.add("김옥자");
stringCollection.add("조미현");		
stringCollection.add("송기숙");
stringCollection.add("최미라");
stringCollection.add("박미남");
stringCollection.add("김구라");
stringCollection.add("김형준");

filter

요소 중 '김'으로 시작하는 문자열을 걸러내어 화면에 표시합니다.

stringCollection
    .stream()
    .filter((s) -> s.startsWith("김"))
    .forEach(System.out::println);

sorted

요소를 정렬하고, 정렬된 리스트에 대해 '김'자로 시작하는 요소를 화면에 표시합니다.

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("김"))
    .forEach(System.out::println);

map

요소 각각에 함수를 호출합니다. 아래는 각 요소의 뒤에 '씨' 자를 붙이고 정렬한 후 출력합니다.

stringCollection
    .stream()
    .map((a) -> a + "씨")
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

count

'김'자로 시작하는 요소의 개수를 얻습니다.

long cntKim = stringCollection
    .stream()
    .filter((s) -> s.startsWith("김"))
    .count();

reduce

각 요소를 하나로 묶습니다.

Optional reduced = stringCollection
    .stream()
    .sorted()
    .reduce((s1, s2) -> s1 + "," + s2);

reduced.ifPresent(System.out::println);

anyMatch, allMatch, noneMatch

각 요소에 대해 지정한 조건에 부합하는지를 검사합니다. 첫번째인 anyMatch는 '김'으로 시작하는 요소가 하나라도 있다면 참입니다. 두번째인 allMatch는 모든 요소가 '김'으로 시작해야만 참입니다. 끝으로 세번째는 '고'로 시작하는 요소가 하나도 없어야 '참'입니다.

boolean anyStartsWithKim = stringCollection
    .stream()
    .anyMatch((s) -> s.startsWith("김"));

boolean allStartsWithKim = stringCollection
    .stream()
    .allMatch((s) -> s.startsWith("김"));

boolean noneStartsWithGo = stringCollection
    .stream()
    .noneMatch((s) -> s.startsWith("고"));
GIS Developer, 김형준은 '모바일 3D 그래픽스' 번역 및 출판 / '모바일 GIS 프로그래밍' 집필 및 출판을 하였으며, 현재 (주)지오서비스 대표이사 및 (주)내가시스템 연구소 소장으로써 GIS 기반기술 연구 및 시스템 개발을 하고 있습니다.
 블로그 키 포스트
 포스트 분류
전체 (784)
GIS 개발 (244)
공간DB 공유 (3)
프로그래밍 (299)
스치는 생각들 (147)
번역 또는 집필 (4)
 블로그 핵심 단어
GIS Xr OpenGL BlackPoint Shader FingerEyes C++ Algorithm Java Android DuraMap Map Engine ActionScript WPF C# ArcObjects 안드로이드 ArcGIS FingerEyes-Xr BlackPoint-Xr HTML5 JavaScript WPF 3D DuraMap-Xr Flex OrangeMap template Service Pattern GPS OpenGL ES Mr.Tiler-Xr .NET XML XGE GIS Korea Graphic Design OOD WebService PSP Mobile GIS golang iOS channel GeoService gomodule KASS BaseMap-Xr SQLite
 월 단위 보관함
2016/09
2016/08
2016/07
 방문자 수
전체 : 4277540명
오늘 : 1081명 (어제 : 1294명)
Powered By TatterTools
rss