golang
에선 context
라는 것을 사용한다.
go
공식 블로그를 보면, http
request
를 예제로 들고 있다. 다음은 그 번역이다.
서버에 들어오는 각각의 모든 request
에 대해 go
서버는 새로운 고루틴을 생성한다. 이를 통해 서버는 여러 요청을 동시에 처리할 수 있다.
이 하나의 request
는 jwt
, deadline
등 여러가지 문맥 정보를 담고 있다. 만약 하나의 요청이 취소되거나 타임아웃되면, 해당 요청에 대한 모든 고루틴은 빠르게 종료되고 자원이 회수되어야 한다. 그래서 컨트롤러, 서비스, 리포지토리 등 여러 레이어에 걸쳐있으면서, request
에 엮인 모든 고루틴을 다루기 쉽도록 context
를 개발했다고 한다.
위 내용을 이해할 수 있으려면, 하나의 요청이 서버의 자원을 점유한다는 것과 자원 점유가 지나치면 서버가 죽을 수 있다는 것을 알고 있어야 한다.
정상적인 상황을 먼저 그림으로 들어보자.
각 클라이언트의 요청은 서버를 경유하여 DB
등의 자원을 점유하게 된다. 각 요청과 점유하는 자원은 똑같은 색으로 표시하였다.
그런데 DB
등의 자원을 사용하고자 할 때, 네트워크 에러가 발생한다면? gRPC
나 네트워크 패킷이 너무 늦게 도착한다면? MSA
인데 다른 마이크로 서비스가 죽어버렸다면?
이 때, 빠른 실패
처리를 위한 타임 아웃
이나 서킷 브레이커
등이 없다고 해보자. 그러면 어떻게 될까?
제일 처음 문제가 된 요청에서 block
이 발생할 것이고, 레이어에 걸친 모든 요청 역시 block
될 것이다. 즉, 문제가 연쇄적으로 전파된다.
그런데 이러한 에러는 다른 요청에 대해서도 비슷한 문제 상황을 발생시킬 가능성이 크다.
시간이 지나면서 다음 그림처럼 쓸데 없는 자원 점유가 서버에 퍼져나갈 것이다.
이처럼 사소한 버그나 에러, 흔히 발생하는 네트워크 문제로 서버 전체가 죽을 수가 있다.
따라서 golang 개발자들은 이러한 문제가 발생하지 않도록, 여러 레이어에 걸쳐있으면서 하나의 요청과 연관된 모든 고루틴들을 빠르게 실패
하도록 context를 개발한 것이라 볼 수 있다.
이러한 context를 이용해 에러가 레이어를 넘어 전파되는 것을 방지할 수 있고, 최종적으로 서버가 뻗는 것을 예방할 수 있다.
context
는 자료구조 트리처럼 최상위 root context
에서 파생된다. root context
는 Background()
나 TODO()
메서드들을 이용해 생성되며, 파생되는 context
들은 주입된 context
에서 WithCancel
, WithDeadline
, WithTimeout
, WithValue
로 생성된다. (자료구조 트리에서 리프 노드로 갈라지는 것을 상상해보자.)
WithValue
를 제외하면 모든 파생 메서드들은 CancelFunc
라는 취소 전용 함수를 리턴한다.
해당 함수를 사용하게 되면, 자식 context
와 그 후손 context
의 모든 작업을 취소시킨다. 뿐만 아니라 부모 context
가 자식 context
에 대한 참조를 삭제하게 한다.
이 역시 의존되는 레이어(자식 context
)에서 문제가 발생할 경우 의존하는 레이어(부모 context
)에게 문제가 전파되지 않도록 하는 조치라 볼 수 있다.
https://go.dev/blog/context
https://btree.dev/golang-context
릴리즈의 모든 것