네트워크 & 인프라

gRPC ③ gRPC + 스프링부트 프로젝트 구성해보기

dodop 2022. 10. 5. 22:20

 

 

 

이번엔 gRPC의 이해를 높이기 위해서 스프링부트 프로젝트를 이용하여 client 모듈과 gRPC 서버 모듈로 분리하여 서로 통신하는 것을 확인해보자. 

 

 

 

참고한 글 
 

Spring Boot + gRPC (and, my experience of gRPC)

이번 포스트는 google에서 개발한 HTTP-based RPC Framework, gRPC를 소개하려 한다. Spring Boot를 사용해 gRPC Server를 만드는 방법을 알아보자.

medium.com

 

gRPC 사용법, gRPC 예제 코드 실행해보기, 원리는 몰라도 gRPC 입문은 가능하다 (grpc java example)

이 포스트는 springcamp2017에서 grpc발표를 하신 오명운님의 발표 자료 및 github소스를 참고해서 작성한 것입니다. gRPC의 장점 service 정의가 단순하다 여러 프로그래밍 언어나 플랫폼에서 사용이 가

jeong-pro.tistory.com

 

GitHub - LogNet/grpc-spring-boot-starter: Spring Boot starter module for gRPC framework.

Spring Boot starter module for gRPC framework. . Contribute to LogNet/grpc-spring-boot-starter development by creating an account on GitHub.

github.com

 

 

 

 

참고한 영상 
 

GitHub - DevProblems/grpc-with-springboot: This repo is part of my youtube video. It covers gRPC with spring boot demo

This repo is part of my youtube video. It covers gRPC with spring boot demo - GitHub - DevProblems/grpc-with-springboot: This repo is part of my youtube video. It covers gRPC with spring boot demo

github.com

 

 

 

 

 

 

 

 

 

1. 기본 프로젝트 실행하기

 

 

 

  • 앞으로 구성할 전체적인 프로젝트 구조는 다음과 같다.

 

 

 

 

 

2. 프로젝트에 proto 모듈 생성
  • proto 모듈에는 기본적으로 grpc의 proto base files를 생성하기위한 정보를 담는다.
  • 먼저 pom.xml에 다음과 같이 grpc 사용을 위한 의존관계를 추가해준다.
  • 여기서 ‘:osx-x86_64’는 자신의 컴퓨터 버전에 알맞게 설정해준다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>grpc-with-springboot</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>proto</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>1.30.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>1.30.0</version>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>
                        com.google.protobuf:protoc:3.3.0:exe:osx-x86_64
                    </protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>
                        io.grpc:protoc-gen-grpc-java:1.4.0:exe:osx-x86_64
                    </pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

 

 

 

 

  • proto / src / main / proto 폴더에 schema.proto 파일을 생성해준다.
  • unary - synchronous, server streaming - asynchronous, client streaming - asynchronous, bidirectional streaming - asynchronous 종류의 서비스를 생성
// 프로토 최신 버전사용
syntax = "proto3";

// 사용할 패키지
package com.example;

// 파일이 자바 멀티플 파일 바탕으로 생성되도록
option java_multiple_files = true;

// 책과 작가의 클래스 정의
message Book {
  int32 book_id = 1;
  string title = 2;
  float price = 3;
  int32 pages = 4;
  int32 author_id = 5;
}

message Author {
  int32 author_id = 1;
  string first_name = 2;
  string last_name = 3;
  string gender = 4;
  int32 book_id = 5;
}

service BookAuthorService {

  // unary - synchronous
  // client will send one request and server will respond with one response
  rpc getAuthor(Author) returns(Author){}

  // server streaming - asynchronous
  // client will send one request and server will respond with stream of messages to the client
  rpc getBookByAuthor(Author) returns(stream Book){}

  // client streaming - asynchronous
  // client will send stream of messages and server will respond with one response
  rpc getExpensiveBook(stream Book) returns(Book){}

  // bidirectional steaming - asynchronous
  // client will send stream of messages and server will respond with stream of messages to the client
    rpc getBookByAuthorGender(stream Book) returns(stream Book){}
}

 

 

 

  • 생성된 proto 폴더를 sources root로 지정해준다.

 

 

 

  • proto / src / main / java / com / example에 TempDb(테스트용 데이터베이스)를 추가로 작성해준다.
package com.example;

import java.util.ArrayList;
import java.util.List;

public class TempDb {
    public static List<Author> getAuthorsFromTempDb() {
        return new ArrayList<Author>() {
            {
                add(Author.newBuilder().setAuthorId(1).setBookId(1).setFirstName("Charles").setLastName("Dickens").setGender("male").build());
                add(Author.newBuilder().setAuthorId(2).setFirstName("William").setLastName("Shakespeare").setGender("male").build());
                add(Author.newBuilder().setAuthorId(3).setFirstName("JK").setLastName("Rowling").setGender("female").build());
                add(Author.newBuilder().setAuthorId(4).setFirstName("Virginia").setLastName("Woolf").setGender("female").build());
            }
        };
    }

