
gRPC 클라이언트 객체
gRPC 클라이언트는 grpc.Dial() 을 통해 생성되는데, 이 과정에서 네트워크 연결을 초기화하고, TLS 핸드셰이크를 수행하며, 내부적으로 커넥션 풀을 관리합니다. 즉, grpc.Dial() 을 매번 호출하면 연결이 생성되어 비효율적일 수 있습니다.
일반적인 gRPC 클라이언트 생성 방식
conn, err := grpc.Dial("server-address:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
client := pb.NewMyServiceClient(conn)
sync.Map을 활용한 gRPC 클라이언트 캐싱
클라이언트 생성 비용 절감을 위해 sync.Map을 활용하면 멀티 쓰레드 환경에서도 안전하게 클라이언트를 공유할 수 있습니다.
import (
"sync"
"google.golang.org/grpc"
pb "example.com/project/proto"
)
// gRPC 클라이언트 캐싱을 위한 전역 변수
var grpcClients sync.Map
// gRPC 클라이언트 반환 함수
func getGRPCClient(target string) (pb.MyServiceClient, error) {
// 1. sync.Map에서 클라이언트를 찾음
if client, ok := grpcClients.Load(target); ok {
return client.(pb.MyServiceClient), nil
}
// 2. 없으면 새로운 gRPC 클라이언트를 생성
conn, err := grpc.Dial(target, grpc.WithInsecure())
if err != nil {
return nil, err
}
client := pb.NewMyServiceClient(conn)
// 3. 생성된 클라이언트를 sync.Map에 저장
grpcClients.Store(target, client)
return client, nil
}
sync.Map 캐싱 원리
grpcClients.Load(target)target(예:"server1:50051")에 대한 gRPC 클라이언트가sync.Map에 저장되어 있는지 확인저장되어 있다면, 기존 클라이언트를 반환하여 재사용 (새로운 연결을 생성하지 않음)
grpc.Dial(target)sync.Map에 없으면, 새로운 gRPC 연결을 생성하고 클라이언트를 만든 후,sync.Map에 저장하여 이후 요청에서 재사용할 수 있도록 함
grpcClients.Store(target, client)생성된 gRPC 클라이언트를
sync.Map에 저장하여 다음 요청에서 동일한 클라이언트를 재사용할 수 있도록 함
gRPC 클라이언트 연결 종료
sync.Map을 사용하면 gRPC 클라이언트 객체를 계속 캐싱하기 때문에, 특정 시점에서 연결을 닫아야 할 수도 있습니다.
// gRPC 클라이언트 캐싱을 위한 전역 변수
var grpcClients sync.Map
func closeAllGRPCConnections() {
grpcClients.Range(func(key, value interface{}) bool {
if client, ok := value.(pb.MyServiceClient); ok {
if conn, ok := client.(*grpc.ClientConn); ok {
conn.Close()
}
}
return true
})
}
sync.Map.Range()를 사용하여 모든 클라이언트를 순회하면서 연결을 닫습니다.서버가 종료될 때 한 번만 실행하여 캐싱된 연결을 정리할 수 있습니다.
sync.Map 캐싱 장점
연결 재사용:
grpc.Dial()을 매번 호출하지 않아 성능 최적화멀티 쓰레드 안전:
sync.Map을 사용하여 여러 고루틴에서 동시 접근 가능효율적인 리소스 관리: 필요할 때만 연결을 생성하고, 이후 재사용
클라이언트 캐싱을 사용하지 않을 경우의 문제점
만약 sync.Map 없이 요청마다 grpc.Dial()을 실행하면, 불필요한 연결이 계속 생성되면서 아래와 같은 문제가 발생할 수 있습니다.
불필요한 TCP 커넥션 증가
매 요청마다
grpc.Dial()이 실행되면 새로운 TCP 연결이 만들어지므로, 서버에 부하가 발생오래된 연결이 남아있는 경우 메모리 누수 및 성능 저하 문제 발생 가능
gRPC 커넥션 풀의 장점을 활용하지 못함
gRPC는 내부적으로 연결 풀(connection pool)을 사용하여 여러 요청을 같은 연결에서 처리할 수 있도록 최적화되어 있음
클라이언트를 재사용하지 않으면, 매번 새로운 연결이 생성되어 gRPC의 최적화 기능을 활용할 수 없음
네트워크 비용 증가
grpc.Dial()호출 시 TLS 핸드셰이크나 인증 과정이 반복되므로 네트워크 및 인증 부하 발생