1. 개요
Protocol Buffers 는 구조화된 데이터의 직렬화 및 역직렬화를 위한 언어 및 플랫폼 중립적 메커니즘으로, 이를 만든 Google이 XML 및 JSON과 같은 다른 유형의 페이로드보다 훨씬 빠르고 작고 간단하다고 선언했습니다.
이 예제은 이 바이너리 기반 메시지 구조를 활용하기 위해 REST API를 설정하는 과정을 안내합니다.
2. 프로토콜 버퍼
이 섹션에서는 프로토콜 버퍼에 대한 몇 가지 기본 정보와 Java 에코시스템에 적용되는 방법을 제공합니다.
2.1. 프로토콜 버퍼 소개
프로토콜 버퍼를 사용하려면 .proto 파일 에 메시지 구조를 정의해야 합니다. 각 파일은 한 노드에서 다른 노드로 전송되거나 데이터 소스에 저장될 수 있는 데이터에 대한 설명입니다. 다음은 이름이 baeldung.proto 이고 src/main/resources 디렉토리 에 있는 .proto 파일 의 예입니다. 이 파일은 나중에 이 예제에서 사용됩니다.
syntax = "proto3";
package baeldung;
option java_package = "com.baeldung.protobuf";
option java_outer_classname = "BaeldungTraining";
message Course {
int32 id = 1;
string course_name = 2;
repeated Student student = 3;
}
message Student {
int32 id = 1;
string first_name = 2;
string last_name = 3;
string email = 4;
repeated PhoneNumber phone = 5;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
enum PhoneType {
MOBILE = 0;
LANDLINE = 1;
}
}
이 예제에서는 프로토콜 버퍼 컴파일러와 프로토콜 버퍼 언어 모두 버전 3을 사용 하므로 .proto 파일은 구문 = "proto3" 선언 으로 시작해야 합니다 . 컴파일러 버전 2가 사용 중인 경우 이 선언은 생략됩니다. 다음은 다른 프로젝트와의 이름 충돌을 피하기 위한 이 메시지 구조의 네임스페이스인 패키지 선언입니다.
다음 두 선언은 Java에만 사용됩니다. java_package 옵션은 생성된 클래스에 대한 패키지를 지정하고 java_outer_classname 옵션은 이 .proto 파일 에 정의된 모든 유형을 포함하는 클래스의 이름을 나타냅니다 .
아래의 2.3절에서는 나머지 요소와 이러한 요소가 Java 코드로 컴파일되는 방법에 대해 설명합니다.
2.2. Java를 사용한 프로토콜 버퍼
메시지 구조가 정의된 후에는 이 언어 중립적인 내용을 Java 코드로 변환하는 컴파일러가 필요합니다. 적절한 컴파일러 버전을 얻기 위해 프로토콜 버퍼 저장소 의 지침을 따를 수 있습니다 . 또는 com.google.protobuf:protoc 아티팩트를 검색한 다음 플랫폼에 적합한 버전을 선택 하여 Maven 중앙 저장소에서 사전 빌드된 바이너리 컴파일러를 다운로드할 수 있습니다.
다음으로 컴파일러를 프로젝트의 src/main 디렉터리에 복사하고 명령줄에서 다음 명령을 실행합니다.
protoc --java_out=java resources/baeldung.proto
이것은 baeldung.proto 파일의 옵션 선언에 지정된 대로 com.baeldung.protobuf 패키지 내의 BaeldungTraining 클래스 에 대한 소스 파일을 생성해야 합니다.
컴파일러 외에도 프로토콜 버퍼 런타임이 필요합니다. 이것은 Maven POM 파일에 다음 의존성을 추가하여 달성할 수 있습니다.
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.0.0-beta-3</version>
</dependency>
컴파일러의 버전과 동일하다면 다른 버전의 런타임을 사용할 수 있습니다. 최신 버전은 이 링크 를 확인하세요 .
2.3. 메시지 설명 컴파일
컴파일러를 사용하여 .proto 파일의 메시지는 정적 중첩 Java 클래스로 컴파일됩니다. 위의 예에서 Course 및 Student 메시지는 각각 Course 및 Student Java 클래스로 변환됩니다. 동시에 메시지의 필드는 생성된 유형 내에서 JavaBeans 스타일 getter 및 setter로 컴파일됩니다. 각 필드 선언의 끝에 있는 등호와 숫자로 구성된 마커는 관련 필드를 이진 형식으로 인코딩하는 데 사용되는 고유 태그입니다.
메시지의 유형이 지정된 필드를 살펴보고 이러한 필드가 접근자 메서드로 변환되는 방법을 확인합니다.
Course 메시지 부터 시작하겠습니다 . id 및 Course_name 을 포함하여 두 개의 간단한 필드가 있습니다 . 프로토콜 버퍼 유형인 int32 및 string 은 Java int 및 String 유형으로 변환됩니다. 다음은 컴파일 후 관련 getter입니다(간결함을 위해 구현은 생략).
public int getId();
public java.lang.String getCourseName();
입력된 필드의 이름은 다른 언어와의 협력을 유지하기 위해 뱀의 경우(개별 단어는 밑줄 문자로 구분됨)여야 합니다. 컴파일러는 Java 규칙에 따라 해당 이름을 카멜 케이스로 변환합니다.
Course 메시지 의 마지막 필드인 student 는 Student 콤플렉스 유형이며 아래에서 설명합니다. 이 필드 앞에는 반복 키워드가 추가됩니다. 즉, 여러 번 반복될 수 있습니다. 컴파일러는 다음과 같이 학생 필드 와 관련된 몇 가지 메서드를 생성 합니다(구현 없이).
public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student> getStudentList();
public int getStudentCount();
public com.baeldung.protobuf.BaeldungTraining.Student getStudent(int index);
이제 Course 메시지의 학생 필드의 복합 유형으로 사용되는 Student 메시지로 이동 합니다. id , first_name , last_name 및 email 을 포함한 간단한 필드 는 Java 접근자 메서드를 만드는 데 사용됩니다.
public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();
마지막 필드인 phone 은 PhoneNumber 복합 유형입니다. Course 메시지의 학생 필드와 유사하게 이 필드는 반복적이며 몇 가지 관련 메서드가 있습니다.
public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber> getPhoneList();
public int getPhoneCount();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber getPhone(int index);
PhoneNumber 메시지 는 메시지 필드에 해당하는 두 개의 getter가 있는 BaeldungTraining.Student.PhoneNumber 중첩 유형 으로 컴파일됩니다 .
public java.lang.String getNumber();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneType getType();
PhoneNumber 메시지 유형 필드의 복합 유형인 PhoneType 은 열거 유형이며 BaeldungTraining.Student 클래스 내에 중첩된 Java 열거 유형 으로 변환됩니다 .
public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
MOBILE(0),
LANDLINE(1),
UNRECOGNIZED(-1),
;
// Other declarations
}
3. Spring REST API의 Protobuf
이 섹션에서는 Spring Boot를 사용하여 REST 서비스를 설정하는 방법을 안내합니다.
3.1. 빈 선언
메인 @SpringBootApplication 의 정의부터 시작하겠습니다 .
@SpringBootApplication
public class Application {
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
@Bean
public CourseRepository createTestCourses() {
Map<Integer, Course> courses = new HashMap<>();
Course course1 = Course.newBuilder()
.setId(1)
.setCourseName("REST with Spring")
.addAllStudent(createTestStudents())
.build();
Course course2 = Course.newBuilder()
.setId(2)
.setCourseName("Learn Spring Security")
.addAllStudent(new ArrayList<Student>())
.build();
courses.put(course1.getId(), course1);
courses.put(course2.getId(), course2);
return new CourseRepository(courses);
}
// Other declarations
}
ProtobufHttpMessageConverter 빈은 @RequestMapping 어노테이션 메소드에 의해 반환된 응답을 프로토콜 버퍼 메시지로 변환하는 데 사용됩니다 .
다른 빈인 CourseRepository 에는 API에 대한 몇 가지 테스트 데이터가 포함되어 있습니다.
여기서 중요한 것은 표준 POJO가 아닌 프로토콜 버퍼 특정 데이터 로 작업한다는 것 입니다.
CourseRepository 의 간단한 구현은 다음과 같습니다 .
public class CourseRepository {
Map<Integer, Course> courses;
public CourseRepository (Map<Integer, Course> courses) {
this.courses = courses;
}
public Course getCourse(int id) {
return courses.get(id);
}
}
3.2. 컨트롤러 구성
다음과 같이 테스트 URL에 대한 @Controller 클래스를 정의할 수 있습니다 .
@RestController
public class CourseController {
@Autowired
CourseRepository courseRepo;
@RequestMapping("/courses/{id}")
Course customer(@PathVariable Integer id) {
return courseRepo.getCourse(id);
}
}
그리고 다시 – 여기서 중요한 것은 컨트롤러 계층에서 반환하는 Course DTO가 표준 POJO가 아니라는 것입니다. 이것은 클라이언트로 다시 전송되기 전에 프로토콜 버퍼 메시지로 변환되는 트리거가 될 것입니다.
4. REST 클라이언트 및 테스트
간단한 API 구현을 살펴보았으므로 이제 클라이언트 측에서 프로토콜 버퍼 메시지의 역직렬화 를 두 가지 방법을 사용하여 설명하겠습니다.
첫 번째 는 미리 구성된 ProtobufHttpMessageConverter 빈과 함께 RestTemplate API를 활용하여 메시지를 자동으로 변환합니다.
두 번째는 protobuf-java-format 을 사용하여 수동으로 프로토콜 버퍼 응답을 JSON 문서로 변환하는 것입니다.
시작하려면 통합 테스트를 위한 컨텍스트를 설정하고 다음과 같이 테스트 클래스를 선언 하여 애플리케이션 클래스에서 구성 정보를 찾도록 Spring Boot에 지시해야 합니다.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
// Other declarations
}
이 섹션의 모든 코드 조각은 ApplicationTest 클래스에 배치됩니다.
4.1. 예상 응답
REST 서비스에 액세스하는 첫 번째 단계는 요청 URL을 확인하는 것입니다.
private static final String COURSE1_URL = "http://localhost:8080/courses/1";
이 COURSE1_URL 은 이전에 생성한 REST 서비스에서 첫 번째 테스트 이중 과정을 가져오는 데 사용됩니다. GET 요청이 위의 URL로 전송된 후 다음 어설션을 사용하여 해당 응답이 확인됩니다.
private void assertResponse(String response) {
assertThat(response, containsString("id"));
assertThat(response, containsString("course_name"));
assertThat(response, containsString("REST with Spring"));
assertThat(response, containsString("student"));
assertThat(response, containsString("first_name"));
assertThat(response, containsString("last_name"));
assertThat(response, containsString("email"));
assertThat(response, containsString("john.doe@baeldung.com"));
assertThat(response, containsString("richard.roe@baeldung.com"));
assertThat(response, containsString("jane.doe@baeldung.com"));
assertThat(response, containsString("phone"));
assertThat(response, containsString("number"));
assertThat(response, containsString("type"));
}
다음 하위 섹션에서 다루는 두 테스트 사례 모두에서 이 도우미 메서드를 사용할 것입니다.
4.2. RestTemplate 으로 테스트하기
다음은 클라이언트를 생성하고, 지정된 목적지에 GET 요청을 보내고, 프로토콜 버퍼 메시지 형태로 응답을 수신하고, RestTemplate API를 사용하여 확인하는 방법입니다.
@Autowired
private RestTemplate restTemplate;
@Test
public void whenUsingRestTemplate_thenSucceed() {
ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
assertResponse(course.toString());
}
이 테스트 케이스가 작동하도록 하려면 구성 클래스에 등록할 RestTemplate 유형의 빈이 필요합니다.
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
ProtobufHttpMessageConverter 유형 의 또 다른 bean은 수신된 프로토콜 버퍼 메시지를 자동으로 변환하는 데도 필요합니다. 이 빈은 하위 섹션 3.1에 정의된 것과 동일합니다. 이 예제에서는 클라이언트와 서버가 동일한 애플리케이션 컨텍스트를 공유하기 때문에 애플리케이션 클래스에서 RestTemplate 빈을 선언하고 ProtobufHttpMessageConverter 빈 을 재사용할 수 있습니다.
4.3. HttpClient 로 테스트
HttpClient API 를 사용하고 프로토콜 버퍼 메시지를 수동으로 변환하는 첫 번째 단계 는 Maven POM 파일에 다음 두 의존성을 추가하는 것입니다.
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
이러한 의존성의 최신 버전은 Maven 중앙 저장소 의 protobuf-java-format 및 httpclient 아티팩트를 살펴보십시오.
클라이언트를 생성하고, GET 요청을 실행하고 , 주어진 URL을 사용하여 연결된 응답을 InputStream 인스턴스 로 변환하는 것으로 넘어갑시다 .
private InputStream executeHttpRequest(String url) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
HttpResponse httpResponse = httpClient.execute(request);
return httpResponse.getEntity().getContent();
}
이제 InputStream 객체 형태의 프로토콜 버퍼 메시지를 JSON 문서로 변환합니다.
private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
JsonFormat jsonFormat = new JsonFormat();
Course course = Course.parseFrom(protobufStream);
return jsonFormat.printToString(course);
}
그리고 테스트 케이스가 위에서 선언한 개인 도우미 메서드를 사용하고 응답을 확인하는 방법은 다음과 같습니다.
@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
InputStream responseStream = executeHttpRequest(COURSE1_URL);
String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
assertResponse(jsonOutput);
}
4.4. JSON의 응답
명확하게 하기 위해 이전 하위 섹션에서 설명한 테스트에서 받은 응답의 JSON 형식이 여기에 포함되어 있습니다.
id: 1
course_name: "REST with Spring"
student {
id: 1
first_name: "John"
last_name: "Doe"
email: "john.doe@baeldung.com"
phone {
number: "123456"
}
}
student {
id: 2
first_name: "Richard"
last_name: "Roe"
email: "richard.roe@baeldung.com"
phone {
number: "234567"
type: LANDLINE
}
}
student {
id: 3
first_name: "Jane"
last_name: "Doe"
email: "jane.doe@baeldung.com"
phone {
number: "345678"
}
phone {
number: "456789"
type: LANDLINE
}
}
5. 결론
이 예제에서는 프로토콜 버퍼를 빠르게 소개하고 Spring에서 형식을 사용하여 REST API를 설정하는 방법을 설명했습니다. 그런 다음 클라이언트 지원 및 직렬화-역직렬화 메커니즘으로 이동했습니다.
모든 예제와 코드 스니펫의 구현은 GitHub 프로젝트 에서 찾을 수 있습니다 .