운영 서버가 목록 조회 중에 반복해서 다운됐다. 로그에는 java.lang.OutOfMemoryError: Java heap space가 남았다. 근본 원인은 사진을 base64로 담은 문자열 컬럼이었다. Mendix는 객체를 조회할 때 모든 속성을 함께 가져오기 때문에, 목록에 쓰지 않는 사진까지 힙에 올라갔다.
문제
아티팩트 저장소 변경으로 빌드 이미지가 바뀐 뒤부터 목록 조회 중 서버가 반복 다운
원인
사진 4장을 base64로 담은 무제한 문자열 컬럼. 50건만 조회해도 사진이 통째로 힙에 올라감. 빌드팩 버전이 바뀌며 힙도 작아짐
해결
필요한 컬럼만 OQL로 조회, 배치마다 새 컨텍스트, 배치 크기 축소. 근본적으로는 사진을 파일로 분리
추적 순서
- 로그 끝까지 확보재시작되면 로그가 넘어가므로 다운 직전 로그를 먼저 저장
- 무엇이 실행 중이었는지 확인CRITICAL 로그에 남은 작업은 업로드가 아니라 목록 조회 XPath였음
- 왜 무거운지 확인목록에 사진은 안 쓰는데 객체 조회는 모든 속성을 읽음. 테이블 크기 2GB 이상
- 힙 크기가 어디서 정해지는지 확인빌드팩이 앱 메모리 한도에서 힙을 계산. 설정 파일에 -Xmx 없음
- 코드와 저장 구조 수정조회 컬럼 축소, 컨텍스트 분리, 사진 저장 방식 변경
다운 직전 로그 (요약)
# 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 spaceOnOutOfMemoryError 옵션Mendix 빌드팩 기본 설정에는 힙이 부족하면 런타임을 바로 종료하는 옵션이 들어 있다. 로그가 OOM 직후 깔끔하게 끊기는 이유다.
힙 크기는 빌드팩이 계산한다
설정 파일
vcap_application.jsonlimits.mem 1024
m2ee.yamljavaopts에 -Xmx 없음
계산
빌드팩
-Xmx 결정메타스페이스, 네이티브, 웹서버 몫을 뺀 나머지
실제 힙1GB 한도에서 약 500~700MB
- 빌드팩 버전이 올라가면 같은 메모리 한도에서도 힙이 달라질 수 있음
- 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편
- PI 담당자가 로우코드로 사내 시스템을 직접 만들고 운영한 기록
- Mendix Datagrid2 대신 HTMLSnippet과 JS로 목록 화면 만들기
- Mendix SPA에서 HTMLSnippet JS가 꼬이는 이유: 이벤트 중복과 DOM 공존
- Mendix mx.data.commit이 조용히 실패할 때
- Mendix Java heap OOM 추적기: base64 이미지 컬럼 (현재 글)
- Mendix 커밋 롤백, 용량 문제가 아니었다: FileDocument 전환기
- Mendix 첫 접속 지연 줄이기: 워밍업과 캐시
- Mendix SAML SSO 자동 로그인과 딥링크 유지
- Mendix Studio Pro에서 안 바뀌는 스타일 main.scss로 바꾸기
- Mendix MPK 파일 열어서 마이크로플로우 구조 보기
- SSO 로그 테이블로 MAU와 재방문율 구하는 SQL
- Mendix DateTime이 BI 도구에서 하루 빠르게 보이는 이유
- 대시보드 숫자가 틀렸을 때 확인하는 순서
- 보기 좋은 대시보드에서 할 일을 알려주는 대시보드로
- 여러 점검 시스템을 하나로 합치는 데이터 모델
- 데이터 비전공자에게 분석 결과 보고하기