데이터가 깨져 보여요..
HI-REMS 프로젝트를 개발하며 가장 당혹스러웠던 순간은 DB에 쌓인 정체불명의 16진수 데이터를 마주했을 때였습니다. 에너지 계측 장치(RTU)는 표준 프로토콜에 따라 데이터를 보내고 있었지만, 이를 단순히 합치거나 읽으려고 하면 전혀 다른 숫자가 출력되었습니다.
body의 형태는 다음과 같았습니다.
14 01 01 00 00 00 02 00 05 02 fa 00 05 02 fa 00 e5 00 03 02 en 03 cb 02 57 00 00 00 00 3a 44 b1 00
예를 들어, 전압 값으로 0x00 0xDC가 들어왔을 때 이를 어떻게 조합하느냐에 따라 220이 될 수도, 혹은 전혀 다른 값이 될 수도 있습니다. 이 혼란의 중심에는 데이터를 메모리에 배열하는 방식인 **엔디안(Endianness)**이 있었습니다.
2. 엔디안(Endianness)이란 무엇인가?
엔디안은 컴퓨터 메모리에 연속된 바이트를 배열하는 **‘순서’**를 의미합니다. 주로 2바이트 이상의 큰 데이터를 처리할 때 어느 쪽을 먼저 저장하느냐에 따라 두 가지로 나뉩니다.
1) 빅 엔디안 (Big-endian)
- 정의: 낮은 주소에 데이터의 상위 바이트(큰 쪽)부터 저장하는 방식입니다.
- 특징: 사람이 숫자를 읽는 방식(왼쪽에서 오른쪽으로)과 같아 직관적입니다.
- 주요 사용: 네트워크 프로토콜 표준(Network Byte Order), 대형 메인프레임 등.
2) 리틀 엔디안 (Little-endian)
- 정의: 낮은 주소에 데이터의 하위 바이트(작은 쪽)부터 저장하는 방식입니다.
- 특징: 수치 연산 시 물리적으로 효율적이며, 하위 바이트만 떼어서 연산하기 유리합니다.
- 주요 사용: Intel x86, ARM 프로세서 (현대 PC 및 모바일 환경의 대다수).
3. 기기 간 데이터 불일치 (GPS 데이터의 경우)
실무에서 엔디안 변환이 필요한 가장 대표적인 경우는 데이터 수집 기기와 처리 서버의 환경이 다를 때입니다.
모바일 GPS 데이터를 PC 서버에서 처리하는 시나리오
예를 들어, 모바일 GPS 데이터를 PC에서 처리할 때 모바일 기기(ARM 기반, 리틀 엔디안)에서 수집한 위도/경도 데이터는 메모리에 역순으로 저장되어 있을 수 있습니다. 하지만 이 데이터를 전송 표준인 네트워크 바이트 순서(빅 엔디안)로 변환하지 않고 그대로 DB에 밀어 넣으면, PC(x86) 환경의 백엔드에서 읽었을 때 좌표가 지구 반대편으로 찍히는 현상이 발생합니다.
따라서 이 과정에서 DB에서 읽은 리틀 에니안 데이터를 시스템 표준인 빅 엔디안으로 재배치하는 과정이 반드시 필요합니다.
4. HI-REMS에서의 실제 적용 및 해결
관련 소스 코드 상세 확인: https://github.com/Hi-REMS/Hi-Rems-Server
HI-REMS 프로젝트는 한국에너지공단의 신재생에너지 표준 프로토콜을 준수합니다. 이 프로토콜은 데이터를 빅 엔디안(Network Byte Order) 방식으로 전송하도록 규정하고 있습니다.
1) 문제 상황의 진단
DB의 log_rtureceivelog 테이블에 저장된 body 값은 16진수 문자열 형태입니다. 이를 자바스크립트 객체로 변환하여 대시보드에 보여주기 위해서는, 각 바이트를 프로토콜 명세에 맞게 조합해야 했습니다.
14 01 01 00 00 00 02 00 05 02 fa 00 05 02 fa 00 e5 00 03 02 en 03 cb 02 57 00 00 00 00 3a 44 b1 00
2) 해결 방법: 비트 연산을 이용한 커스텀 파서(Parser) 구현
src/energy/parser.js 내에 엔디안을 고려한 정수 변환 유틸리티 함수를 구축하여 문제를 해결했습니다.
핵심 구현 코드:
// 2바이트(u16) 빅 엔디안 변환: 상위 바이트를 8비트 밀어내고 하위와 결합
const u16 = (a, i) => ((a[i] << 8) | a[i + 1]) >>> 0;
// 4바이트(u32) 빅 엔디안 변환: 가장 앞의 바이트를 가장 높은 자리수로 처리
const u32 = (a, i) =>
(((a[i] << 24) | (a[i + 1] << 16) | (a[i + 2] << 8) | a[i + 3]) >>> 0) >>> 0;
// 8바이트(u64) 누적 발전량 데이터 처리
const u64 = (a, i) =>
(BigInt(a[i]) << 56n) | (BigInt(a[i + 1]) << 48n) | ... | BigInt(a[i + 7]);
위에서 정의한 u16과 u32 함수가 실제 body 데이터를 어떻게 적용하는지 살펴보자면, 프로토콜 가이드라인에 따르면 태양광 단상(energy 0x01, type 0x01)의 전압 데이터는 5번 오프셋부터 2바이트를 차지합니다.
// 예시 데이터(body) 일부
// ... 00 00 00 02 00 05 ... (5번 인덱스부터 '00 05')
const pvVoltage = u16(b, 5);
// 연산 과정: (0x00 << 8) | 0x05 => 0x0005 => 5(V)
이처럼 단순한 16진수 나열이 비트 연산을 거쳐 우리가 이해할 수 있는 전압(5V)라는 유의미한 수치로 변환됩니다.
5. 데이터 정형화의 완성
이러한 파싱 과정을 거치면, 파편화된 Hex 데이터는 백엔드 서버 내에서 다음과 같은 깔끔한
JSON 객체로 재탄생합니다.
{
"ok": true,
"energyName": "태양광",
"metrics": {
"pvVoltage": 5,
"pvCurrent": 2,
"pvPowerW": 10,
"cumulativeWh": "250000",
"isOperating": true
}
}
6. 마치며
HI-REMS 프로젝트를 진행하며 비트 하나하나를 꼼꼼히 따져 구현한 이 파서 로직은, 현재 GS 인증을 준비하는 과정에서도 데이터의 무결성을 증명하는 강력한 무기가 되었습니다. 하드웨어의 언어(Hex)를 소프트웨어의
언어(Object)로 번역하는 과정은 주요한 역량중 하나가 아닐까 생각합니다.