🔥 동시성을 활용한 웹 크롤러 만들기

759자
8분

강의 목차

Go 언어는 동시성 프로그래밍을 위한 강력한 기능들을 제공합니다. 이번 예제에서는 Go의 동시성 기능을 활용하여 웹 크롤러를 병렬로 처리하는 방법에 대해 알아보겠습니다.

먼저, Crawl 함수를 수정하여 URL을 병렬로 가져오되, 같은 URL을 두 번 가져오지 않도록 해보겠습니다.

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 맵을 사용하여 이미 가져온 URL을 추적합니다.
  • 맵의 키는 URL이고, 값은 해당 URL을 방문했는지 여부를 나타내는 불리언 값입니다.
var mu sync.Mutex
 
go
  • sync.Mutex를 사용하여 맵에 대한 동시 접근을 동기화합니다.
  • 맵은 여러 고루틴에서 동시에 접근할 수 있으므로, 뮤텍스를 사용하여 경쟁 상태를 방지합니다.
var wg sync.WaitGroup
 
go
  • sync.WaitGroup을 사용하여 생성된 고루틴들을 관리합니다.
  • 작업 그룹은 모든 고루틴이 완료될 때까지 기다리는 역할을 합니다.
queue := []string{url}
 
go
  • 재귀 호출 대신 큐를 사용하여 URL을 저장합니다.
  • 초기에는 시작 URL만 큐에 추가됩니다.
for len(queue) > 0 {
	url := queue[0]
	queue = queue[1:]
	// ...
}
 
go
  • 큐에 URL이 있는 동안 반복합니다.
  • 큐에서 URL을 꺼내고, 해당 URL에 대한 작업을 수행합니다.
mu.Lock()
if visited[url] {
	mu.Unlock()
	continue
}
visited[url] = true
mu.Unlock()
 
go
  • 뮤텍스를 사용하여 visited 맵에 대한 접근을 동기화합니다.
  • 이미 방문한 URL인 경우 건너뜁니다.
  • 방문하지 않은 URL인 경우 visited 맵에 추가합니다.
wg.Add(1)
 
go
  • 작업 그룹에 작업을 추가합니다.
  • wg.Add(1)은 작업 그룹에 새로운 작업이 추가되었음을 알립니다.
go func(url string) {
	defer wg.Done()
	// ...
}(url)
 
go
  • 고루틴을 생성하여 URL을 가져옵니다.
  • defer wg.Done()은 고루틴이 완료되면 작업 그룹에 알립니다.
  • 고루틴 내에서 URL을 가져오고, 결과를 출력합니다.
mu.Lock()
for _, u := range urls {
	if !visited[u] {
		queue = append(queue, u)
	}
}
mu.Unlock()
 
go
  • 새로 찾은 URL을 큐에 추가합니다.
  • 뮤텍스를 사용하여 visited 맵과 큐에 대한 접근을 동기화합니다.
  • 방문하지 않은 URL만 큐에 추가합니다.
wg.Wait()
 
go
  • 모든 작업이 완료될 때까지 기다립니다.
  • wg.Wait()은 모든 고루틴이 완료될 때까지 블로킹합니다.

이렇게 수정된 Crawl 함수는 URL을 병렬로 가져오면서도 같은 URL을 두 번 가져오지 않도록 합니다. 고루틴을 사용하여 동시성을 활용하고, 뮤텍스와 작업 그룹을 사용하여 동기화와 관리를 수행합니다.

전체 코드

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

이 코드는 Go 언어의 동시성 기능을 활용하여 웹 크롤러를 병렬로 처리하는 예제입니다. Crawl 함수를 수정하여 URL을 병렬로 가져오면서도 같은 URL을 두 번 가져오지 않도록 했습니다. 고루틴, 뮤텍스, 작업 그룹을 사용하여 동시성을 제어하고 동기화를 수행했죠.

이렇게 Go 언어의 동시성 기능을 활용하면 효율적이고 빠른 웹 크롤러를 만들 수 있습니다. 병렬 처리를 통해 크롤링 속도를 높이고, 동기화 기술을 사용하여 안전하게 데이터를 처리할 수 있죠.