실무에서 서버 하나에 gRPC
와 http
포트를 두개 띄울 필요가 있었다.
각각 포트1과 포트2라고 했을 때, 서버 하나는 포트1과 포트2의 요청을 모두 처리할 필요가 있었다. 그래서 동시성이 필요하다고 생각하였고 고루틴을 활용하였다. 또한 gRPC
와 http
가 계속 작동할 필요가 있다고 생각하여 waitgroups
를 활용하여 고루틴이 끝날 때까지 기다리게 하였다.
다음은 문제가 되는 server.go 코드이다. 사수 분의 코드 리뷰를 받고 문제점을 알게 되었다.
func main() {
var wg sync.WaitGroup
wg.Add(2)
go startHTTPServer(&wg, entClient, redisClient)
go startGRPCServer(&wg, entClient)
wg.Wait()
}
func startHTTPServer(
wg *sync.WaitGroup,
entClient *ent.Client,
redisClient *redis.Client,
) {
defer wg.Done()
port := os.Getenv("SERVICE_PORT")
server := handler.NewDefaultServer(
resolver.NewSchema(
entClient,
redisClient,
),
)
server.Use(entgql.Transactioner{TxOpener: entClient})
// 서버 구동을 위한 코드는 생략...
log.Fatal(http.ListenAndServe(":"+port, nil))
}
무엇이 문제일까?
바로 waitgroups
로 인해 고루틴이 끝날 때까지 기다린다는 것과
http.ListenAndServe(":"+port, nil)
라는 코드의 경우 서버가 요청을 수신하고 처리하는 동안 계속 실행되며, 오류가 발생하거나 서버가 명시적으로 종료될 때까지 반환되지 않는다는 것 때문이다.
순서를 따지면 다음과 같다.
1. 메인 고루틴은 waitgroups
로 gRPC
와 http
각각의 고루틴이 끝날 때까지 대기한다. waitgroups
는 세마포어처럼 카운터를 사용하는데, Done()
은 그 카운터를 감소시키는 역할을 한다.
2. http
고루틴의 경우 http.ListenAndServe(":"+port, nil)
라는 코드에 의해 에러가 발생하지 않는다.
3. http
고루틴에서 에러가 발생하지 않는 한, defer wg.Done()
은 호출되지 않는다.
4. defer wg.Done()
은 호출되지 않으면 메인 고루틴은 http
고루틴을 영원히 기다리게 된다.
요약하자면, 위 코드는 끝나지 않는 고루틴을 기다리며 메인 고루틴이 영원히 대기하게 만드는 문제적 코드라는 것이다.
이는 메인 고루틴을 쓸데없이 영원히 기다리게 하므로 명백한 리소스 낭비이다.
다음은 개선된 코드이다.
func main() {
go startGRPCServer(entClient)
startHTTPServer(entClient, redisClient)
}
func startHTTPServer(
entClient *ent.Client,
redisClient *redis.Client,
) {
port := os.Getenv("SERVICE_PORT")
server := handler.NewDefaultServer(
resolver.NewSchema(
entClient,
redisClient,
),
)
//중략
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func startGRPCServer(entClient *ent.Client) {
grpcServer := grpc.NewServer()
listen, err := net.Listen("tcp", ???serverAddress)
//중략
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
if err := grpcServer.Serve(listen); err != nil {
log.Fatalf("server ended: %s", err)
}
}
먼저, gRPC
와 http
모두 Serve()
, ListenAndServe()
라는 메서드를 이용해 무한 대기한다. 따라서 메인 고루틴에서 waitgroups
를 사용할 필요가 없어져서 삭제했다.
그리고 http
요청을 메인 고루틴에서 처리하게 함으로써 불필요한 리소스 낭비를 줄였다.
https://www.scalent.io/golang/waitgroups-in-golang-in-detail/
https://pkg.go.dev/net/http#ListenAndServe
https://pkg.go.dev/sync