    public static List<Book> getBooksFromTempDb() {
        return new ArrayList<Book>() {
            {
                add(Book.newBuilder().setBookId(1).setAuthorId(1).setTitle("Oliver Twist").setPrice(123.3f).setPages(100).build());
                add(Book.newBuilder().setBookId(2).setAuthorId(1).setTitle("A Christmas Carol").setPrice(223.3f).setPages(150).build());
                add(Book.newBuilder().setBookId(3).setAuthorId(2).setTitle("Hamlet").setPrice(723.3f).setPages(250).build());
                add(Book.newBuilder().setBookId(4).setAuthorId(3).setTitle("Harry Potter").setPrice(423.3f).setPages(350).build());
                add(Book.newBuilder().setBookId(5).setAuthorId(3).setTitle("The Casual Vacancy").setPrice(523.3f).setPages(450).build());
                add(Book.newBuilder().setBookId(6).setAuthorId(4).setTitle("Mrs. Dalloway").setPrice(623.3f).setPages(550).build());
            }
        };
    }
}

 

 

 

  • proto 모듈의 maven compile을 실행하여 proto base files가 다음과 같이 자동 생성되는 것을 확인한다.

 

 

 

  • grpc-java 폴더와 java 폴더를 generated sources root로 지정해준다.

 

 

 

  • proto 모듈의 전체적인 구조는 다음과 같다.

 

 

 

 

 

 

 

 

 

3. 프로젝트에 grpc-service 모듈 생성
  • grpc 서버 파트를 담당할 모듈을 생성하고 다음과 같이 dependency를 추가해준다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>grpc-with-springboot</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>grpc-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>proto</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>io.grpc</groupId>
                    <artifactId>grpc-stub</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.grpc</groupId>
                    <artifactId>google-protobuf</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-spring-boot-starter</artifactId>
            <version>2.9.0.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 

 

 

  • grpc-service / src / main / java / com / example / BookAuthorServerService 클래스를 생성한다.
  • BookAuthorServerService 클래스는 BookAuthorServiceGrpc.BookAuthorServiceImplBase를 상속받고 메서드를 오버라이딩 해준다.
  • 각각의 메서드는 StreamObserver 는 client streaming 유무에 따라 다르게 작성된다.
package com.example;

import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

import java.util.ArrayList;
import java.util.List;

// proto에 정의한 메서드를 만듦
@GrpcService
public class BookAuthorServerService extends BookAuthorServiceGrpc.BookAuthorServiceImplBase {
    // onNext : back to client, onCompleted : call the successful end of the method
    @Override
    public void getAuthor(Author request, StreamObserver<Author> responseObserver) {
        TempDb.getAuthorsFromTempDb().stream()
                .filter(author -> author.getAuthorId() == request.getAuthorId())
                .findFirst()
                .ifPresent(responseObserver::onNext);
        responseObserver.onCompleted();
    }

    @Override
    public void getBookByAuthor(Author request, StreamObserver<Book> responseObserver) {
        TempDb.getBooksFromTempDb().stream()
                .filter(book -> book.getAuthorId() == request.getAuthorId())
                .forEach(responseObserver::onNext);
        responseObserver.onCompleted();
    }

    @Override
    public StreamObserver<Book> getExpensiveBook(StreamObserver<Book> responseObserver) {
        return new StreamObserver<Book>() {
            Book expensiveBook = null;
            float priceTrack = 0;

            // 각각의 책들은 book 파라미터로 넘어올 것이다
            @Override
            public void onNext(Book book) {
                if (book.getPrice() > priceTrack) {
                    priceTrack = book.getPrice();
                    expensiveBook = book;
                }
            }

            @Override
            public void onError(Throwable throwable) {
                responseObserver.onError(throwable);
            }

            @Override
            public void onCompleted() {
                // 다시한번 onNext 를 call (book 1개를 리턴하기 때문)
                responseObserver.onNext(expensiveBook);
                responseObserver.onCompleted();
            }
        };
    }

