URL Checker (w. Goroutine, chanel)

최준호·2022년 11월 1일
0

Go

목록 보기
9/9
post-thumbnail

📗 url checker project

📄 url 호출 해보기

참고 go http 문서

go에서 제공하는 http를 가져다 사용하면 되는데

package main

import (
	"errors"
	"fmt"
	"net/http"
)

var errRequestFail = errors.New("Request Fail")

func main() {
	urls := []string{
		"https://www.google.com",
		"https://www.facebook.com",
		"https://www.naver.com",
	}
	for _, url := range urls {
		hitURL(url)
	}
}

func hitURL(url string) error {
	fmt.Println("check url :", url)
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFail
	}
	return nil
}

http.Get http.Post 등 페이지에 요청해볼 수 있다.

실제 호출이 일어나고 에러 없이 반환됨을 확인할 수 있었다.

📄 map으로 반환 받아보기

var result = make(map[string]string)
var result = map[string]string{}

참고로 위 두 코드는 동일한 코드이다. map으로 선언된 result를 최초로 초기화해주어야 그 후에 값을 넣을 수 있기 때문에 make를 사용하거나 {}로 초기화 해주어야한다.

package main

import (
	"errors"
	"fmt"
	"net/http"
)

var errRequestFail = errors.New("Request Fail")

func main() {
	var results = make(map[string]string)

	urls := []string{
		"https://www.google.com",
		"https://www.facebook.com",
		"https://www.naver.com",
	}

	for _, url := range urls {
		result := "OK"
		err := hitURL(url)
		if err != nil {
			result = "FAILED"
		}
		results[url] = result
	}
	for url, result := range results {
		fmt.Println(url, result)
	}
}

func hitURL(url string) error {
	fmt.Println("check url :", url)
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode >= 400 {
		return errRequestFail
	}
	return nil
}

다음과 같이 url들을 하나씩 체크해보면

모두 정상 처리됨을 더 확실하게 알수 있다.

📗 Goroutine

위에서 Url Checker를 실행해보면서 하나씩 체크하는 것이 아니라 모든 처리를 동시에 진행할 순 없을까? 라는 생각이 들수 있다. 왜냐면 굳이 구글을 먼저 체크하고 페이스북을 체크하고 네이버를 체크하면서 순서대로 체크할 필요가 없이 한번에 다 체크하는게 더 빠르기 때문이다.

그래서 go에서는 goroutine을 통해 간단하게 지원해준다.

📄 goroutine 예제

package main

import (
	"fmt"
	"time"
)

func main() {
	sexyCount("juno")
	sexyCount("richard")
}

func sexyCount(name string) {
	for i := 0; i < 10; i++ {
		fmt.Println(name, "is sexy", i)
		time.Sleep(time.Second * 1)
	}
}

해당 코드를 작성하고 우리는 결과를 보기 위해 총 20초를 기다려야한다.

이 결과를 모두 대기하고 있어야하기 때문이다.

여기서 goroutine을 적용하면 한번에 처리할 수 있게 되는데

package main

import (
	"fmt"
	"time"
)

func main() {
	go sexyCount("juno")
	sexyCount("richard")
}

func sexyCount(name string) {
	for i := 0; i < 10; i++ {
		fmt.Println(name, "is sexy", i)
		time.Sleep(time.Second * 1)
	}
}

다음과 같이 총 10초만에 결과를 모두 받아볼 수 있게 된다. 여기서 우리는 main 내에 go라는 예약어 하나만 붙여주었는데. 이 go 하나만으로 해당 함수를 비동기로 처리할 수 있게 되었다. 그럼 둘다 비동기로 만들어버리면 어떻게 될까

package main

import (
	"fmt"
	"time"
)

func main() {
	go sexyCount("juno")
	go sexyCount("richard")
}

func sexyCount(name string) {
	for i := 0; i < 10; i++ {
		fmt.Println(name, "is sexy", i)
		time.Sleep(time.Second * 1)
	}
}

다음과 같이 두 함수를 모두 비동기로 만들어버리면

다음과 같이 main이 바로 종료되어버린다. 그 이유가 뭘까?

go가 함수를 처리하는 과정을 그림으로 보면 다음과 같은데 function 1~3까지를 순서대로 스택에 쌓고 들어온 순서대로 처리하는 것이다.

