Protobuf에서 Java 컨버터
.proto 스키마를 붙여넣으세요. 프로젝트에 그대로 넣을 수 있는 평범한 Java 클래스를 받습니다 — public 필드, getter 없음, 수정도 쉽습니다.
입력 (.proto 스키마)
출력 (Java)
이 도구가 하는 일
Protocol Buffers 스키마가 있고 그 메시지를 소비하는 Java 서비스가 있습니다 — gRPC 위일 수도 있고 HTTP 위 JSON일 수도 있죠. 공식 protoc를 Java 플러그인과 함께 돌리면 Builder 패턴 생성 클래스가 나옵니다. 운영에는 좋지만 스케치, 프로토타이핑, JSON을 POJO에 손으로 매핑하기에는 무겁습니다. 이 컨버터는 평범한 Java 클래스를 내보냅니다 — public 필드, 합리적인 기본값, 메시지당 한 클래스, enum은 별도 타입.
타입 매핑은 실제 현장에서 손으로 짜는 Java를 따릅니다: string → String, bool → boolean, int32/sint32/sfixed32 → int, int64/sint64/sfixed64 → long, double → double, float → float, bytes → byte[]. Java에는 부호 없는 숫자 타입이 없어서 uint32/fixed32는 int로, uint64/fixed64는 long으로 떨어집니다 — 대부분의 경우에 충분합니다. 정말 최상위 비트가 필요하면 경계에서 Integer.toUnsignedLong을 쓰세요. repeated T는 List<T>가 되고, map<K, V>는 Map<K, V>가 됩니다. 둘 다 java.util에서 옵니다.
필드 이름은 snake_case(Protobuf 관례)에서 camelCase(Java 관례)로 변환됩니다 — protoc가 하는 것과 일치하고, proto3 JSON 매핑이 사용하는 방식과도 같습니다. enum은 최상위 public enum 타입이 되며, 와이어의 정수 값은 final 필드로 보존되므로 언젠가 전환하면 protoc 생성 코드와 라운드트립할 수 있습니다. 중첩 메시지는 최상위 클래스로 평탄화되어, 출력을 분할할 때 각각을 자체 파일로 옮길 수 있습니다. 모든 것은 브라우저에서 실행됩니다 — 스키마는 페이지를 떠나지 않습니다.
사용 방법
세 단계입니다. 출력은 Java 프로젝트에 바로 떨어뜨릴 수 있습니다.
.proto 스키마 붙여넣기
왼쪽 에디터에 스키마를 넣으세요. 맨 위의 syntax = "proto3";는 있어도 좋고 없어도 됩니다. 파서는 중첩된 message 블록, enum 선언, oneof, map<K, V>, 필드 옵션을 처리합니다. import 지시문은 인식되지만 건너뜁니다 — 필요하면 임포트된 타입을 인라인으로 붙여넣으세요.
필드 이름 변환은 자동입니다: .proto의 order_id는 Java에서 orderId가 됩니다. 메시지와 enum 이름은 그대로 유지됩니다(이미 PascalCase).
출력 읽기
오른쪽: 단일 .java 블록에 enum 전부가 먼저 오고, 그 다음 선언 순서대로 클래스 전부가 옵니다. 각 클래스는 프리미티브용 기본값(0, 0.0, false, "")을 가진 public 필드를 가지며, 참조 타입(List, Map, 메시지 참조)은 채울 때까지 null로 남습니다. java.util.List와 java.util.Map 임포트는 스키마가 필요로 할 때만 추가됩니다.
프로젝트에 떨어뜨리기
각 클래스를 자체 .java 파일로 복사하세요(Java는 파일당 하나의 public 클래스를 요구합니다). package 선언을 추가한 다음, 원하는 JSON 디시리얼라이저에 클래스를 연결하세요 — Jackson은 기본적으로 public 필드를 잡고, proto3 JSON 매핑은 camelCase를 사용하므로 필드 이름은 이미 일치합니다. getter/setter를 선호한다면 IntelliJ의 "Encapsulate Fields" 리팩토링이 한 번에 처리해 줍니다.
실제로 시간을 아껴주는 순간
gRPC 서비스용 Java 클라이언트 스케치
기존 gRPC 백엔드(Spring Boot일 수도, Quarkus일 수도)에 대해 Java 클라이언트를 스파이크 중인데, 아직 protoc Maven/Gradle 플러그인을 풀로 세팅하고 싶지는 않습니다. 스키마를 붙여넣고, src/main/java/dto에 클래스를 넣고, Jackson으로 JSON 응답을 디시리얼라이즈하고, 프로토타입을 출하하세요.
proto와 일치하는 DTO를 손으로 만들기
팀은 와이어상 진실의 출처로 Protobuf를 쓰지만, 소비하는 Java 서비스는 필드 서너 개만 필요하고 Message/Builder 의존을 들이고 싶지 않습니다. 스키마를 붙여넣고 필요 없는 필드를 지우면, 단독으로 컴파일되는 평범한 DTO가 나옵니다.
Protobuf API 변경 리뷰
백엔드 팀원이 메시지에 필드를 추가했습니다. 빌드를 돌리지 않고 Java POJO에 어떤 영향이 있는지 보고 싶습니다. 새 .proto를 붙여넣고, Java 출력을 현재 클래스와 diff한 뒤, 핵심을 짚는 리뷰 댓글을 남기세요.
protoc 생성 출력과 교차 확인
빌드는 공식 protoc Java 플러그인을 사용해 Builder 패턴 클래스를 만들어냅니다. 평범한 Java가 어떻게 생겼는지에 대한 깔끔한 참조용으로 여기에 스키마를 붙여넣으세요. 문서, 온보딩, 데이터 클래스를 선호하는 Kotlin 팀원에게 유용합니다.
자주 묻는 질문
제 스키마가 어딘가로 전송되나요?
아니요. 파서와 Java 이미터는 전적으로 브라우저에서 JavaScript로 실행됩니다. 붙여넣는 동안 DevTools를 열어 Network 탭을 보세요 — 요청은 0건입니다. 스키마에 내부 패키지 경로, 타입 이름, 또는 서드파티 서비스에 보내고 싶지 않은 무언가가 포함될 때 유용합니다.
왜 getter/setter가 아니라 public 필드인가요?
두 가지 이유. 첫째, 출력은 적응할 출발점으로 만들어졌기 때문입니다 — public 필드는 컴파일이 되는 가장 작은 형태이고, 언제든 IDE의 "Encapsulate Fields"를 돌릴 수 있습니다. 둘째, 가장 흔한 소비자가 Jackson 디시리얼라이즈인데, public 필드에 그대로 동작합니다. record, 불변 타입, Lombok @Data 클래스가 필요하면 출력을 붙여넣고 리팩터링하세요.
uint32와 uint64는 어떻게 처리되나요?
Java에는 부호 없는 정수 타입이 없어서 uint32/fixed32는 int로, uint64/fixed64는 long으로 매핑됩니다. 부호 있는 범위에 들어가는 값이라면 잘 동작합니다. Integer.MAX_VALUE나 Long.MAX_VALUE를 넘는 값에서는 음수가 보일 겁니다. 표준적인 해결책은 경계에서 Integer.toUnsignedLong(x)를 쓰거나 java.lang.Long의 산술 헬퍼를 사용하는 것 — 부호 없는 헬퍼는 Java 21 API 문서를 참고하세요.
왜 int64 필드가 String이 아니라 그냥 long인가요?
TypeScript와 JSON은 53비트 정밀도 한계를 가지므로, proto3 JSON 스펙은 정밀도를 보존하기 위해 64비트 정수를 문자열로 인코딩합니다. Java의 long은 정밀도 손실 없이 진짜 64비트 부호 있는 정수이므로, Java 타입은 long으로 유지합니다. 서버가 이미 int64를 문자열로 인코딩한 JSON을 디시리얼라이즈하고 있다면, Jackson을 @JsonFormat이나 커스텀 디시리얼라이저로 설정하세요. 기반 타입을 바꿀 필요는 없습니다.
중첩 메시지는 어떻게 처리되나요?
각 중첩 메시지는 최상위 클래스로 평탄화됩니다. Java 관례는 파일당 하나의 public 클래스이므로, 평탄한 최상위 타입이 각 클래스를 자체 .java 파일로 복사할 때 분할하기 더 쉽습니다. protoc가 내는 스타일의 static inner class를 선호한다면, 출력을 붙여넣고 중첩 클래스를 부모 안으로 옮기세요 — 필드 참조는 이미 leaf 이름을 사용하고 있으므로 같은 스코프에 들어가면 해결됩니다.
필드는 optional로 표시되나요?
아니요 — proto3 필드는 와이어 포맷에서 항상 기본값을 가지므로, 프리미티브 필드는 Java의 0값(0, 0.0, false, "")으로 초기화됩니다. 참조 타입(List, Map, 중첩 메시지, byte[])은 null에서 시작하므로, 요소를 추가하기 전에 초기화하는 것을 잊지 마세요. 명시적 Optional<T> 래퍼를 원한다면, 그건 수동 단계입니다.
oneof를 처리하나요?
oneof의 각 필드는 일반 클래스 필드로 출력됩니다. 출력은 oneof가 의미하는 "정확히 하나" 제약을 강제하지 않습니다 — 그러려면 sealed 타입 계층이나 런타임 검사가 필요한데, 둘 다 평범한 POJO에는 맞지 않습니다. 더 엄격한 모델링을 원한다면, 출력을 가져다가 oneof 필드를 케이스당 하나의 record를 가진 sealed 인터페이스로 바꾸세요.
이 클래스를 공식 protobuf-java 런타임과 함께 쓸 수 있나요?
직접은 안 됩니다 — 공식 런타임은 Builder, parseFrom, 그리고 com.google.protobuf.MessageOrBuilder 계약의 나머지 부분을 갖춘 protoc 생성 클래스를 기대합니다. 이 도구의 클래스는 평범한 POJO이며, JSON 직렬화(Jackson, Gson, Moshi)를 위한 것입니다. 바이너리 와이어 포맷에는 여전히 공식 codegen이 필요합니다 — 설정은 Java 튜토리얼을 참고하세요.
관련 도구
Protobuf, JSON, Java를 다룬다면 이들의 조합이 잘 어울립니다: