Mendix Java heap OOM 추적기: base64 이미지 컬럼

2026.05.15
  • 시리즈 5/16
  • 개발
  • Mendix 10
  • 성능

운영 서버가 목록 조회 중에 반복해서 다운됐다. 로그에는 java.lang.OutOfMemoryError: Java heap space가 남았다. 근본 원인은 사진을 base64로 담은 문자열 컬럼이었다. Mendix는 객체를 조회할 때 모든 속성을 함께 가져오기 때문에, 목록에 쓰지 않는 사진까지 힙에 올라갔다.

문제

아티팩트 저장소 변경으로 빌드 이미지가 바뀐 뒤부터 목록 조회 중 서버가 반복 다운

원인

사진 4장을 base64로 담은 무제한 문자열 컬럼. 50건만 조회해도 사진이 통째로 힙에 올라감. 빌드팩 버전이 바뀌며 힙도 작아짐

해결

필요한 컬럼만 OQL로 조회, 배치마다 새 컨텍스트, 배치 크기 축소. 근본적으로는 사진을 파일로 분리

추적 순서

  1. 로그 끝까지 확보재시작되면 로그가 넘어가므로 다운 직전 로그를 먼저 저장
  2. 무엇이 실행 중이었는지 확인CRITICAL 로그에 남은 작업은 업로드가 아니라 목록 조회 XPath였음
  3. 왜 무거운지 확인목록에 사진은 안 쓰는데 객체 조회는 모든 속성을 읽음. 테이블 크기 2GB 이상
  4. 힙 크기가 어디서 정해지는지 확인빌드팩이 앱 메모리 한도에서 힙을 계산. 설정 파일에 -Xmx 없음
  5. 코드와 저장 구조 수정조회 컬럼 축소, 컨텍스트 분리, 사진 저장 방식 변경
다운 직전 로그 (요약)
# java.lang.OutOfMemoryError: Java heap space
# -XX:OnOutOfMemoryError="kill -s USR2 ..."
INFO  - M2EE: Runtime shutdown requested.
CRITICAL - ActionManager: Error in execution of monitored action
  {"xpath":"//Issue.InspectionIssue[DetectDate >= '...' and DetectDate <= '...']",
   "amount":50, "type":"RetrieveXPathAction"}
java.lang.OutOfMemoryError: Java heap space
OnOutOfMemoryError 옵션Mendix 빌드팩 기본 설정에는 힙이 부족하면 런타임을 바로 종료하는 옵션이 들어 있다. 로그가 OOM 직후 깔끔하게 끊기는 이유다.

힙 크기는 빌드팩이 계산한다

설정 파일
vcap_application.jsonlimits.mem 1024
m2ee.yamljavaopts에 -Xmx 없음
빌드팩
-Xmx 결정메타스페이스, 네이티브, 웹서버 몫을 뺀 나머지
실제 힙1GB 한도에서 약 500~700MB
Cloud Foundry 계열 Mendix 빌드팩 기준
  • 빌드팩 버전이 올라가면 같은 메모리 한도에서도 힙이 달라질 수 있음
  • limits.mem을 올려도 컨테이너에 배정된 메모리가 그대로면 컨테이너 단에서 강제 종료됨
  • 로그 모양으로 구분 가능. OOM 메시지가 남으면 힙 부족, 메시지 없이 끊기면 컨테이너 한도 초과

코드 수정

필요한 컬럼만 OQL로

XPath 조회는 객체 전체를 읽는다. 목록이나 채번처럼 몇 개 컬럼만 필요하면 OQL로 컬럼을 지정한다.

Java: 채번은 최댓값 한 줄만
IContext ctx = Core.createSystemContext();
IDataTable t = Core.retrieveOQLDataTable(ctx,
    "SELECT MAX(i.SeqNo) AS maxNo FROM Issue.InspectionIssue AS i WHERE i.Year = 2026");
Long maxNo = t.getRows().isEmpty() ? null : (Long) t.getRows().get(0).getValue(ctx, 0);
long next = (maxNo == null ? 0 : maxNo) + 1;

배치마다 새 컨텍스트

배치로 나눠 조회해도 컨텍스트가 하나면 조회한 객체가 계속 쌓인다. 배치마다 컨텍스트를 새로 만들면 앞 배치 객체가 정리 대상이 된다.

Java: 배치 조회
int batch = 10, offset = 0, max = 1500;
JSONArray arr = new JSONArray();

while (offset < max) {
  IContext ctx = Core.createSystemContext();     // 배치마다 새로
  List<IMendixObject> list = Core.createXPathQuery(xpath)
      .addSort("DetectDate", false)
      .setAmount(batch).setOffset(offset)
      .execute(ctx);
  if (list.isEmpty()) break;
  for (IMendixObject o : list) arr.put(toJson(ctx, o));
  offset += batch;
}
배치 크기를 줄이는 것은 임시 조치배치를 50건에서 10건으로 줄이면 순간 점유는 줄지만 조회 횟수는 늘어난다. 사진이 객체에 붙어 있는 한 데이터가 늘면 다시 한계에 닿는다.

근본 조치

  • 사진과 첨부는 문자열 컬럼이 아니라 파일 엔티티로 분리
  • 목록, 대시보드용 조회는 화면에 쓰는 컬럼만
  • 집계는 미리 만든 결과 테이블에서 읽기

관련 글: 6편 Mendix 커밋 롤백, 용량 문제가 아니었다, 7편 Mendix 첫 접속 지연 줄이기

점검 목록

  • 무제한 문자열 컬럼에 base64 데이터를 저장하고 있지 않은가
  • 대량 조회 코드가 하나의 컨텍스트를 끝까지 쓰고 있지 않은가
  • 앱에 실제로 잡힌 힙 크기를 알고 있는가
  • 빌드 이미지나 빌드팩 버전이 바뀐 뒤 부하 테스트를 했는가
시리즈 목차 16편
  1. PI 담당자가 로우코드로 사내 시스템을 직접 만들고 운영한 기록
  2. Mendix Datagrid2 대신 HTMLSnippet과 JS로 목록 화면 만들기
  3. Mendix SPA에서 HTMLSnippet JS가 꼬이는 이유: 이벤트 중복과 DOM 공존
  4. Mendix mx.data.commit이 조용히 실패할 때
  5. Mendix Java heap OOM 추적기: base64 이미지 컬럼 (현재 글)
  6. Mendix 커밋 롤백, 용량 문제가 아니었다: FileDocument 전환기
  7. Mendix 첫 접속 지연 줄이기: 워밍업과 캐시
  8. Mendix SAML SSO 자동 로그인과 딥링크 유지
  9. Mendix Studio Pro에서 안 바뀌는 스타일 main.scss로 바꾸기
  10. Mendix MPK 파일 열어서 마이크로플로우 구조 보기
  11. SSO 로그 테이블로 MAU와 재방문율 구하는 SQL
  12. Mendix DateTime이 BI 도구에서 하루 빠르게 보이는 이유
  13. 대시보드 숫자가 틀렸을 때 확인하는 순서
  14. 보기 좋은 대시보드에서 할 일을 알려주는 대시보드로
  15. 여러 점검 시스템을 하나로 합치는 데이터 모델
  16. 데이터 비전공자에게 분석 결과 보고하기
  • #Mendix
  • #OutOfMemoryError
  • #JavaHeap
  • #OQL
  • #CloudFoundry
  • #성능개선

댓글