gRPC의 내부 구조 파헤치기(2): Channel & Stub
[kt cloud 플랫폼Innovation팀 강솔 님]
gRPC의 내부 구조 파헤치기(2): Channel & Stub
이번 포스팅에서는 gRPC의 핵심 동작 원리인 채널(Channel)과 스텁(Stub)의 개념과 역할을 살펴보겠습니다. 이를 통해 gRPC 서버와 클라이언트가 어떻게 연결되고, 클라이언트가 서버의 원격 메서드를 호출하는 과정을 명확히 이해할 수 있습니다.
1. 채널(Channel)과 스텁(stub)을 통한 서버와 클라이언트 연결하기
gRPC를 사용할 경우, 클라이언트는 서버의 원격 메서드를 마치 로컬에 있는 것처럼 호출할 수 있습니다. 이는 내부적으로 채널과 스텁이 서버와 클라이언트 간의 네트워크 연결과 호출 전달을 처리하기 때문입니다.
이제 채널과 스텁이 어떻게 상호 작용하며 동작하는지에 대해 알아보겠습니다. 이를 통해 클라이언트와 서버가 gRPC를 통해 어떻게 연결되고 효율적으로 통신하는지 알 수 있습니다.
1.1. 채널(Channel) 이란?
- 역할
- 클라이언트 - 서버 간 네트워크 연결 담당을 담당하는 객체입니다.
- 특징
- 어플리케이션 시작 시 한번 생성되며, Lifecycle이 동일합니다.
- HTTP/2 프로토콜에 기반하여 다수의 gRPC 호출 처리 가능합니다.
- 로드밸런싱 및 연결 실패 시 재시도 기능 등을 처리합니다.
1.2. 스텁(Stub) 이란?
- 역할
- 스텁은 채널을 통해 서버의 메서드를 호출하는 역할 수행합니다.
- 서버의 메서드를 호출하는 인터페이스 역할을 수행합니다.
- 특징
- 스레드 안정성
- 여러 스레드에서 동일한 스텁 인스턴스를 사용해도 문제가 발생하지 않습니다.
- 서비스와 관련된 의미 있는 네이밍 사용을 권고합니다.
- 동기 및 비동기 호출 지원
- 스레드 안정성
1.3. gRPC 서버와 클라이언트 연결 과정
이제 간단한 은행 계좌 잔액 확인 서비스를 예로 들어 gRPC 서버와 클라이언트 간 연결 프로세스를 살펴보겠습니다. 이 때 채널과 스텁이 어떤 순서로 정의되는지 설명하겠습니다.
예제 서비스
- BankService
service BankService { rpc getAccountBalance(BalanceCheckRequest) return (Balance) } |
1. gRPC 서버 준비
a. 클라이언트로부터 원격 메서드 호출을 받을 수 있도록 서버를 준비합니다.
1) 예시) localhost, 6565 포트
public class GrpcServer { public static void main(String[] args) throws IOException, InterruptedException { var server = ServerBuilder.forPort(6565) .addService(new ExampleService()) .build(); server.start(); server.awaitTermination(); } } |
2.클라이언트 측에서 채널(Channel) 생성
a. 클라이언트는 ManagedChannel 객체를 사용해 서버와 연결을 설정합니다.
1)대상 서버의 IP와 Port 지정
public class GrpcClient{ public static void main(String[] args){ var channel = ManagedChannelBuilder .forAddress("localhost", 6565) // 서버 주소와 포트 지정 .usePlaintext() // SSL, TLS 연결 없이 사용(테스트용) .build(); } } |
3. 스텁(Stub) 생성 및 메서드 호출
a. 클라이언트는 앞서 생성한 채널을 사용하여 스텁 객체를 생성합니다.
1)스텁 객체는 원격 메서드 호출을 위한 인터페이스 역할을 수행합니다.
public class GrpcClient{ public static void main(String[] args){ var channel = ManagedChannelBuilder.forAddress("localhost", 6565) .usePlaintext() .build(); var bankService = BankServiceGrpc.newBlockingStub(channel); // 서버로 요청 보내고 응답 받기 var balance = bankService.getAccountBalance(BalanceCheckRequest.newBuilder() .setAccountNumber(12345) .build()); // 응답 출력 System.out.println("Balance: " + balance.getBalance()); } } |
2. 스텁(stub)을 활용한 다양한 통신 패턴 구현하기
앞서 우리는 간단하게 채널과 스텁 객체를 생성하고 이를 통해 서버의 원격 메서드를 호출하는 방법을 살펴보았습니다.
이 때 스텁 객체의 종류에 따라 다양한 통신 패턴을 구현할 수 있습니다. 스텁 객체는 크게 동기와 비동기 방식으로 구분되며, 각 방식의 특징은 다음과 같습니다.
2.1. 동기적 스텁 vs 비동기적 스텁
- 동기적 스텁은 코드가 간결하고 직관적이지만, 서버 응답을 기다리는 동안 스레드가 차단되어 자원 사용이 비효율적일 수 있습니다.
- 비동기적 스텁은 서버 응답을 기다리지 않고 다른 작업을 병렬로 수행할 수 있어, 고성능 애플리케이션에 적합합니다. 하지만 코드가 복잡해지고 디버깅이 어려워질 수 있습니다.
사용 예시
- 동기적 스텁 사용(Blocking Stub)
public class GrpcClient{ public static void main(String[] args){ var channel = ManagedChannelBuilder.forAddress("localhost", 6565) .usePlaintext() .build(); // 동기 스텁 생성 - 의미 있는 이름으로 네이밍 var bankService = BankServiceGrpc.newBlockingStub(channel); // 서버로 요청 보내고 응답 받기 var balance = bankService.getAccountBalance(BalanceCheckRequest.newBuilder() .setAccountNumber(12345) .build()); // 응답 출력 System.out.println("Balance: " + balance.getBalance()); } } |
- 비동기적 스텁 사용(Async Stub)
public class GrpcClient { public static void main(String[] args) { var channel = ManagedChannelBuilder.forAddress("localhost", 6565) .usePlaintext().build(); var bankService = BankServiceGrpc.newStub(channel); bankService.getAccountBalance(BalanceCheckRequest.newBuilder().setAccountNumber(2).build(), new StreamObserver<>() { @Override public void onNext(AccountBalance accountBalance) { // 서버에서 응답을 받았을 때 호출 System.out.println("Received account balance: " + accountBalance.getBalance()); } @Override public void onError(Throwable throwable) { // 오류가 발생했을 때 호출 System.out.println("Error: " + throwable.getMessage()); } @Override public void onCompleted() { // 서버 응답 완료 시 호출 System.out.println("Completed"); } }); // 비동기 응답 대기 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } |
2.2. gRPC가 제공하는 다양한 스텁 객체
gRPC는 다양한 통신 패턴을 지원하기 위해 3가지 종류의 스텁 객체를 제공합니다. 각 스텁 객체는 통신 방식과 요구사항에 맞춰 다른 생성자를 사용합니다.
Future Stub 사용 예시
- Future 패턴의 경우 사용 비동기적으로 처리하지만, 결과를 가져오는 과정에서 쓰레드 블로킹이 발생할 수 있습니다.
// Future 스텁 생성 var futureStub = BalanceServiceGrpc.newFutureStub(channel); // 비동기 요청 보내기 ListenableFuture<Balance> future = futureStub.getBalance(request); // 비동기적으로 결과 가져오기 Balance balance = future.get(); // 결과가 준비될 때까지 블로킹 발생 가능 System.out.println("Balance: " + balance.getAmount()); |
2.3. gRPC 4가지 통신 패턴에 따른 사용 가능한 스텁 객체
- Unary 패턴을 제외하고는 대부분 Async Stub을 사용합니다.
- Server Streaming 패턴을 사용할 때 Blocking Stub을 사용하여 서버로부터 스트리밍되는 응답을 대기할 수 있습니다.
Iterator<Balance> balances = blockingStub.listBalances(request); while (balances.hasNext()) { System.out.println(balances.next().getAmount()); } |
관련글