    @Override
    public StreamObserver<Book> getBookByAuthorGender(StreamObserver<Book> responseObserver) {
        return new StreamObserver<Book>() {
            List<Book> books = new ArrayList<>();

            @Override
            public void onNext(Book book) {
                TempDb.getBooksFromTempDb().stream()
                        .filter(booksFromDb -> book.getAuthorId() == booksFromDb.getAuthorId())
                        .forEach(books::add);
            }

            @Override
            public void onError(Throwable throwable) {
                responseObserver.onError(throwable);
            }

            @Override
            public void onCompleted() {
                // 클라이언트의 옵저버를 부름
                books.forEach(responseObserver::onNext);
                responseObserver.onCompleted();
            }
        };
    }
}

 

 

 

  • 가장 메인 클래스인 ServerApplication은 다음과 같이 작성해준다.
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
}

 

 

 

  • resources / application.ym에 client 서버와 포트를 다르게 설정하기 위해 다음과 같이 작성한다.
  • 톰캣 포트는 8081에, grpc 포트(기본 9090)는 9000에 열린다.
server:
  port: 8081
grpc:
  server:
    port: 9000

 

 

 

  • grpc-service 모듈의 구조는 다음과 같다.

 

 

 

 

 

 

 

 

 

4. 프로젝트에 client-service 모듈 생성
  • grpc 서버를 이용할 클라이언트 모듈을 생성하고 다음과 같이 dependency를 추가해준다.
  • 여기서 grpc-client-spring-boot-starter가 서버와 다름을 확인할 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>grpc-with-springboot</artifactId>
        <groupId>com.example</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>client-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>proto</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>io.grpc</groupId>
                    <artifactId>grpc-stub</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.grpc</groupId>
                    <artifactId>google-protobuf</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-client-spring-boot-starter</artifactId>
            <version>2.9.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.12.0</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

 

 

 

  • client-service / src / main / java / com / example / controller 폴더에 BookAuthorController 에 다음과 같이 작성한다.
  • 이를 통해 client 호스트를 통해서 들어온 Rest API 요청이 전달된다.
  • 전체적인 흐름
    • 클라이언트의 BookAuthorController 를 통해 요청이 들어옴
    • BookAuthorClientService의 메서드를 호출 (asynchronous 의 경우에 CountDownLatch를 사용)
    • streamObserver를 이용해서 BookAuthorClientService의 옵저버를 작성한다.
    • 해당 서비스 메서드에서 gRPC 서버의 옵저버를 호출하여 onNext동작
    • gRPC의 서비스 메서드에서는 클라이언트 서비스의 옵저버를 통해 onNext 및 onComplete동작 수행
    • 클라이언트 서비스 메서드에서 onComplete 수행 및 반환값 반환
    • 결과가 controller로 전해짐
import com.google.protobuf.Descriptors;

import java.util.List;
import java.util.Map;

@RestController
public class BookAuthorController {

    private final BookAuthorClientService bookAuthorClientService;

    public BookAuthorController(BookAuthorClientService bookAuthorClientService) {
        this.bookAuthorClientService = bookAuthorClientService;
    }

    @GetMapping("/authors/{authorId}")
    public Map<Descriptors.FieldDescriptor, Object> getAuthor(@PathVariable String authorId){
        return bookAuthorClientService.getAuthor(Integer.parseInt(authorId));
    }

    @GetMapping("/books/authors/{authorId}")
    public List<Map<Descriptors.FieldDescriptor, Object>> getBooksByAuthor(@PathVariable String authorId) throws InterruptedException {
        return bookAuthorClientService.getBooksByAuthor(Integer.parseInt(authorId));
    }

    @GetMapping("/expensive-book")
    public Map<String, Map<Descriptors.FieldDescriptor, Object>> getExpensiveBook() throws InterruptedException {
        return bookAuthorClientService.getExpensiveBook();
    }

    @GetMapping("/books/by-authors-gender/{gender}")
    public List<Map<Descriptors.FieldDescriptor, Object>> getBooksByAuthorGender(@PathVariable String gender) throws InterruptedException {
        return bookAuthorClientService.getBooksByAuthorGender(gender);
    }
}

 

 

 

 

  • client-service / src / main / java / com / example / service 폴더에 BookAuthorClientService 에 다음과 같이 작성한다.
  • @GrpcClient 어노테이션을 통해서 grpc 통신에 사용할 채널을 정해준다.
  • synchronous, asynchronous 등의 여부에 따라서 BookAuthorServiceBlockingStub, BookAuthorServiceStub 등을 정한다.
package com.example.service;

import com.example.Author;
import com.example.Book;
import com.example.BookAuthorServiceGrpc;
import com.example.TempDb;
import com.google.protobuf.Descriptors;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Service
public class BookAuthorClientService {

    // (채널)을 열어준다
    @GrpcClient("grpc-example-service")
    BookAuthorServiceGrpc.BookAuthorServiceBlockingStub synchronousClient;

    @GrpcClient("grpc-example-service")
    BookAuthorServiceGrpc.BookAuthorServiceStub asynchronousClient;

    public Map<Descriptors.FieldDescriptor, Object> getAuthor(int authorId) {
        Author authorRequest = Author.newBuilder().setAuthorId(authorId).build();
        Author authorResponse = synchronousClient.getAuthor(authorRequest);
        return authorResponse.getAllFields();
    }

