본문 바로가기

클라우드 기반

[GoLang] Select, Reflection, Sync, Context

Select

HTTP GET으로 두개의 URL을 가지고 먼저 반환된 URL을 반환하여 "경쟁"하는 SebSiteRacer라는 함수를 만들어야 한다고 하자. 10초 이내에 반환되는 항목이 없으면 오류를 반환해야 함. 어떻게 해야할까?

 

=>

 

경쟁을 하게끔 하려면 고루틴을 쓰되 채널링을 안 쓰면 되지 않을까 싶음.

1 http 관련 네트워크 통신을 써야 하므로 net/http 라이브러리를 쓰면 될 것이고

2 net/http/httptest를 통해 테스트를 하면 될 것.

 

우선 이를 테스트 하기 위해서 테스팅 함수부터 만듦. 아래의 함수는 분명 완벽하지 않으나, 예제들에서 지속적으로 강조하는 건 "완벽하지 않은 코드를 짜라. 완벽 추구했다가는 내일이 되도 못 짠다"는 거다. 무조건 일단 짜고 보라는 것.

package select
import testing

func TestRacer(t *testing.T){
    slowURL := "http://www.facebook.com"
    fastURL := "http://www.quii.co.uk"
    
    got := Racer(slowURL, fastURL)
    want := fastURL
    
    if got != want {
    	t.Errorf("got %q, want %q", got, want)
    }
}

 

이게 테스트라고, 테스트의 대상이 되는 Racer 함수는 뼈대만 두면 아래와 같음.

import http/net

func Racer(a, b string) (winner string) {
    startA := time.Now()
    http.Get(a)
    aDuration := time.Since(startA)
    
    startB := time.Now()
    http.Get(b)
    bDuration := time.Since(startB)
    
    if aDuration < bDuration {
    	return a
    }
    
    return b
}

 

자 여기서 새로운 함수들 몇개를 얻어낼 수 있음.

 

1 defer를 붙이면 해당 함수를 포함하는 함수의 끝에서 호출함. 이는 서사적으로 진행되는 일반적인 흐름에 반하는 것으로, 함수가 끝나면 비로소 실행되기를 원하지만 코드를 읽을 타인을 위해 서버를 생성한 위치 근처에 명령어를 보관하기 위함.

2 select 구조를 쓰면, 동기화 프로세스를 쉽고 명확하게 하는 데에 도움이 됨.

 

두가지 방식이 가능한데, 첫번째는 select-case를 극대화 시키는 방식.

package concurrency

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

