문서를 닫아도 공유 라이브러리는 남아 있었다

AI Commons는 PDF나 PPTX를 가져와 페이지별 이미지로 변환한다. PDF 렌더에는 PDFium의 WebAssembly 라이브러리를 사용한다. WebAssembly는 브라우저나 서버에서 실행할 수 있는 바이너리 런타임이며, JavaScript 객체 밖에도 큰 메모리 영역을 가질 수 있다.

변환한 문서를 닫는 것과 렌더 라이브러리의 수명을 끝내는 것은 달랐다. 서비스는 PDFium 인스턴스를 캐시해 다음 가져오기에서도 재사용했다. 문서의 destroy()가 끝나도 서비스의 라이브러리 참조는 유지됐다. 작업이 없는 동안에도 라이브러리와 연결된 메모리가 남을 수 있는 구조였다.

관찰 대상뜻혼동하기 쉬운 점
JavaScript heapJS 객체가 사용하는 관리 메모리이 값만으로 WASM 사용량을 설명할 수 없다
WASM 메모리렌더 런타임의 메모리 영역문서 종료와 인스턴스 해제는 다르다
RSS프로세스가 물리 메모리에 점유한 전체 페이지참조 해제 직후 같은 양이 줄어드는 것은 아니다

이 구분 없이 RSS만 보고 GC 호출을 늘리면 객체가 아직 필요한지, WASM 인스턴스가 살아 있는지, 메모리 할당기가 해제 공간을 보관하는지 구분하기 어렵다. 먼저 자원의 소유자와 종료 조건을 정해야 했다.

동시 가져오기는 하나의 초기화를 공유한다

변경은 라이브러리를 획득하는 acquire와 반납하는 release로 묶었다. 캐시에는 완성 인스턴스 대신 초기화 중인 Promise를 저장한다. 두 요청이 초기화 완료 전 들어와도 같은 Promise를 기다리므로 별도 WASM 인스턴스를 동시에 만들지 않는다.

획득할 때 사용자 수를 늘리고, 초기화 실패 시 다시 줄인다. 실패한 초기화 Promise는 캐시에서 제거해 다음 가져오기가 새로 시도할 수 있게 한다. 정상 획득 뒤에는 파일 읽기·문서 로드·페이지 저장을 try/finally로 감싸고, 성공 여부와 관계없이 반납한다.

const library = await acquirePdfiumLibrary();
try {
  const doc = await library.loadDocument(pdfBuffer);
  await saveConvertedPages({ doc, /* ... */ });
} finally {
  await releasePdfiumLibrary();
}

PPTX는 LibreOffice로 PDF 변환을 끝낸 뒤 라이브러리를 획득한다. 외부 변환을 기다리는 동안 PDFium을 붙잡을 이유가 없기 때문이다. 소유 구간은 PDF 문서 로드부터 페이지 저장과 문서 파괴가 끝날 때까지다.

마지막 사용자가 반납할 때만 파괴한다

  1. A·B가 획득초기화 Promise 하나를 공유하고 사용자 수는 2가 된다.
  2. A가 반납사용자 수는 1이다. B가 쓰는 라이브러리를 파괴하지 않는다.
  3. B가 반납사용자 수가 0이 된다. 캐시를 먼저 null로 바꾸고 기존 인스턴스를 파괴한다.
  4. 그동안 새 획득이 들어옴캐시가 비었으므로 새 초기화를 시작한다. 파괴 중인 대상을 다시 쓰지 않는다.

파괴 대상은 지역 변수에 남기고 서비스의 참조를 먼저 끊는다. destroy()로 라이브러리의 자원을 정리한 뒤, 실행 환경이 global.gc를 제공하면 GC를 한 번 요청한다. 라이브러리 정리, JS 참조 제거, 런타임의 메모리 회수는 서로 다른 단계다.

코드 주석에는 당시 destroy 후 유휴 상태에서도 RSS가 유지됐고, 참조 해제 뒤 GC 호출에서 내려갔다는 측정이 있다. 이 글에서는 다른 파일 크기와 런타임에 재현한 결과가 없어 그 값을 일반 성능 지표로 사용하지 않는다.

정리 중의 오류가 변환 결과를 덮지 않게 한다

라이브러리 파괴 실패는 경고로 남기고 격리한다. 정리 실패가 이미 발생한 파일 읽기나 렌더 오류를 새 예외로 덮으면 원래 실패를 추적하기 어려워진다. 임시 파일 삭제와 변환 제한 타이머도 같은 이유로 종료 경로에서 해제해야 한다.

별도의 테스트 변경은 변환 시간 초과 뒤 타이머 정리와 임시 파일 삭제 실패 격리를 다룬다. 임시 파일 정리 기준은 정상 변환 시간 상한보다 길어야 한다. 사용 중인 파일을 오래된 파일로 오인해 지우는 정리 정책은 메모리 문제를 고쳐도 다른 실패를 만들 수 있다.

PDFium 수명 테스트는 라이브러리를 모킹해 공유 초기화, 동시 사용 중 미파괴, 마지막 반납, 실패 후 재초기화 같은 동작을 검사한다. 실제 WASM 메모리 곡선이나 전체 RSS 반환을 검증하는 부하 테스트는 아니다.

덜 보관하면 다음 렌더는 다시 초기화해야 한다

마지막 반납마다 파괴하면 유휴 메모리 보관을 줄일 수 있지만 다음 가져오기는 초기화 비용을 다시 낸다. 명시적 GC도 짧은 정지 시간을 만들 수 있으며, 제공되지 않는 환경에서는 호출하지 않는다. 모든 요청마다 강제 GC를 수행하는 일반 처방으로 옮길 수는 없다.

재초기화가 잦은 짧은 작업과 큰 문서를 가끔 가져오는 작업은 다른 절충을 가진다. AI Commons의 변경은 영구 캐시를 기본으로 두던 수명을 “동시에 쓰는 가져오기가 모두 끝날 때”로 좁혔다. 그 경계를 정한 뒤에야 메모리 영역별 추이와 다음 작업의 지연을 비교할 수 있다.