OCPP JSON 메모리에서 시작한 tinyalloc 구조 개선

충전기에서 OCPP 메시지는 JSON 형식을 사용하고 있고, JSON 생성과 파싱에는 cJSON을 사용하고 있다. JSON 객체를 만들고 해제하는 과정에서는 작은 동적 메모리 할당이 반복된다. 장시간 재부팅 없이 동작해야 하는 충전기에서 이 할당을 시스템 공용 Heap과 그대로 섞어 쓰는 것은 부담이 있었다. 그래서 JSON이 사용하는 메모리를 FreeRTOS Heap과 분리하고, 별도의 고정 RAM 영역을 tinyalloc으로 관리해 왔다.

 

GitHub - thi-ng/tinyalloc: malloc / free replacement for unmanaged, linear memory situations (e.g. WASM, embedded devices...)

malloc / free replacement for unmanaged, linear memory situations (e.g. WASM, embedded devices...) - thi-ng/tinyalloc

github.com

tinyalloc은 사용자가 지정한 메모리 영역을 독립적인 heap처럼 사용할 수 있는 작은 allocator다. 원본은 embedded device나 WASM처럼 unmanaged linear memory를 사용하는 환경을 주요 대상으로 설명하고 있다.

OCPP JSON 메모리와 FreeRTOS Heap 분리

cJSON은 JSON 객체를 노드 구조로 관리한다. OCPP 메시지를 생성하거나 수신한 JSON을 파싱하는 동안 객체와 key/value 문자열을 위한 동적 할당과 해제가 반복된다. 이 패턴을 FreeRTOS 공용 Heap에서 계속 처리하면 다른 기능에서 사용하는 동적 메모리와 섞인다. 전체 free 공간이 남아 있더라도 작은 크기의 할당과 해제가 반복되면서 연속된 free block이 잘게 나뉘면 max_block이 작아질 수 있다. 결국 전체 여유 공간과 별개로 필요한 크기의 연속 메모리를 확보하지 못하는 상황을 피하고 싶었다.

 

그래서 JSON용 RAM 영역을 따로 두고 cJSON의 할당을 tinyalloc으로 연결했다. 현재 JSON 객체는 생성 후 서버로 전송하거나, 수신 메시지를 파싱해 필요한 로직을 처리한 뒤 바로 해제한다. 객체 수명이 길게 겹치는 구조가 아니어서 지금까지 tinyalloc 자체의 fragmentation이 실제 문제로 나타난 적은 없다.

tinyalloc 메타데이터 오버헤드

다만 JSON 영역으로 16KB를 잡았다고 해서 16KB 전체를 데이터에 사용할 수 있는 것은 아니었다. tinyalloc 역시 할당 상태를 관리하기 위한 메타데이터가 필요하다. 현재 프로젝트에서 확인한 구조를 기준으로 allocator 관리 영역은 약 24바이트이고, 각 block descriptor는 12바이트를 사용한다. 따라서 max_blocks를 크게 잡을수록 동시에 관리할 수 있는 block 수는 늘어나지만 실제 payload가 사용할 수 있는 RAM은 줄어든다.

JSON Pool : 16KB
max_blocks: 256

metadata
= 24 + (12 × 256)
= 3,096 bytes

실제 데이터에 사용할 수 있는 영역
= 16,384 - 3,096
= 13,288 bytes

예전에는 16KB JSON pool에 max_blocks를 512개까지 잡아 사용한 이력이 있었는데, 이 경우 descriptor 오버헤드가 약 6KB까지 커진다. 이번 정리 과정에서는 실제 payload 영역을 확보하기 위해 16KB pool의 max_blocks를 256개로 줄였다. 대신 block descriptor 사용량이 80% 이상으로 올라가면 warning을 출력하도록 진단 로직을 추가했다. 메모리 크기뿐 아니라 block 개수도 별도의 제한이라는 점을 런타임에서 확인하기 위한 장치다.

GetConfiguration AllKey의 cJSON 메모리 한계

이 제약이 가장 잘 드러나는 경우가 OCPP GetConfiguration의 AllKey 응답이었다. 충전기에 설정된 Configuration Key가 많아질수록 cJSON으로 전체 응답을 만들 때 필요한 노드 수도 함께 증가한다. 이때 최종 JSON 문자열 크기만큼만 메모리가 필요한 것이 아니다. 최종 문자열을 만들기 전까지 cJSON 객체와 key/value 문자열이 동시에 존재하고, 각각의 할당은 tinyalloc block도 사용한다. 현재 충전기에서 AllKey 요청에 대해 실제 전송하는 JSON 문자열 길이는 약 5KB다. 하지만 전체 Configuration Key를 cJSON tree로 구성하면 node 수와 메모리 사용량이 먼저 한계에 접근했다.

AllKey JSON 직접 생성

