Spring

프로토콜 버퍼가 있는 Spring REST API

기록만이살길 2022. 11. 8. 23:29
반응형

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 클래스로 컴파일됩니다. 위의 예에서 CourseStudent 메시지는 각각 CourseStudent Java 클래스로 변환됩니다. 동시에 메시지의 필드는 생성된 유형 내에서 JavaBeans 스타일 getter 및 setter로 컴파일됩니다. 각 필드 선언의 끝에 있는 등호와 숫자로 구성된 마커는 관련 필드를 이진 형식으로 인코딩하는 데 사용되는 고유 태그입니다.

메시지의 유형이 지정된 필드를 살펴보고 이러한 필드가 접근자 메서드로 변환되는 방법을 확인합니다.

Course 메시지 부터 시작하겠습니다 . idCourse_name 을 포함하여 두 개의 간단한 필드가 있습니다 . 프로토콜 버퍼 유형인 int32string 은 Java intString 유형으로 변환됩니다. 다음은 컴파일 후 관련 getter입니다(간결함을 위해 구현은 생략).

public int getId();
public java.lang.String getCourseName();

입력된 필드의 이름은 다른 언어와의 협력을 유지하기 위해 뱀의 경우(개별 단어는 밑줄 문자로 구분됨)여야 합니다. 컴파일러는 Java 규칙에 따라 해당 이름을 카멜 케이스로 변환합니다.

Course 메시지 의 마지막 필드인 studentStudent 콤플렉스 유형이며 아래에서 설명합니다. 이 필드 앞에는 반복 키워드가 추가됩니다. 즉, 여러 번 반복될 수 있습니다. 컴파일러는 다음과 같이 학생 필드 와 관련된 몇 가지 메서드를 생성 합니다(구현 없이).

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_nameemail 을 포함한 간단한 필드 는 Java 접근자 메서드를 만드는 데 사용됩니다.

public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();

마지막 필드인 phonePhoneNumber 복합 유형입니다. 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-formathttpclient 아티팩트를 살펴보십시오.

클라이언트를 생성하고, 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 프로젝트 에서 찾을 수 있습니다 .

REST footer banner
참고
  • https://docs.spring.io/spring-framework/docs/current/reference/html
  • https://www.baeldung.com/spring-rest-api-with-protocol-buffers
반응형