// Racer 함수는 a와 b URL 중 더 빨리 응답하는 URL을 반환합니다.
// 만약 10초 내에 두 URL 모두 응답이 없으면 에러를 반환합니다.
func Racer(a, b string) (winner string, err error) {
	// select 문은 여러 채널 작업을 기다리다가, 가장 먼저 준비되는 하나를 선택합니다.
	select {
	case <-ping(a): // a URL로부터 응답이 오면 이 케이스가 선택됩니다.
		return a, nil
	case <-ping(b): // b URL로부터 응답이 오면 이 케이스가 선택됩니다.
		return b, nil
	case <-time.After(10 * time.Second): // 10초가 지나면 이 케이스가 선택됩니다.
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

// ping 함수는 주어진 URL에 HTTP GET 요청을 보내고,
// 응답이 오면 닫히는 채널을 반환하는 '신호' 역할을 합니다.
func ping(url string) chan struct{} {
	// struct{} 타입의 채널은 데이터를 전달하기 위함이 아니라,
	// 단순히 신호를 보내는 용도로 사용할 때 유용합니다. (메모리를 차지하지 않음)
	ch := make(chan struct{})
	go func() {
		http.Get(url) // GET 요청을 보냅니다. 에러 처리는 예시를 위해 생략.
		close(ch)     // 작업이 끝나면 채널을 닫아서 신호를 보냅니다.
	}()
	return ch
}

 

select 문의 각각의 case에서 서로 다른 채널을 각각 기다릴 수 있기 때문에, 하나의 작업 당 하나의 채널 패턴이 select의 구조와 자연스럽게 맞아떨어짐. 특히 소수의, 정해진 개수의 작업을 경쟁시키는 지금과 같은 상황에서 직관적인 패턴이라고 할 수 있음. 물론 a와 b가 엄밀한 의미에서의 "동시 출발"은 아니지만, 사람의 관점에서 봤을 때는 그렇다고 봐도 무방함.

 

* 아래와 같이 표현하는 게 좀 충격이긴 함. case 하고 조건절에 bool 절이 아니라 <- ping 이런 식으로 가능하네,,;;

 

 

그러나 위에서 우리가 다룬 것처럼, 하나의 채널 내에서 여러개의 고루틴이 돌아가는 의도대로 하려면 또 이렇게도 가능함.

package concurrency

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

// RacerV2 함수는 공유된 단일 채널을 사용하여 경쟁을 구현합니다.
func RacerV2(a, b string) (winner string, err error) {
	// 1. 모든 고루틴이 결과를 보낼 '단 하나의 공유 채널'을 만듭니다.
	// 버퍼를 1로 주어, 가장 먼저 끝난 고루틴이 블로킹 없이
	// 즉시 결과를 채널에 넣고 사라질 수 있도록 할 수 있습니다. (선택사항)
	ch := make(chan string, 1)

	// 2. 각 URL에 대해 고루틴을 시작하고, '공유 채널 ch'를 전달합니다.
	go func(url string) {
		http.Get(url)
		ch <- url // 작업이 끝나면 공유 채널 ch에 자신의 url을 보냅니다.
	}(a)

	go func(url string) {
		http.Get(url)
		ch <- url // 이 고루틴도 똑같은 공유 채널 ch에 url을 보냅니다.
	}(b)

	// 3. select 문은 여전히 유효합니다.
	// ch 채널과 타임아웃 채널을 동시에 기다립니다.
	select {
	case winner := <-ch: // a와 b 중 먼저 끝난 고루틴이 보낸 결과가 ch 채널에 도착합니다.
		return winner, nil
	case <-time.After(10 * time.Second):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

 

이렇게 가능함. 

 

 

Reflection

프로그램이 실행 중에 자기 자신의 구조를 살펴보고 조작할 수 있게 하는 기능. "거울"을 통해 자기 자신을 비춰보는 것과 같음.

컴파일 시점에는 알 수 없었던 동적인 타입 정보를 런타임에 알아내서 처리할 수 있게 해주는 메타프로그래밍 기능임.

 

예를 들어 fmt.Println 등의 범용적인 프린트 함수가 이에 해당됨. fmt.Println이 어떤 타입의 변수든 알아서 예쁘게 출력할 수 있는 것도 내부적으로 리플렉션을 활용하여 동적으로 판단하기 때문임.

 

아래와 같은 코드가 reflection의 예시.

func inspect(i interface{}) {
	fmt.Printf("\n--- Inspecting: %+v ---\n", i)

	// i의 실제 값(Value)과 타입(Type) 정보를 가져옵니다.
	v := reflect.ValueOf(i)
	t := reflect.TypeOf(i)
	// 또는 t := v.Type()

	fmt.Printf("타입(Type): %s\n", t.Name())   // 타입의 이름 (예: User)
	fmt.Printf("종류(Kind): %s\n", t.Kind()) // 타입의 종류 (예: struct, int)

	// 만약 종류가 구조체(struct)라면, 필드를 순회해봅니다.
	if t.Kind() == reflect.Struct {
		fmt.Println("필드(Fields):")
		// .NumField()는 필드의 개수를 반환합니다.
		for j := 0; j < t.NumField(); j++ {
			field := t.Field(j) // j번째 필드의 타입 정보
			value := v.Field(j) // j번째 필드의 값 정보

			// PkgPath가 비어있지 않으면 비공개(unexported) 필드입니다.
			// 비공개 필드의 값에는 접근할 수 없습니다.
			if field.PkgPath != "" {
				fmt.Printf("  %d: %s (%s) = <비공개 필드>\n", j, field.Name, field.Type)
			} else {
				fmt.Printf("  %d: %s (%s) = %v\n", j, field.Name, field.Type, value.Interface())
			}
		}
	}
}

 

그러나 단점도 있음. 런타임에 타입 분석을 하기 때문에 일반적인 go 언어의 특성을 살리지 못하고 훨씬 느리다는 단점.

 

Context

소프트웨어는 종종 오랜 기간 실행되고 리소스를 많이 점유하는 프로세스를 실행하는데, 어떤 이유로 취소 혹은 실패할 경우 프로그램에서 실행된 프로세스들을 일관된 방법으로 멈춰줄 필요가 있음. 이 챕터에서는 context 패키지의 도움을 받아 오래 실행되는 프로세스를 관리해

볼 것. 웹 서버에서, 데이터를 가져오는 와중에 사용자가 요청을 취소하는 경우를 가정하고 이때 프로세스가 중단될 수 있도록 하면? 

 

일단 가정할 상황은 이렇다.

 

더 이상 실행할 필요가 없는 작업을 알아채고 작업 중지를 할 수 있게끔 하는 게 context Package라고 함.

 

type StubStore struct {
    response string
}

func (s *StubStore) Fetch() string {
    return s.response
}

func TestServer(t *testing.T) {
    data := "hello, world"
    svr := Server(&StubStore{data})

    request := httptest.NewRequest(http.MethodGet, "/", nil)
    response := httptest.NewRecorder()

    svr.ServeHTTP(response, request)

    if response.Body.String() != data {
        t.Errorf(`got "%s", want "%s"`, response.Body.String(), data)
    }
}

 

svr := Server(&StubStore{data})

 

이 부분을 봐도 알 수 있겠지만, data 부분은 response랑 매치가 됨. response를 명시하지 않아도 된다는 것. 변수를 넘겨주면, 구조체의 순서대로 들어감.

 

이외에도 context 관련한 여러가지가 있지만, 이제 이론적인 것들보다는 실전으로 쳐가면서 배우는 걸로.