본문 바로가기
golang

[golang] go context의 활용1(go context사용법 및 예제1)

by devjh 2021. 12. 7.
반응형

이번 게시글에서는 go context를 활용 예제를 정리합니다.

 

1. go context란

golang을 사용하다보면, 메서드를 호출할때

context를 아규먼트로 요구하는 경우가 종종 있습니다.

context를 아규먼트로 넘겨서 사용할때, 주의해야 할 점과

직접 context를 활용한 개발을 할때 어떻게 활용하는지 간단한 예제를 만들어봤습니다.

 

go context가 처음이라면 해당 게시글에 어떤 기능들이 있는지 간단히 정리해놨습니다.

 

[golang] golang 컨텍스트(context)란

이번 게시글에서는 golang의 context에 대해 정리합니다. 1. context란 운영체제를 공부할때 들었던 컨텍스트 스위칭(문맥교환으로 외웠던)에 사용된 용어 입니다. 고랭의 context도 비슷합니다. 상태나

frozenpond.tistory.com

 

2. go context는 어떤 상황에서 사용할수 있을까?

go context는

"문맥을 유지하여 생명주기를 쉽게 제어하고 불필요한 작업을 수행하지 않기 위해서 사용한다"

라고 쓰여있습니다.

예제화 된게 부족한것 같아 예제를 만들어봤습니다.

(golang 관련 예제는 csdn(중국)과 mideum, geeksforgeeks 를 참고하여 만들었습니다)
개발해야하는 요구사항은 아래와 같습니다.

(1) 요청이 오면 3개의 서버에 request를 보내고 데이터를 받아와 파싱한후 response를 내려준다.
(2) 각각의 요청은 비동기로 진행되어야 한다.
(3) 하나의 요청이라도 에러가 발생했다면 나머지서버에 요청을 기다리지않고 클라이언트에게 즉시 에러를 내려줘야한다.
(4) 하나의 요청이라도 에러가 발생했다면 나머지서버에 요청을 보내거나, 데이터를 파싱하는 작업을 진행하지 않아야 한다.

 

output

$ go run main.go
서버1: 요청 받았습니다.
서버3: 요청 받았습니다.
서버1: 시스템 에러가 발생했습니다.
server1 error
에러가 발생했으니 response부터 내립니다.
클라이언트: response 받았습니다.
context canceled
해당문맥은 종료되었으니 서버2에 요청을 보내지 않겠습니다.
해당문맥은 종료되었으니 서버3의 응답을 이용한 로직을 진행하지 않겠습니다.

 

3. 예제

package main

import (
   "context"
   "fmt"
   "github.com/pkg/errors"
   "time"
)

// context를 아규먼트로 요구하는 메서드는
// context.Background()를 만들어 넘기면 돌아갑니다.
// 메인문은 아래에서 올바르게 context를 요청하는 방식으로 수정합니다.
func main() {
	data, err := multiRequestWithCtx(context.Background())
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(data)
	}
}

func multiRequestWithCtx(parentCtx context.Context) ([]string, error) {
	var cancel context.CancelFunc
	var ctx context.Context
	ctx, cancel = context.WithCancel(parentCtx)
        defer cancel()

	result1 := make(chan string)
	result2 := make(chan string)
	result3 := make(chan string)

	// 각각의 request 전에 ctx.Done()을 확인하고
	// 이미 문맥이 종료된 상황이라면 더이상 불필요한 로직을 진행시키지 않습니다.
	go func(result1 chan string, cancel context.CancelFunc) {
		select {
		case <-ctx.Done():
			return
		default:

		}
		result, err := getDataFromServer1()
		if err != nil {
			fmt.Println("server1 error")
			// 1번서버가 바로 에러를 줬으므로 cancel()이 바로 호출됩니다.
			cancel()
			return
		}
		select {
		case <-ctx.Done():
			fmt.Println("해당문맥은 종료되었으니 서버3의 응답을 이용한 로직을 진행하지 않겠습니다.")
			return
		default:

		}
		parsedResult := fmt.Sprintf("서버1의 응답은 %s 입니다.", result)
		result1 <- parsedResult
	}(result1, cancel)

	go func(result2 chan string, cancel context.CancelFunc) {
		<-time.After(time.Millisecond * 100)
		select {
		case <-ctx.Done():
			fmt.Println("해당문맥은 종료되었으니 서버2에 요청을 보내지 않겠습니다.")
			return
		default:
		}
		result, err := getDataFromServer2()
		if err != nil {
			fmt.Println("server2 error")
			cancel()
			return
		}
		select {
		case <-ctx.Done():
			fmt.Println("해당문맥은 종료되었으니 서버2의 응답을 이용한 로직을 진행하지 않겠습니다.")
			return
		default:

		}
		parsedResult := fmt.Sprintf("서버2의 응답은 %s 입니다.", result)
		result2 <- parsedResult
	}(result2, cancel)

	go func(result3 chan string, cancel context.CancelFunc) {
		select {
		case <-ctx.Done():
			fmt.Println("해당문맥은 종료되었으니 서버3에 요청을 보내지 않겠습니다.")
			return
		default:

		}
		result, err := getDataFromServer3()
		if err != nil {
			fmt.Println("server3 error")
			cancel()
			return
		}
		select {
		case <-ctx.Done():
			fmt.Println("해당문맥은 종료되었으니 서버3의 응답을 이용한 로직을 진행하지 않겠습니다.")
			return
		default:

		}
		parsedResult := fmt.Sprintf("서버3의 응답은 %s 입니다.", result)
		result3 <- parsedResult
	}(result3, cancel)

	// 각 고루틴의 채널과, 컨텍스트의 종료를 대기합니다.
	// 세개의 서버에서 모두 응답이 왔을때(모든 채널에서 응답이 왔을때) 세개의 응답을 []string으로 내려줍니다.
	var response []string
	for {
		select {
		case <-ctx.Done():
			// 하나의 응답이라도 에러가 났다면
			// 외부에서 모든 서버의 요청의 응답을 기다릴 필요가 없습니다.
			// 해당문맥에서 cancel()이 호출되면 에러를 바로 리턴해줄수 있습니다.
			fmt.Println("에러가 발생했으니 response부터 내립니다.")
			return nil, ctx.Err()
		case result := <-result1:
			response = append(response, result)
			if len(response) == 3 {
				return response, nil
			}
		case result := <-result2:
			response = append(response, result)
			if len(response) == 3 {
				return response, nil
			}
		case result := <-result3:
			response = append(response, result)
			if len(response) == 3 {
				return response, nil
			}
		}
	}
}
func getDataFromServer1() (string, error) {
	fmt.Println("서버1: 요청 받았습니다.")
	fmt.Println("서버1: 시스템 에러가 발생했습니다.")
	return "", errors.New("Fail")
	//return "A", nil
}

