DynamicMessage
What
Descriptor를 런타임에 받아 아무 protobuf 메시지 타입이나 담을 수 있는 범용 Message 구현체다.
생성 클래스(ConnectDefault1) 을 대체한다.
public final class DynamicMessage extends AbstractMessage {
DynamicMessage(Descriptor type,
FieldSet<FiledDescriptor> fields, // 값이 여기 담긴다.
FieldDescriptor[] oneOfCases,
UnknownFieldSet unknownFields);
}
생성 클래스는 필드가 Java field 로 박혀 있지만
DynamicMessage는 FieldDescriptor -> Object 맵 (FieldSet)에 담는다.
그래서 타입이 컴파일 타임에 필요 없다.
| 생성 클래스 | DynamicMessage | |
|---|---|---|
| 타입 정보 | 클래스 자체 | 생성자에 받은 Descriptor |
| 값 저장 | private ByteString messageData_; |
FieldSet<FieldDescriptor> |
| 필드 접근 | msg.getMessageData() |
msg.getField(fd) → Object |
| 필드 지정 | 컴파일 타임 검증 | findFieldByName("...") 런타임 조회 |
| 파싱 | ConnectDefault1.parseFrom(bytes) |
DynamicMessage.parseFrom(descriptor, bytes) |
Why
사전에 생성 클래스를 만들 수 없는 상황을 위함이다.
컴파일 시점에 .proto 파일이 없는 케이스가 대표적이다.
Descriptor 가 설계도라면 DynamicMessage 는 설계도로 찍어낸 인스턴스다.
대가가 있다.
- Field Access 가 Map 조회 + Boxing 이라 생성 클래스보다 느리고 할당이 많다.
getField()는Object를 반환한다. 캐스팅 실패시 런타임ClassCastException이 발생하낟.- 필드를 문자열 이름으로 찾는다.
How
(1) Parsing
Descriptor descriptor = envelopeDescriptor(schemaId, indexes);
DynamicMessage envelope = DynamicMessage.parseFrom(descriptor, payload);
(2) 단일 필드 읽기
FieldDescriptor field = envelope.getDescriptorForType().findFieldByName("MessageData");
if (field == null) {
throw WireFormatException.withNoStackTrace("Not a PubSub envelope: no field MessageData");
}
byte[] data = ((ByteString) envelope.getField(field)).toByteArray();
값 타입 매핑 — getField 가 돌려주는 Java 타입
캐스팅을 틀리기 쉬운 부분이다.
| proto 타입 | Java 객체 |
|---|---|
double / float |
Double / Float |
int32, sint32, uint32, fixed32 |
Integer |
int64, sint64, uint64, fixed64 |
Long |
bool |
Boolean |
string |
String |
bytes |
ByteString (byte[] 아님) |
enum |
EnumValueDescriptor |
message |
Message (실제 구현은 DynamicMessage) |
repeated T |
List<위 타입> |
Descriptor
What
메시지 타입 하나의 런타임 타입 정보. Java 의 Class 객체에 대응한다.
.proto 파일 하나는 FileDescriptor 가 되고, 그 안에 메시지별 Descriptor 가 들어 있다.
ConnectDefault1 의 Descriptor 가 갖는 정보:
name: ConnectDefault1
fields:
1 MessageData bytes optional
2 AttributesMap message repeated → ConnectDefault2Entry
nested types:
ConnectDefault2Entry
1 key string
2 value string
Why
직렬화된 protobuf 바이너리에는 필드 번호와 wire type 만 있고 field명, type명이 없다.
0a 86 02 ...
^^ field 1, wiretype 2 ← 바이트가 가진 정보는 여기까지
이걸 MessageData 라는 bytes 필드로 해석하려면 설계도가 필요하다. Descriptor가 바로 그 설계도다.protoc 로 생성한 클래스도 내부에 반드시 Descriptor 를 포함한다.
즉, 런타임에 타입 정보를 가지고 .proto 로 직렬화된 데이터를 각 언어의 객체로 파싱 지원하기 위함이다.
Descriptor 는 항상 존재한다.
ConnectDefault1.getDescriptor() // 생성 클래스에도 있다
envelope.getDescriptorForType() // DynamicMessage 에도 있다 (Message 인터페이스 메서드)
How
FieldDescriptor field = envelope.getDescriptorForType() // 이 메시지의 설계도
.findFieldByName(ccPubSubProperties.messageData()); // "MessageData" 필드 찾기
return ((ByteString) envelope.getField(field)).toByteArray();
Java 리플랙션으로 필드를 꺼내는 것과 구조가 같다.
Class<-> Descriptor- Field <-> FieldDescriptor
위와 같이 대응된다.
'기타 > 잡동사니' 카테고리의 다른 글
| 2025년 개발 회고 (0) | 2026.01.10 |
|---|---|
| Clean Code - 테스트는 어디서든 반복 실행 가능해야한다 (0) | 2025.11.07 |
| cursor AI Editor 설정 (0) | 2025.03.31 |
| Yaml 파일 쓰기 - 고급 (0) | 2024.11.11 |
| Semantic Versioning (0) | 2024.09.20 |