이 그림은 Java 개발자들이라면 JVM 구조와 똑같다고 생각할 수 있을거 같다.

근데 goroutine은 이 구조를

이렇게 동시에 처리하도록 해주는 것이다. 그럼 왜 모두 go를 붙였을 때 실행이 되지 않았을까?

이건 제 개인적인 생각입니다!

모두 go를 붙여서 처리했을 때 main과 동일하게 실행하게 되어서 main이 종료되는 시점에 모두 종료되어 버리기 때문에 아무것도 실행이 되지 않은것 처럼 보이는 것이다.

📗 Chanel

우리는 위에서 goroutine을 통해 함수들을 동시에 처리하도록 만들어봤다. 근데 만약에 비동기로 처리한 뒤에 응답값을 가져오고 싶을 때가 있다면 어떻게 해야할까?

go는 그때를 위해 Chanel을 만들어 두었다. 비동기로 처리하는 함수여도 go가 해당 응답을 자동으로 찾아주는 것이다.

위에서 모두 비동기로 처리된 코드를

package main

import (
	"time"
)

func main() {
	c := make(chan bool)	// make를 통해 채널을 생성
	go isSexy("juno", c)
	go isSexy("richard", c)
}

func isSexy(name string, c chan bool) {
	time.Sleep(time.Second * 2)
	if name == "juno" {
		c <- false	// 매개변수로 받은 채널에 값을 넣어준다.
	} else {
		c <- true
	}
}

다음과 같이 코드를 수정했다. 이 코드를 실행시키면 위와 같이 그냥 똑같이 종료되어버린다. 하지만 go는 chanel을 통해 응답값을 가져오고 있는것이다. 우리는 위 코드를 통해 go에서 채널을 생성하고 채널에 값을 넣어봤다.

이제 go에서 채널을 통해 가져온 값을 가져오는지 확인해보자.

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan bool) // make를 통해 채널을 생성
	go isSexy("juno", c)
	go isSexy("richard", c)
	fmt.Println(<-c)
	fmt.Println(<-c)
}

func isSexy(name string, c chan bool) {
	time.Sleep(time.Second * 2)
	fmt.Print(name, " ")
	if name == "juno" {
		c <- false // 매개변수로 받은 채널에 값을 넣어준다.
	} else {
		c <- true
	}
}

다음 코드를 통해 실행해보면 되는데 <-c를 통해서 채널에 들어가 있는 값을 반환받을 수 있고 go는 채널을 통해 반환받는 값을 기다려준다. 실제로 실행해보면 아까처럼 바로 종료되는 것이 아닌 응답을 기다려주고

결과 값을 찍어낼 수 있는 것을 확인할 수 있다.

📗 더 빠른 url checker

위에서 만들었던 UrlChecker를 더 빠르게 만들어보자

📄 코드 작성

package main

import (
	"errors"
	"fmt"
	"net/http"
)

var errRequestFail = errors.New("Request Fail")

type result struct {
	url    string
	status string
}

func main() {
	c := make(chan result) //chanel 생성

	urls := []string{
		"https://www.google.com",
		"https://www.facebook.com",
		"https://www.naver.com",
		"https://www.daum.net",
		"https://www.kakaocorp.com/page/",
	}

	for _, url := range urls {
		go hitURL(url, c)
	}

	for _, url := range urls {
		fmt.Println(url, " is", <-c)
	}

}

func hitURL(url string, c chan<- result) { // chanel is send only
	fmt.Println("check url :", url)
	resp, err := http.Get(url)

	status := "OK"
	if err != nil || resp.StatusCode >= 400 {
		status = "FAIL"
	}
	c <- result{url: url, status: status}
}

다음과 같이 기존의 동기식 방식에서 goroutine과 chanel을 사용하여 코드를 작성해보았다. 훨신 빠르게 실행되는 것을 확인할 수 있었고 결과도 잘 나오는 것을 확인할 수 있었다.

진짜 go는 너무 간단하게 메모리와 쓰레드를 관리할 수 있음에 놀라울 뿐이다. 그리고 이 강의가 2년 전 강의라는 것에 내가 2년만 더 빨리 공부했다면 좋았을텐데... 라는 생각 뿐이였다. 그리고 2년 동안 더 엄청난 언어가 되었을지 기대된다...

profile
해당 주소로 이전하였습니다. 감사합니다. https://ililil9482.tistory.com

0개의 댓글