func getDataFromServer2() (string, error) {
	fmt.Println("서버2: 요청 받았습니다.")
	<-time.After(time.Millisecond * 100)
	return "B", nil

}

func getDataFromServer3() (string, error) {
	fmt.Println("서버3: 요청 받았습니다.")
	<-time.After(time.Millisecond * 100)
	return "C", nil
}

컨텍스트를 사용하는 메서드 내부에서는
3개의 고루틴을 만들어 3개의 서버에 요청을 보내고 각각의 고루틴은 서버의 응답값을 파싱해 채널에 넣어줍니다.

메인루틴에서는
select를 사용하여 문맥이 종료되었는지
모든 요청이 정상적으로 돌아왔는지를 확인하며 하나의 응답이라도 에러가 발생했다면
나머지 서버의 응답을 대기하지않고 그즉시 리턴해줍니다.

3개의 고루틴 또한 자신의 문맥이 종료됐는지를 확인한 후
서버에 요청을 보내기 전에 종료됐다면 요청을 보내지않고
요청을 받은후 종료됐다면 파싱하고 채널에 넣는로직을 진행하지 않습니다.

3개의 서버에 요청을 보내는 고루틴에서는 아래의 select문이 핵심입니다. 

select {
    case <-ctx.Done():
        return
    default:
}

select문은 채널에 수신할 요청이 있는지 확인하고 default가 진행된후 종료됩니다.
각각의 고루틴은 요청을 보내기 전과 요청을 받은후 데이터를 파싱하기 전에
문맥의 종료를 확인하는 로직을 진행하도록 합니다.

메인루틴은 아래의 select문이 핵심입니다.

for {
    select {
    case <-ctx.Done():
        // 하나의 응답이라도 에러가 났다면
        // 외부에서 모든 서버의 요청의 응답을 기다릴 필요가 없습니다.
        // 해당문맥에서 cancel()이 호출되면 에러를 바로 리턴해줄수 있습니다.
        fmt.Println("에러가 발생했으니 response부터 내립니다.")
        return nil, ctx.Err()
        
    case result := <-result1:
        ~~~~~~
}

해당 문맥의 종료를 확인하고 문맥의 종료를 확인한 순간 다른 요청을 기다리지않고 바로 response를 내려줍니다. 


3. context를 포함하는 메서드를 조금더 효과적으로 실행하는 방법

func main() {
   // context를 포함하는 메서드를 호출할때는 context를 외부에서 제어할 수도 있습니다.
   // 단 cancel은 부모의 cancel이 호출됐을시 자식은 모두 Done되지만
   // 자식이 cancel됐을때 부모가 Done 되지는 않습니다.
   resultCh := make(chan string)
   ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   defer cancel()
   go func() {
      data, err := multiRequestWithCtx(ctx)
      fmt.Println("클라이언트: response 받았습니다.")
      if err != nil {
         resultCh <- err.Error()
      } else {
         resultCh <- strings.Join(data, " ")
      }
   }()
   select {
   case <-ctx.Done():
      // 자식에서 cancel이 발생해도 부모의 ctx.Done은 호출되지 않습니다.
      // 해당 ctx.Done은 부모의 timeout에서만 호출됩니다.
      fmt.Println(ctx.Err())
   case result := <-resultCh:
      fmt.Println(result)
   }
   // 메인문이 종료되면 프로세스가 내려가버리니
   // 나머지 고루틴이 불필요한 로직을 진행하지 않는걸 확인하기위해 대기합니다.
   <-time.After(time.Second * 2)
}

코드 라인이 너무 길어져 메인문에서 multiRequestWithCtx를 호출하기만 했는데

외부에서 context를 포함하는 메서드를 실행할 때는 위와같은 방식으로 호출합니다.

3개의 서버 모두 에러를 리턴하지는 않더라도 하나의 서버가 응답을 주는데 30초가 걸릴수 있다면 컨텍스트를 활용하면 쉽게 timeout을 제어할 수 있습니다.

 

고루틴, 채널, 컨텍스트를 활용해 비동기 큐를 만드는 추가 예제입니다.

 

[golang] go context의 활용2(go context사용법 및 예제2)

저번 게시글에서는 context를 활용하여 불필요한 트래픽을 제어하고, 클라이언트에게 즉시 리턴을 내려주는 방법에 대해 정리했습니다. [golang] go context의 활용1(go context사용법 및 예제1) 이번 게시

frozenpond.tistory.com

 

반응형

댓글