고루틴 waitgroups를 잘못 쓰고 있었다.

dasd412·2024년 10월 2일
0

실무 문제 해결

목록 보기
7/17

잘못 쓰고 있던 고루틴 waitgroups

배경

실무에서 서버 하나에 gRPChttp 포트를 두개 띄울 필요가 있었다.
각각 포트1과 포트2라고 했을 때, 서버 하나는 포트1과 포트2의 요청을 모두 처리할 필요가 있었다. 그래서 동시성이 필요하다고 생각하였고 고루틴을 활용하였다. 또한 gRPChttp가 계속 작동할 필요가 있다고 생각하여 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. 메인 고루틴은 waitgroupsgRPChttp 각각의 고루틴이 끝날 때까지 대기한다. 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)
	}
}

개선된 코드 분석

먼저, gRPChttp 모두 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


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글