• Feed
  • Explore
  • Ranking
/
/
    Go

    gRPC 클라이언트 캐싱 구현하기

    Go로 구현하는 gRPC Client 캐싱
    gRPCGoDevOps
    A
    Antonio
    2025.03.10
    ·
    6 min read

    3847

    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 캐싱 원리

    1. grpcClients.Load(target)

      • target(예: "server1:50051")에 대한 gRPC 클라이언트가 sync.Map에 저장되어 있는지 확인

      • 저장되어 있다면, 기존 클라이언트를 반환하여 재사용 (새로운 연결을 생성하지 않음)

    2. grpc.Dial(target)

      • sync.Map에 없으면, 새로운 gRPC 연결을 생성하고 클라이언트를 만든 후, sync.Map에 저장하여 이후 요청에서 재사용할 수 있도록 함

    3. 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()을 실행하면, 불필요한 연결이 계속 생성되면서 아래와 같은 문제가 발생할 수 있습니다.

    1. 불필요한 TCP 커넥션 증가

      • 매 요청마다 grpc.Dial()이 실행되면 새로운 TCP 연결이 만들어지므로, 서버에 부하가 발생

      • 오래된 연결이 남아있는 경우 메모리 누수 및 성능 저하 문제 발생 가능

    2. gRPC 커넥션 풀의 장점을 활용하지 못함

      • gRPC는 내부적으로 연결 풀(connection pool)을 사용하여 여러 요청을 같은 연결에서 처리할 수 있도록 최적화되어 있음

      • 클라이언트를 재사용하지 않으면, 매번 새로운 연결이 생성되어 gRPC의 최적화 기능을 활용할 수 없음

    3. 네트워크 비용 증가

      • grpc.Dial() 호출 시 TLS 핸드셰이크나 인증 과정이 반복되므로 네트워크 및 인증 부하 발생


    sync package - sync - Go Packages
    https://pkg.go.dev/sync






    - 컬렉션 아티클