-
gRPC ③ gRPC + 스프링부트 프로젝트 구성해보기네트워크 & 인프라 2022. 10. 5. 22:20
이번엔 gRPC의 이해를 높이기 위해서 스프링부트 프로젝트를 이용하여 client 모듈과 gRPC 서버 모듈로 분리하여 서로 통신하는 것을 확인해보자.
참고한 글
참고한 영상
- 👍🏼 ✨ 코드 → https://github.com/DevProblems/grpc-with-springboot
- 모든 코드는 윗 영상에서 참고하여 연습하였습니다. 🙌
1. 기본 프로젝트 실행하기
- 먼저 https://start.spring.io/에서 다음과 같이 프로젝트를 생성해준다.
- 앞으로 구성할 전체적인 프로젝트 구조는 다음과 같다.
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를 이용했기 때문에 다음과 같이 조회해도 올바른 결과 값을 조회하는 것을 확인이 가능하다.
'네트워크 & 인프라' 카테고리의 다른 글
쿠버네티스 ② 메인 K8s component (2) (0) 2022.10.12 쿠버네티스 ① 메인 K8s component (1) (1) 2022.10.08 gRPC ② gRPC + 자바 프로젝트 구성해보기 (1) 2022.10.05 gRPC ① gRPC란 ( + Kotlin 설정) (1) 2022.09.30 SSH 별칭으로 접속 시도시 RSA 공유키 충돌 문제 발생 (0) 2022.05.30