개요
[GoLang] Go에서 동시성이란
Go 스케줄링고루틴 : Go의 최소 실행 단위로, main도 profile도 모든 게 다 고루틴고의 동시성에서는 Go 스케줄러로 어떻게 효율적으로 관리하느냐에 크게 의존합니다. goroutine 생성새로운 작업(Gorou
seung.tistory.com
요즘 고루틴에 대한 기억을 다시 떠올리며 공부하고 있습니다.
생각해 보니 GoLang을 http 서버로 이용할 때도 고루틴으로 선언 안 해도 모든 요청을 고루틴으로 돌리더군요.
즉 샘플 코드를 안만들어도, 이렇게 가까이에 좋은 예제가 있던 겁니다.
다음처럼 아주 간단한 샘플 코드를 만들어봅시다.
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
log.Println("Starting server on localhost:8080")
if err := http.ListenAndServe("localhost:8080", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
이 예제에서 localhost:8080으로 요청을 동시적으로 수만 건을 보내도 고는 동시적으로 처리할 겁니다.
ListenAndServe에서 이 부분을 처리하기 때문이죠.
이는 go의 기본 라이브러리인 http 패키지에서 제공하는 함수이고, 이번에 설명할 전체 내부 로직은 아래와 같습니다.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
func (s *Server) ListenAndServe() error {
if s.shuttingDown() {
return ErrServerClosed
}
addr := s.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
func (s *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(s, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := s.setupHTTP2_Serve(); err != nil {
return err
}
if !s.trackListener(&l, true) {
return ErrServerClosed
}
defer s.trackListener(&l, false)
baseCtx := context.Background()
if s.BaseContext != nil {
baseCtx = s.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, s)
for {
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
이 코드는 우선 ListenAndServe로 TCP 리스너를 만들고, 이걸 s.Serve로 실행시킵니다.
TCP 리스너로 만듦으로 정해진 포트로 들어오는 TCP 연결을 받아 HTTP 요청을 처리하죠
func (s *Server) ListenAndServe() error {
...
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
Serve에서 아래의 순서대로 동작합니다.
- l.Accept() 호출을 통해 새로운 TCP 연결을 수신해서 무한루프에서 요청이 들어올 때까지 대기
- 에러 발생 시, 백오프(back-off) 전략을 사용하여 잠시 대기한 후 다시 연결을 수신
- 서버의 context에 정보를 넣어 이후 단계에서 활용할 수 있게 함
- 고루틴을 생성함으로써 HTTP 요청은 모두 독립적이고 병렬적으로 실행
func (s *Server) Serve(l net.Listener) error {
...
ctx := context.WithValue(baseCtx, ServerContextKey, s)
for {
// TCP 요청 대기
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
// 에러 발생시 재시도
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
// 일시적이지 않은 에러는 에러 반환
return err
}
// 새로 수신한 연결에 대해 컨텍스트 확장
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
// 에러가 없으니, 기다리지 않음
tempDelay = 0
c := s.newConn(rw)
// 연결 상태를 새 연결로 설정
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
// 수신된 연결마다 새로운 고루틴 생성하여 요청 처리
go c.serve(connCtx)
}
}
여태까지 잘 사용해 왔던 http 서버는 이런 식으로 돌아가던 겁니다.
회사 코드 중에 아래와 같이 돌아가던 코드가 있는데
이건 왜 이렇게 하신지 모르겠네요, 5년 전에 작성했고 그분은 이미 퇴사했습니다...
제가 모르는 분야가 있을 수 있으니 수정은 보류입니다
go func() {
if err := server.ListenAndServe(); err != nil {
// elog.Info(1, fmt.Sprintf("starting server error : %v", err))
logger.Errorf("starting server error : %v\n", err)
}
}()
'Go Lang > Study' 카테고리의 다른 글
[GoLang] GC 튜닝 Profiling (0) | 2025.02.05 |
---|---|
[GoLang] Garbage Collector 개념 (0) | 2025.01.29 |
[GoLang] 추상 팩토리 디자인 사용해보기 (1) | 2025.01.17 |
[GoLang] GraphQL API 만들기 part 1 (라이브러리 탐색) (38) | 2024.03.01 |
[GoLang] GoLang 면접 질문 정리 (2) | 2024.02.26 |