들어가며 링크 복사
안녕하세요. 택시개발팀에서 서버 개발을 담당하는 폰드입니다. 택시개발팀은 승객과 기사 간의 택시 매칭부터 운행 완료까지 필요한 정보를 제공하고 전반적인 흐름을 관리합니다. 우리의 목표는 정확하고 안정적인 주행을 위해 다양한 기능을 개발하고 개선하는 것입니다.
최근에는 온라인 미터기 개선 작업을 진행했습니다. '온라인 미터기'라는 용어가 생소할 수 있지만, '미터기'는 익숙하실 겁니다. 택시에 탑승하면 차량 내부의 미터기에서 요금이 표시되는 걸 보셨을 테니까요. 온라인 미터기도 이와 비슷하게 작동합니다. 기사용 애플리케이션(이하 기사 앱)의 GPS 정보를 활용해 실시간으로 요금을 계산합니다. 이는 오프라인 미터기가 없는 차량의 운행 요금을 계산하거나, 운행 요금이 적절하게 청구되는지 검증하는 데 사용됩니다.
이렇게 중요한 역할을 하는 온라인 미터기를 고도화하면서 Protocol Buffers(이하 protobuf)를 도입한 이야기를 소개해 드리려고 합니다.
protobuf 도입 배경 링크 복사
온라인 미터기는 기사님의 운행 경로를 API로 전달받아 요금을 계산합니다. 이 경로 데이터는 운행 중 일정 간격으로 택시가 전송하는 위도, 경도, 시간 등의 정보로 구성됩니다. 따라서 운행 시간이 길어질수록 경로 데이터의 크기도 증가합니다.
서버 간 주고받는 데이터 크기가 커지면 여러 문제가 발생할 수 있습니다. 데이터 크기 증가로 직렬화 및 파싱 작업에 많은 자원과 시간이 소비되며, 메모리와 네트워크 대역폭 사용량이 늘어나 전체 시스템의 성능과 속도가 저하될 수 있습니다. 이러한 문제를 해결하기 위해 효율적인 압축 및 파싱이 가능한 protobuf를 도입하게 되었습니다.
protobuf는 JSON과 같은 데이터 직렬화 구조의 하나로, 미리 정의한 데이터 스키마를 이용해 이진 직렬화를 수행합니다. 이는 데이터 압축률을 높이고 빠른 속도로 데이터를 파싱할 수 있게 합니다. 주로 크기가 크고 파싱해야 할 양이 많은 대용량 데이터를 다룰 때 사용되며, 자동 생성된 클래스는 다양한 언어와 플랫폼에서 최적화된 방식으로 데이터를 처리할 수 있습니다.
개발 작업 과정 링크 복사
기존 서버가 Java와 Spring Boot로 구성되어 있어, 이 환경에서의 protobuf 작업 과정을 설명하겠습니다.
전체적인 개발 과정은 다음과 같습니다.
- Proto 파일 정의
- Java 파일로 컴파일
- 인코딩 및 코드 작업
이제 각 단계를 자세히 살펴보겠습니다.
1. Proto 파일 정의 링크 복사
먼저, protobuf를 사용하여 서버 간에 주고받을 데이터 형식을 proto 파일에 정의합니다. 아래는 FareRequest.proto 파일의 예시입니다. 이 파일은 택시 요금 계산에 필요한 운행 경로를 정의합니다.
[FareRequest.proto 파일 예시]
message FareRequest {
repeated Path route = 1;
}
message Path {
int64 time = 1;
double lat = 2;
double lng = 3;
}
message FareRequest {
repeated Path route = 1;
}
message Path {
int64 time = 1;
double lat = 2;
double lng = 3;
}
2. Java 파일로 컴파일 링크 복사
다음으로, 작성한 proto 파일을 protoc 명령어를 사용하여 Java 파일로 컴파일해야 합니다.
protoc는 proto 파일을 각 언어 및 플랫폼에 맞게 컴파일하는 도구입니다. 컴퓨터에 protoc를 설치한 후, 명령어로 컴파일할 수 있습니다. 예를 들어, src 디렉터리에 있는 FareRequest.proto 파일을 컴파일하여 build/gen 디렉터리에 저장하려면 다음 명령어를 실행하면 됩니다.
protoc --proto_path=src --java_out=build/gen src/FareRequest.proto
protoc --proto_path=src --java_out=build/gen src/FareRequest.proto
3. 인코딩 및 코드 작업 링크 복사
마지막으로, 호출하는 서버와 호출되는 서버 간 protobuf 파일을 통한 통신을 위해 인코딩 및 코드 작업을 수행합니다. Spring Boot는 protobuf message converter 라이브러리를 제공하며, 이를 통해 protobuf 파일의 인코딩과 디코딩이 가능합니다. protoc로 컴파일된 Java 파일에는 자동 생성된 필드에 대한 접근자 메서드가 포함되어 있습니다. getter, setter, newBuilder 등의 접근자를 활용하여 protobuf 데이터를 효과적으로 파싱하고 생성할 수 있습니다.
도입 과정에서 생긴 이슈와 해결 과정 링크 복사
protobuf를 사용할 때는 주고받는 데이터의 형식을 proto 파일로 미리 정의해야 합니다. 이 과정에서 데이터 형식이 수정될 때 문제가 발생했습니다. 택시 요금 계산에는 경로 데이터뿐만 아니라 택시의 운행 지역, 통행료 등 추가 정보가 필요합니다. 이에 따라 작업 중 데이터의 타입이 바뀌거나 추가적인 정보가 필요하게 될 경우가 생겼습니다. 처음에는 수정된 데이터 형식을 반영하여 새로운 버전의 proto 파일로 데이터 형식을 재정의하려고 했습니다. protobuf는 스키마 의존적인 특성이 강하기 때문에 스키마를 버저닝하여 관리하는 것이 편할 것이라고 생각했기 때문입니다. 그러나 이러한 접근 방식은 몇 가지 문제를 야기했습니다. 새로운 스키마를 사용할 때 API 버전 관리로 인한 관련 서버들의 배포가 필요했고, 데이터 형식이 변경될 때마다 새로운 proto 파일을 만들어야 했기 때문에 유연성이 떨어졌습니다.
이러한 불편을 해결하기 위해 다음과 같은 방식으로 수정했습니다. 경로 데이터처럼 크기가 크고 거의 변경되지 않는 데이터는 proto 파일로 미리 정의했습니다. 반면, 압축이 필요 없고 수정 가능성이 높은 매개변수들은 query parameter를 통해 전달하도록 했습니다. 이러한 접근 방식으로 매개변수 수정 시 API 버전 관리나 proto 파일 수정 및 컴파일 같은 추가 작업 없이 query parameter만 수정하면 됩니다. 결과적으로 부담 없이 수월하게 수정 작업을 진행할 수 있었습니다.
도입 효과 링크 복사
Protobuf의 도입 효과를 확인하기 위해 경로 데이터를 JSON과 Protobuf로 각각 인코딩하는 두 가지 API를 구성했습니다.
@PostMapping(value = "/test/json", consumes = "application/json")
public void json(@RequestBody FareRequestJson request, @RequestHeader HttpHeaders header) {
log.info("JSON content length : {}", header.getContentLength());
}
@PostMapping(value = "/test/json", consumes = "application/json")
public void json(@RequestBody FareRequestJson request, @RequestHeader HttpHeaders header) {
log.info("JSON content length : {}", header.getContentLength());
}
@PostMapping(value = "/test/protobuf", consumes = {"application/x-protobuf"})
public void protobuf(@RequestBody FareRequest request, @RequestHeader HttpHeaders header) {
log.info("protobuf content length : {}", header.getContentLength());
}
@PostMapping(value = "/test/protobuf", consumes = {"application/x-protobuf"})
public void protobuf(@RequestBody FareRequest request, @RequestHeader HttpHeaders header) {
log.info("protobuf content length : {}", header.getContentLength());
}
그다음, 무작위로 선택한 100건의 택시 운행에 대한 실제 경로 데이터를 추출하여 각 API에 요청했습니다. 그리고 HTTP 요청의 Content-Length 값을 비교했습니다. Content-Length는 HTTP 요청 본문의 크기를 바이트 단위로 나타내는 필드입니다. 이 값을 통해 데이터의 크기를 추정하고 전송 시간을 예측할 수 있습니다. 아래는 Content-Length 값을 비교한 표입니다.
x축은 HTTP 요청을, y축은 각 요청에 대한 인코딩 방식별 Content-Length 값을 나타냅니다. 그래프를 보면 Protobuf로 인코딩한 데이터의 크기가 JSON으로 인코딩한 것보다 확연히 작은 것을 알 수 있습니다.
각 개별 요청에 대한 표도 있습니다. 이 표는 Protobuf로 인코딩했을 때의 크기를 JSON 인코딩 크기와 비교한 차이 비율을 보여줍니다. 비율은 소수점 셋째 자리에서 반올림하여 계산되었습니다.
| - | JSON (Byte) | protobuf (Byte) | 크기 차이 (Byte) | 크기 차이 비율 (%) |
|---|---|---|---|---|
| 29911 | 13986 | 15925 | 53.24 | |
| 11315 | 5292 | 6023 | 53.23 | |
| 67474 | 31509 | 35965 | 53.30 | |
| 12214 | 5697 | 6517 | 53.36 | |
| 17439 | 8154 | 9285 | 53.24 | |
| ... | ... | ... | ... | |
| 평균 | 26012.5 | 11913.75 | 13574.81 | 53.27 |
protobuf로 인코딩할 경우, JSON에 비해 데이터 크기가 약 53% 줄어듭니다. 이는 protobuf 도입 효과를 확인할 수 있는 중요한 지표입니다. protobuf의 데이터 압축 기능으로 전송되는 데이터의 크기가 감소했음을 명확히 보여줍니다. 이러한 데이터 크기 감소는 네트워크 트래픽을 줄이고 전송 시간을 단축해 더욱 효율적인 통신을 가능하게 합니다.
마치며 링크 복사
이 글에서는 대용량 데이터 전송이 필요한 서버 간 데이터 교환 시, 데이터 압축 및 파싱 성능 향상을 위해 protobuf를 도입한 경험을 공유했습니다. 위의 실습에서 확인할 수 있듯이, protobuf는 러닝 커브가 완만하여 쉽게 적용할 수 있으면서도 큰 효과를 볼 수 있는 강력한 기술입니다. 하지만 protobuf는 모든 상황에 완벽하지 않습니다. 이진 직렬화를 사용하기 때문에 디버깅 시 메시지 내용을 확인하기 어렵고, 테스트 과정이 단순 HTTP 요청보다 복잡할 수 있습니다. 각 상황에 맞는 인코딩 기술을 사용하면 효율적이고 안정적인 서버 구축에 큰 도움이 될 것입니다.
이번 작업을 통해 개선점을 파악하고 문제를 해결하며 검증하는 과정을 거쳤습니다. 개발자로서 최적화된 서버 구축을 위해 최선의 선택을 고민한 결과, 눈에 보이는 유의미한 성과를 얻을 수 있어 매우 흥미로운 경험이었습니다. 앞으로도 택시개발팀은 사용자가 안정적으로 택시 서비스를 이용할 수 있도록 지속적으로 문제점을 발견하고 개선해 나갈 것입니다.