CheckWebsites
함수를 작성했다.true
, 잘못된 응답에는 false
을 반환한다.WebsiteChecker
를 통과해야 한다. 해당 함수는 단일의 URL을 필요로 하고 boolean 값을 반환한다. 이 기능은 모든 웹 사이트를 확인하는 데 사용된다.CheckWebsites
의 속도를 테스트해보겠다.CheckWebsites
와 가짜의 구현체를 사용한 WebsiteChecker
를 테스트한다. slowStubWebsiteChecker
는 의도적으로 느리게 만들었다. 해당 코드는 time.Sleep
를 사용하여 20ms를 기다렸다가 true 값을 반환한다.go test -bench=.
를 실행하자. (혹은 만약 윈도우 PowerShell을 사용한다면 go test -bench="."
이다.)CheckWebsites
가 2249228637 나노 초로 기준(benchmark)이 되었다 - 2와 1/4초이다.CheckWebsites
을 어떻게 더 빠르게 만들지 이해를 할 수 있을 것이다. 다음 웹 사이트에 요청을 보내기 전에 웹 사이트가 응답하기를 기다리는 대신에, 우리가 컴퓨터에게 대기하는 시간 동안 다음 요청을 하도록 만들어 보겠다.doSomething()
이라는 함수를 호출했을 때 반환이 될 때까지 기다려야 한다(반환 값이 없다고 하더라도 함수가 끝날 때까지 기다린다). 우리는 이러한 연산을 동기(blocking) - 이것은 우리가 끝날 때까지 기다리도록 만든다라고 말한다. 동기적으로 실행되지 않는 연산은 goroutine이라고 하는 별도의 프로세스에서 실행된다. Go 코드를 상단부터 아래로 읽어 내려가는 동작을 생각하면, 각 함수를 만날 때마다 코드의 '내부'로 들어가 무슨 기능을 하는지 읽게 된다. 별도의 프로세스가 시작되면 원래 읽던 사람과는 다르게 다른 읽는 사람이 함수 내부를 읽어 내려가는 것과 같다.go
를 함수 앞에 붙이는 방법: go doSomething()
으로 함수 호출을 go
statement로 바꿀 수 있다.go
를 함수 호출 앞에 붙이는 것이기 때문에, goroutine을 시작하기 위해 종종 익명 함수를 사용하기도 한다. 익명 함수는 정규 함수 선언과 동일하게 보이지만 이름이 없다(당연하다). 위에 적힌 코드의 for
반복문 내의 몸체 부분에서 볼 수 있다.()
이 붙어있는 것이다. 두 번째로는 정의된 곳에서의 lexical scope에 대한 접근을 유지한다는 것이다 - 익명 함수를 선언할 때 사용할 수 있는 모든 변수들을 함수 본문에서도 사용할 수 있다.WebsiteChecker
함수)와 동시적으로 실행되어 각 결과를 결과 map에 추가한다는 것이다.go test
로 실행을 하면:CheckWebsites
가 빈 map 값을 반환하는 것을 볼 수 있다. 무엇이 잘못되었을까?for
반복문이 시작되고 난 후 goroutine들 중 하나도 결괏값을 results
map에 추가할 시간이 없었다; WebsiteChecker
함수가 goroutine들에게는 너무 빨라서 비어있는 map이 반환되는 것이다.url
이 모든 for
반복 때마다 재사용 된다는 것이다 - urls
에서 매번 새로운 값을 가져간다. 하지만 우리의 각 goroutine들은 각 url
변수에 대한 참조를 가지고 있다 - 그들은 그들만의 독립된 복사본을 갖고 있지 않다. 그래서 그들은 모두 url
이 반복이 끝날 때 갖는 값을 쓰고 있다 - 마지막 url 말이다. 이것이 우리가 결과로 마지막 url만 받은 이유이다.u
- 를 부여한 다음 url
을 인수로 하여 익명 함수를 호출하고, u
의 값을 goroutine을 실행하는 루프의 반복에 대한 url
값으로 고정되도록 한다. u
는 url
의 값을 복사한 것이므로 변경되지 않는다.fatal error: concurrent map writes
. 가끔, 테스트를 할 때, 2개의 goroutine들이 같은 시간에 결과 map에 쓸 때가 있다. Go에 있는 Map은 한 번에 여러 개를 쓰는 것을 싫어하기 때문에 fetal error
가 난 것이다.race
옵션과 함께 실행하면 된다: go test -race
.WARNING: DATA RACE
는 꽤 모호하지 않다. 오류 본문을 읽어보면 2가지의 다른 goroutine들이 map에 쓰려고 하는 것을 볼 수 있다.Write at 0x00c420084d20 by goroutine 8:
Previous write at 0x00c420084d20 by goroutine 7:
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11
WebsiteChecker
함수를 수행하게 하는 각 goroutine들 간의 통신에 대해 생각해 보려 한다.results
map과 더불어 이제는 같은 방법으로 만든(make)
resultChannel
이 있다. chan result
는 채널의 타입이다 - result
채널의. 새로운 타입인 result
는 WebsiteChecher
의 반환 값과 확인 중인 url을 연결하기 위해 만들어졌다 - 이것은 string
과 bool
로 이루어졌다. 두 값 중 어느 것도 이름을 붙일 필요가 없기 때문에, 각각의 값은 구조 내에서 익명으로 되어 있다; 이것은 값의 이름을 무엇으로 붙여야 할지 알기 어려울 때 유용할 수 있다.map
에 바로 적는 것 대신에 wc
로 각 요청 때마다 result
구조를 resultChannel
에 보내는 수식 과 함께 보낸다. 이것은 <-
연산자를 사용하고, 좌측에 있는 채널과 우측의 값을 사용한다:for
반복문은 각 url 들에 대해 1번씩 반복된다. 내부에서는 받는 수식 을 사용하고 있는데, 이 식은 채널에서 수신한 값을 변수에 할당한다. 이것 또한 <-
연산자를 사용하지만, 2개의 피연산자들의 위치가 뒤바뀐다: 채널이 우측에 위치하고 우리가 할당할 변수는 좌측에 위치한다:result
를 사용하여 map을 갱신한다.wc
를 호출하고, 각각 결과 채널로 보내지만, 이 일은 자체 프로세스 내에서 병렬적으로 수행되어 결괏값이 받는 수식을 사용하여 결과 채널에서 값을 추출할 때 각 결과가 한 번에 하나씩 처리된다.CheckWebsites
함수의 긴 리팩터링에 참여하고 있다; 입력과 출력은 변하지 않고, 더 빨라졌을 뿐이다. 그러나 우리가 작성한 벤치마크와 함께 시행한 테스트는, 소프트웨어가 여전히 작동 중이라는 신뢰를 유지하는 방식으로 Check Website
를 리팩터링 할 수 있게 해주었고 실제로 더 빨라졌음을 보여주었다.[Premature optimization is the root of all evil(조급한 최적화는 모든 악의 근원이다)][popt] -- Donald Knuth