		// 비동기 설정 
    public List<Map<Descriptors.FieldDescriptor, Object>> getBooksByAuthor(int authorId) throws InterruptedException {
        // streamObserver는 다른 쓰레드에서 동작하게 된다
        // main Thread는 다른 쓰레드가 완료하기 까지 기다려야만 한다
        // rest api는 반응을 빠르게 갖는다
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        Author authorRequest = Author.newBuilder().setAuthorId(authorId).build();
        final List<Map<Descriptors.FieldDescriptor, Object>> response = new ArrayList<>();
        asynchronousClient.getBookByAuthor(authorRequest, new StreamObserver<Book>() {
            @Override
            public void onNext(Book book) {
                response.add(book.getAllFields());
            }

            // 만약 오류가 발생하면 countDown을 하여 메인 쓰레드가 기다리지 않도록 한다
            @Override
            public void onError(Throwable throwable) {
                countDownLatch.countDown();
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        // 하나의 쓰레드가 완료하기 까지 1분 기다림
        boolean await = countDownLatch.await(1, TimeUnit.MINUTES);
        // 완료 되었으면 반응을 return, 아니면 빈 리스트 반환
        return await ? response : Collections.emptyList();
    }

    public Map<String, Map<Descriptors.FieldDescriptor, Object>> getExpensiveBook() throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        final Map<String, Map<Descriptors.FieldDescriptor, Object>> response = new HashMap<>();
        // callback registration
        StreamObserver<Book> responseObserver = asynchronousClient.getExpensiveBook(new StreamObserver<Book>() {
            @Override
            public void onNext(Book book) {
                response.put("ExpensiveBook", book.getAllFields());
            }

            @Override
            public void onError(Throwable throwable) {
                countDownLatch.countDown();
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });

        TempDb.getBooksFromTempDb().forEach(responseObserver::onNext);
        responseObserver.onCompleted();
        boolean await = countDownLatch.await(1, TimeUnit.MINUTES);
        return await ? response : Collections.emptyMap();
    }

    public List<Map<Descriptors.FieldDescriptor, Object>> getBooksByAuthorGender(String gender) throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        final List<Map<Descriptors.FieldDescriptor, Object>> response = new ArrayList<>();
        // callback registration
        StreamObserver<Book> responseObserver = asynchronousClient.getBookByAuthorGender(new StreamObserver<Book>() {
            // 서버에서 부른 클라이언트의 옵저버 onNext
            @Override
            public void onNext(Book book) {
                response.add(book.getAllFields());
            }

            @Override
            public void onError(Throwable throwable) {
                countDownLatch.countDown();
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });

        TempDb.getAuthorsFromTempDb()
                .stream()
                .filter(author -> author.getGender().equalsIgnoreCase(gender))
                // server의 옵저버를 부름
                .forEach(author -> responseObserver.onNext(Book.newBuilder().setAuthorId(author.getAuthorId()).build()));
        responseObserver.onCompleted();
        boolean await = countDownLatch.await(1, TimeUnit.MINUTES);
        return await ? response : Collections.emptyList();
    }

}

 

 

 

 

  • 가장 메인 클래스인 ClientApplication은 다음과 같이 작성해준다.
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
}
  • grpc 서버의 설정을 위해 다음과 같이 resources / application.yml 파일을 작성해준다.
grpc:
  client:
    grpc-example-service:
      address: static://localhost:9000
      negotiationType: plaintext

 

 

 

 

  • client-service 모듈의 구조는 다음과 같다.

 

 

 

 

 

 

 

5. 프로젝트 실행 및 API 콜
  • 이제 차례대로 올바르게 결과값을 반환하는지 확인해보자.
  • client-service와 grpc-service를 run한 상태에서 API를 조회한다.
  • 먼저 작가의 아이디를 통해 작가정보를 반환하는 unary - synchronous getAuthor 메소드

 

 

 

  • 두번째로, 작가의 아이디를 통해 작가의 책 정보들을 반환하는  server streaming - asynchronous getBooksByAuthor 메소드

 

 

 

 

  • 세번째로, 주어진 TempDb의 책중에서(client streaming) 가장 비싼 책 정보를 반환하는 client streaming - asynchronous의 getExpensiveBook 메소드 

 

 

 

 

 

  • 마지막으로, 주어진 TempDb의 책중에서(client streaming) 주어진 gender 정보와 일치하는 책 리스틑 정보를(server streaming) 반환하는  bidirectional streaming - asynchronous의 getBooksByAuthorGender 메소드 

 

 

  • 알파벳 케이스 ignore를 이용했기 때문에 다음과 같이 조회해도 올바른 결과 값을 조회하는 것을 확인이 가능하다.