본문 바로가기
기타 학습

[Protocol Buffer] 프로토콜 버퍼를 사용한 Spring REST API 구현 (HTTP 통신)

by 비전공자 기록광 2022. 12. 6.
반응형

이전에 프로토콜 버퍼 개념과 간단히 데이터 구조를 만드는 실습을 진행했다.

 

 

[Protocol Buffer] 프로토콜 버퍼 (개념 / 입문 / Spring Boot Maven 실습)

업무에서 TCP통신과 모드버스로 프로토콜 버퍼까지 이야기가 흘러 gRPC와 함께 공부하게 되었다. 네트워크에 자신 없는 터라 좀 이해하는데 좀 오래 걸렸다... 😕 네트워크 공부 다시해야지.. 프

datamoney.tistory.com

 

이번에는 프로토콜 버퍼를 사용해 Rest API 를 구현해보려한다.

 

Java + Spring Boot + Maven 실습

프로젝트 셋팅은 그전 블로깅 내용과 유사하다. 자세한 설명은 거기서 참고..

 

서버간의 Restful HTTP 통신이기때문에 똑같은 프로젝트를 하나 더 만들어줬다.

 

protoc라는 이름의 프로젝트는 서버포트 8091을 가지는 데이터를 요청하는 곳이고

protoc2라는 이름의 프로젝트는 서버포트 8080을 가지는 데이터를 가지고 보내는 곳이다.

 

 

Pom.xml

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.21.9</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.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.21.9:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.25.0:exe:${os.detected.classifier}</pluginArtifact>
                    <clearOutputDirectory>false</clearOutputDirectory>
                    <protoSourceRoot>${project.basedir}/src/main/resources/proto</protoSourceRoot>
                    <outputDirectory>${project.basedir}/src/main/java/</outputDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>
 
cs

 

 

Person.proto

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto3";
 
package com.test.protoc.domain;
 
option java_outer_classname = "PersonInfo";
 
message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}
 
message AddPerson {
  repeated Person people = 1;
}
 
cs

package명만 잘 맞춰주자..

 

그리고 서버 포트를 각각 설정해줬다.

 

 

application.properties

- protoc

1
server.port=8091
cs

 

- protoc2

1
server.port=8080
cs

 

 

이제 각각 프로젝트를 나눠서 작성해보겠다.

 

데이터를 가지고 전송해주는 서버쪽 (protoc2)

RestTestController

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.test.protoc2.domain.PersonInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
 
@Controller
public class RestTestController {
 
    @Autowired
    RestTestService service;
 
    @GetMapping("/rest/test")
    public ResponseEntity<Object> rest() {
        PersonInfo.AddPerson people = service.dummyData();
        return new ResponseEntity<>(people, HttpStatus.OK);
    }
}
cs

 

일단 컨트롤러에서 요청을 받을 메소드를 생성해준다.

이 메소드는 배열 형태의 Person을 가지는 AddPerson이라는 데이터 구조를 리턴해준다.

 

 

RestTestService

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.test.protoc2.domain.PersonInfo;
import org.springframework.stereotype.Service;
 
@Service
public class RestTestService {
 
    public PersonInfo.AddPerson dummyData() {
        PersonInfo.Person person1 = PersonInfo.Person.newBuilder().setId(1).setName("홍길동").setEmail("hong11@test.com").build();
        PersonInfo.Person person2 = PersonInfo.Person.newBuilder().setId(2).setName("고길동").setEmail("go22@test.com").build();
        PersonInfo.Person person3 = PersonInfo.Person.newBuilder().setId(3).setName("오길동").setEmail("oh33@test.com").build();
 
        PersonInfo.AddPerson people = PersonInfo.AddPerson.newBuilder().addPeople(person1).addPeople(person2).addPeople(person3).build();
        return people;
    }
}
cs

서비스단에서 더미데이터를 만들어줬다.

 

 

데이터를 요청하는 서비스쪽 (protoc)

여기에서는 따로 controller와 service를 만들어주지 않고 main 메소드에서 직접 호출해줬다.

 

ProtocApplication

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.test.protoc.domain.PersonInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
 
@SpringBootApplication
public class ProtocApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ProtocApplication.class, args);
 
        RestTemplate restTemplate = new RestTemplate();
 
        String uri = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/rest/test").toUriString();
        PersonInfo.AddPerson people = restTemplate.getForEntity(uri, PersonInfo.AddPerson.class).getBody();
 
        for (PersonInfo.Person p : people.getPeopleList()) {
            System.out.println(p.getId());
            System.out.println(p.getName());
            System.out.println(p.getEmail());
            System.out.println(p.getAllFields());
        }
    }
 
}
cs

 

이대로 각각 실행시켜주면~

잘될줄 알았는데...! 삽질 시작...

 

Exception in thread "main" org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.test.protoc.domain.PersonInfo$AddPerson] and content type [application/x-protobuf;charset=UTF-8]

 

적절한 MessageConverter가 없다고 에러가 난다.

 

baeldung 튜토리얼에서 답을 찾았다.

 

- protoc

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import com.test.protoc.domain.PersonInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
 
import java.util.ArrayList;
import java.util.List;
 
@SpringBootApplication
public class ProtocApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ProtocApplication.class, args);
 
        ProtobufHttpMessageConverter protobufHttpMessageConverter = new ProtobufHttpMessageConverter();
        List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
        converters.add(protobufHttpMessageConverter);
 
        RestTemplate restTemplate = new RestTemplate(converters);
        HttpHeaders headers = new HttpHeaders();
 
        String uri = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/rest/test").toUriString();
        PersonInfo.AddPerson people = restTemplate.getForEntity(uri, PersonInfo.AddPerson.class).getBody();
 
        for (PersonInfo.Person p : people.getPeopleList()) {
            System.out.println(p.getId());
            System.out.println(p.getName());
            System.out.println(p.getEmail());
            System.out.println(p.getAllFields());
        }
    }
 
}
cs

요청하는 쪽에 RestTemplate에 받아오는 데이터를 converte 해줄 ProtobufHttpMessageConverter을 추가해준다.

 

 

- protoc2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
 
@SpringBootApplication
public class Protoc2Application {
 
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }
 
    public static void main(String[] args) {
        SpringApplication.run(Protoc2Application.class, args);
    }
 
}
 
cs

데이터를 주는 쪽에서도 ProtobufHttpMessageConverter가 필요하다. 빈으로 주입해준다.

 

안해주면 만든 데이터가 담기지 않아 no body 에러가 난다.

Exception in thread "main" org.springframework.web.client.HttpClientErrorException$NotAcceptable: 406 : [no body]

 

이렇게 실행해주면 둘 사이에 요청-응답을 지나 가져온 데이터를 잘 찍히는 지 볼 수 있다.

 

 

이렇게 프로토콜 버퍼를 사용해 통신을 하면 다양한 언어에서 호환 가능하고 바이너리 전송을 통해 속도 개선의 이점을 챙길 수 있을 것이다.

 

 


코드

https://github.com/recordbuffer/TIL/tree/main/ProtocolBuffer

 

참고

Spring REST API with Protocol Buffers | Baeldung

 

반응형

댓글