그래서 AllKey처럼 노드를 많이 사용하는 일부 OCPP 메시지는 cJSON object tree를 만들지 않고 JSON 문자열을 직접 생성하는 방식을 사용하고 있다. cJSON_malloc()으로 JSON 전용 tinyalloc 영역에서 연속된 버퍼를 하나 확보하고, sprintf 계열로 JSON 형식을 직접 만든 뒤 서버로 전송한다. 전송이 끝나면 cJSON_free()로 바로 해제한다.

일반 OCPP 메시지
cJSON object 생성 → 전송/처리 → 즉시 해제

GetConfiguration AllKey
cJSON_malloc() → JSON 문자열 직접 생성 → 전송 → cJSON_free()

현재 충전기에서는 약 5KB의 AllKey 전송 버퍼를 FreeRTOS 공용 Heap에 추가로 잡기 어려웠기 때문에, 기존 JSON 전용 tinyalloc 영역을 그대로 활용했다. 결과 문자열 자체는 크더라도 수많은 cJSON node를 동시에 만들지 않기 때문에 block 수와 peak memory 사용량을 줄일 수 있다.

PnC 확장과 기존 tinyalloc 구조의 한계

현재까지 tinyalloc은 사실상 cJSON 전용 allocator였다. cJSON은 OCPP 처리뿐 아니라 ESP32와 데이터를 처리하는 다른 thread에서도 사용하고 있기 때문에, 서로 다른 thread가 같은 JSON pool에 접근할 수 있었다. 그래서 기존에는 오픈소스 cJSON 쪽에 mutex 처리를 추가해 JSON 메모리 접근을 보호했다. tinyalloc의 사용처가 cJSON 하나뿐일 때는 이 구조로도 충분했다.

 

하지만 PnC 기능을 확장하면서 상황이 달라졌다. 인증서 관련 데이터를 처리하려면 JSON과 별개의 메모리 풀을 만들 필요가 생겼고, 확보되는 RAM 영역에 CERT 전용 pool을 구성하는 방향을 검토하고 있다. tinyalloc을 cJSON 외의 메모리 풀에서도 사용하려면 동기화 책임을 cJSON에 둘 수 없다. 실제 공유 자원은 상위의 JSON 라이브러리가 아니라 tinyalloc이 관리하는 memory pool이기 때문이다.

tinyalloc Multi-Pool과 Mutex 구조

이번 작업에서는 먼저 tinyalloc을 여러 개의 독립된 memory pool에서 사용할 수 있는 구조로 정리했다. 동시에 mutex 처리도 cJSON에서 제거하고 allocator 내부에서 처리하도록 방향을 변경했다.

기존

OCPP Thread ─┐
             ├─ cJSON + Mutex ── tinyalloc ── JSON Pool
ESP32 Thread ┘


변경 방향

cJSON ───────┐
             ├─ tinyalloc ── JSON Pool + Mutex
CERT 처리 ───┘           └─ CERT Pool + Mutex

구조 변경 방향과 적용 범위를 정한 뒤 Codex를 이용해 현재 tinyalloc 사용 위치, Multi-Pool 적용 가능성, mutex 처리 위치를 순서대로 검토했다. 이후 수정 범위를 cJSON과 tinyalloc 중심으로 한정해 Multi-Pool 기반구조와 allocator 내부 mutex 처리를 적용하고, 기존 JSON 처리의 기본 동작까지 확인했다.

 

작업 과정에서 max_blocks에 따른 메타데이터 오버헤드도 다시 확인해 16KB JSON pool의 block 수를 512개에서 256개로 조정했고, descriptor 사용률이 80% 이상일 때 warning을 남기도록 진단 로직도 추가했다.

기반구조 구현과 현재 검증 범위

이번 작업에서 PnC 인증서용 memory pool을 충전기 전체 코드에 적용한 것은 아니다. 현재 완료된 범위는 tinyalloc을 여러 memory pool에서 사용할 수 있도록 기반구조를 변경하고, 멀티스레드 환경에서 allocator 자체가 mutex를 이용해 접근을 보호하도록 정리한 것까지다. 변경 후 기존 JSON 처리의 기본 동작도 확인했다.

 

실제 CERT pool을 어느 RAM 영역에 구성할지, 인증서 데이터의 allocation/free 시점을 어떻게 가져갈지, PnC 처리 로직과 연결했을 때 기존 기능에 어떤 영향이 있는지는 아직 후속 검토가 필요하다. 처음에는 OCPP JSON의 잦은 동적 할당을 FreeRTOS 공용 Heap에서 분리하기 위해 사용한 tinyalloc이었다. PnC 기능을 준비하면서 이제는 JSON 하나를 위한 allocator가 아니라 여러 전용 메모리 영역을 관리할 수 있는 기반구조가 필요해졌다. 이번에는 그 기반구조와 기존 기능의 기본 동작 확인까지 정리했다.