🔥 동시성을 활용한 웹 크롤러 만들기
759자
8분
강의 목차
Go 언어는 동시성 프로그래밍을 위한 강력한 기능들을 제공합니다. 이번 예제에서는 Go의 동시성 기능을 활용하여 웹 크롤러를 병렬로 처리하는 방법에 대해 알아보겠습니다.
먼저, Crawl 함수를 수정하여 URL을 병렬로 가져오되, 같은 URL을 두 번 가져오지 않도록 해보겠습니다.
go
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
// 이미 가져온 URL인지 확인하기 위해 맵을 사용합니다.
visited := make(map[string]bool)
// 작업을 동기화하기 위해 뮤텍스를 사용합니다.
var mu sync.Mutex
// 작업 그룹을 생성하여 고루틴을 관리합니다.
var wg sync.WaitGroup
// 재귀 호출 대신 큐를 사용하여 URL을 저장합니다.
queue := []string{url}
for len(queue) > 0 {
// 큐에서 URL을 꺼냅니다.
url := queue[0]
queue = queue[1:]
// 이미 방문한 URL인 경우 건너뜁니다.
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
// 작업 그룹에 작업을 추가합니다.
wg.Add(1)
// 고루틴을 생성하여 URL을 가져옵니다.
go func(url string) {
defer wg.Done()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 새로 찾은 URL을 큐에 추가합니다.
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
}(url)
}
// 모든 작업이 완료될 때까지 기다립니다.
wg.Wait()
}
go
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
// 이미 가져온 URL인지 확인하기 위해 맵을 사용합니다.
visited := make(map[string]bool)
// 작업을 동기화하기 위해 뮤텍스를 사용합니다.
var mu sync.Mutex
// 작업 그룹을 생성하여 고루틴을 관리합니다.
var wg sync.WaitGroup
// 재귀 호출 대신 큐를 사용하여 URL을 저장합니다.
queue := []string{url}
for len(queue) > 0 {
// 큐에서 URL을 꺼냅니다.
url := queue[0]
queue = queue[1:]
// 이미 방문한 URL인 경우 건너뜁니다.
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
// 작업 그룹에 작업을 추가합니다.
wg.Add(1)
// 고루틴을 생성하여 URL을 가져옵니다.
go func(url string) {
defer wg.Done()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 새로 찾은 URL을 큐에 추가합니다.
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
}(url)
}
// 모든 작업이 완료될 때까지 기다립니다.
wg.Wait()
}
이제 코드를 하나씩 살펴보겠습니다.
go
visited := make(map[string]bool)
go
visited := make(map[string]bool)
visited맵을 사용하여 이미 가져온 URL을 추적합니다.- 맵의 키는 URL이고, 값은 해당 URL을 방문했는지 여부를 나타내는 불리언 값입니다.
go
var mu sync.Mutex
go
var mu sync.Mutex
sync.Mutex를 사용하여 맵에 대한 동시 접근을 동기화합니다.- 맵은 여러 고루틴에서 동시에 접근할 수 있으므로, 뮤텍스를 사용하여 경쟁 상태를 방지합니다.
go
var wg sync.WaitGroup
go
var wg sync.WaitGroup
sync.WaitGroup을 사용하여 생성된 고루틴들을 관리합니다.- 작업 그룹은 모든 고루틴이 완료될 때까지 기다리는 역할을 합니다.
go
queue := []string{url}
go
queue := []string{url}
- 재귀 호출 대신 큐를 사용하여 URL을 저장합니다.
- 초기에는 시작 URL만 큐에 추가됩니다.
go
for len(queue) > 0 {
url := queue[0]
queue = queue[1:]
// ...
}
go
for len(queue) > 0 {
url := queue[0]
queue = queue[1:]
// ...
}
- 큐에 URL이 있는 동안 반복합니다.
- 큐에서 URL을 꺼내고, 해당 URL에 대한 작업을 수행합니다.
go
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
go
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
- 뮤텍스를 사용하여
visited맵에 대한 접근을 동기화합니다. - 이미 방문한 URL인 경우 건너뜁니다.
- 방문하지 않은 URL인 경우
visited맵에 추가합니다.
go
wg.Add(1)
go
wg.Add(1)
- 작업 그룹에 작업을 추가합니다.
wg.Add(1)은 작업 그룹에 새로운 작업이 추가되었음을 알립니다.
go
go func(url string) {
defer wg.Done()
// ...
}(url)
go
go func(url string) {
defer wg.Done()
// ...
}(url)
- 고루틴을 생성하여 URL을 가져옵니다.
defer wg.Done()은 고루틴이 완료되면 작업 그룹에 알립니다.- 고루틴 내에서 URL을 가져오고, 결과를 출력합니다.
go
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
go
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
- 새로 찾은 URL을 큐에 추가합니다.
- 뮤텍스를 사용하여
visited맵과 큐에 대한 접근을 동기화합니다. - 방문하지 않은 URL만 큐에 추가합니다.
go
wg.Wait()
go
wg.Wait()
- 모든 작업이 완료될 때까지 기다립니다.
wg.Wait()은 모든 고루틴이 완료될 때까지 블로킹합니다.
이렇게 수정된 Crawl 함수는 URL을 병렬로 가져오면서도 같은 URL을 두 번 가져오지 않도록 합니다. 고루틴을 사용하여 동시성을 활용하고, 뮤텍스와 작업 그룹을 사용하여 동기화와 관리를 수행합니다.
전체 코드
go
package main
import (
"fmt"
"sync"
)
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error)
}
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
visited := make(map[string]bool)
var mu sync.Mutex
var wg sync.WaitGroup
queue := []string{url}
for len(queue) > 0 {
url := queue[0]
queue = queue[1:]
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
wg.Add(1)
go func(url string) {
defer wg.Done()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
}(url)
}
wg.Wait()
}
func main() {
Crawl("<https://golang.org/>", 4, fetcher)
}
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
var fetcher = fakeFetcher{
"<https://golang.org/>": &fakeResult{
"The Go Programming Language",
[]string{
"<https://golang.org/pkg/>",
"<https://golang.org/cmd/>",
},
},
"<https://golang.org/pkg/>": &fakeResult{
"Packages",
[]string{
"<https://golang.org/>",
"<https://golang.org/cmd/>",
"<https://golang.org/pkg/fmt/>",
"<https://golang.org/pkg/os/>",
},
},
"<https://golang.org/pkg/fmt/>": &fakeResult{
"Package fmt",
[]string{
"<https://golang.org/>",
"<https://golang.org/pkg/>",
},
},
"<https://golang.org/pkg/os/>": &fakeResult{
"Package os",
[]string{
"<https://golang.org/>",
"<https://golang.org/pkg/>",
},
},
}
go
package main
import (
"fmt"
"sync"
)
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error)
}
func Crawl(url string, depth int, fetcher Fetcher) {
if depth <= 0 {
return
}
visited := make(map[string]bool)
var mu sync.Mutex
var wg sync.WaitGroup
queue := []string{url}
for len(queue) > 0 {
url := queue[0]
queue = queue[1:]
mu.Lock()
if visited[url] {
mu.Unlock()
continue
}
visited[url] = true
mu.Unlock()
wg.Add(1)
go func(url string) {
defer wg.Done()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
mu.Lock()
for _, u := range urls {
if !visited[u] {
queue = append(queue, u)
}
}
mu.Unlock()
}(url)
}
wg.Wait()
}
func main() {
Crawl("<https://golang.org/>", 4, fetcher)
}
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
var fetcher = fakeFetcher{
"<https://golang.org/>": &fakeResult{
"The Go Programming Language",
[]string{
"<https://golang.org/pkg/>",
"<https://golang.org/cmd/>",
},
},
"<https://golang.org/pkg/>": &fakeResult{
"Packages",
[]string{
"<https://golang.org/>",
"<https://golang.org/cmd/>",
"<https://golang.org/pkg/fmt/>",
"<https://golang.org/pkg/os/>",
},
},
"<https://golang.org/pkg/fmt/>": &fakeResult{
"Package fmt",
[]string{
"<https://golang.org/>",
"<https://golang.org/pkg/>",
},
},
"<https://golang.org/pkg/os/>": &fakeResult{
"Package os",
[]string{
"<https://golang.org/>",
"<https://golang.org/pkg/>",
},
},
}
이 코드는 Go 언어의 동시성 기능을 활용하여 웹 크롤러를 병렬로 처리하는 예제입니다. Crawl 함수를 수정하여 URL을 병렬로 가져오면서도 같은 URL을 두 번 가져오지 않도록 했습니다. 고루틴, 뮤텍스, 작업 그룹을 사용하여 동시성을 제어하고 동기화를 수행했죠.
이렇게 Go 언어의 동시성 기능을 활용하면 효율적이고 빠른 웹 크롤러를 만들 수 있습니다. 병렬 처리를 통해 크롤링 속도를 높이고, 동기화 기술을 사용하여 안전하게 데이터를 처리할 수 있죠.










