[JAVA] Heap 메모리 최적화와 오류 수정 과정

업데이트:




✅ JVM Memory 최적화 문제

사내 시스템 중 특이한 서버가 하나 있다. 셀레니움 기반 데이터 수집 서버와 문서 병합을 수행하는 DCS 서버가 주를 이루고, 일부 배치작업들이 가동되고 있는 윈도우 원격 서버이다. 가장 많이 동작하는 서버는 DCS 서버이다. CS팀에서 실무를 하실 때 여러개의 문서를 병합하거나 회사 도장을 찍어서 외부에 반출하는 경우가 상당히 많은데 이런 작업을 지속적으로 수행하는 서버이기 때문에 상당히 많이 사용된다.

이 서버가 제작되고 가동된지는 꽤 오래됐다고 들었다. 얼마나 오래됐는지는 모르겠지만 적어도 5년은 넘게 코드 수정이 거의 없이 유지되고 있는 서버 정도로 알고있다.

그런데 한가지 특이한 점은 서버에 문제가 지속적으로 발생해 매일 아침 서버를 재시작 하고 있다는 사실.. 그것도 몇년 동안이나..! 매일 재시작 하고계신 사수님도 “메모리 누수 때문에 생긴 문제로 인수인계 받았다” 정도로 인지하고 계셨고 정확한 문제 원인은 모르는 상황이었다. ( 사실은 메모리 누수가 아닌 메모리 최적화 문제였다 )

다른 업무가 굉장히 많은 상황속에서 단순 재시작만으로 메모리 누수를 해결할 수 있다는 사실은 꽤나 효율적(?)이라고 볼 수 있지만 아직 입사 3개월차인 나는 담당하고 있는 업무가 많지 않기에 이번 기회에 문제 원인을 파악하고 해결해보면 좋을 것 같다는 생각이 들었다. 본인이 이런거 방치해두는 걸 못보는 편이기도 하고 .. 무엇보다 재밌어 보여서 바로 시작했다.

그 과정을 시간순으로 쭉 기록해 보기 위한 글이다.







✅ 문제 원인을 먼저 파악해보자

메모리 누수 문제라고 전달 받았지만 진짜 메모리 누수 때문에 오류가 발생하는 건지, 다른 이유인지 분명히 확인할 필요가 있어보였다. 근데 나는 메모리 분석을 해본적이 없었다.. 어떻게 하는지 전혀 몰랐기 때문에 관련 방법을 많이 찾아보며 삽질을 시작했다.



📌 Window의 리소스 모니터


먼저 DCS 서버가 얼마나 많은 메모리를 사용하고 있는지 확인하고 싶었다. 가장 단순한 방법으로는 리소스 모니터에서 메모리 탭을 통해 확인할 수 있었다. DCS서버는 JAVA 프로젝트로 가동 중 이였기 때문에 javaw.exe 라는 이름을 가진 프로세스의 메모리 사용량을 확인했고 약 1GBytes 정도를 차지하고 있었다.

원격서버의 전체 메모리가 8GBytes 인데 자바 프로그램 하나가 1GB를 차지하고 있다는 점은 좀 이상했다. 지나치게 큰 메모리를 차지하고 있는것으로 보였다. 그렇지만 이것만으로는 메모리 누수를 일으키고 있다 라고 볼 수 없었다. 메모리 누수가 생기고 있다면 메모리 사용량이 점점 증가되고 있음을 확인해야만 하는데 이 데이터로는 확인이 불가능했다.



📌 Window의 성능 모니터


메모리 증감 추이를 확인하기 위해서는 실시간 모니터링이 필요하다고 생각했다. VisualVM이나 Eclipse Memory Analyzer 같은 모니터링 툴을 찾아봤는데 러닝커브도 꽤 있어보이고 원격서버에 새로운 프로그램을 설치해서 모니터링 하는 작업은 안그래도 버벅거리는 서버에 더 큰 부하를 줄 수 있을거 같아 일단 차치해두었다. ( 결국 위의 툴을 쓰긴함 )

가장 간단한 방법은 윈도우에서 제공하는 성능 모니터 였다. 원하는 프로세스의 데이터를 선택해서 실시간으로 사용량을 추출해주는 아주 간단한 방식이였기에 시도해봤다.

아무런 작업을 하지않고 사용중인 DCS 서버의 메모리 사용량 추이의 일부이다. 리소스 모니터의 값과 동일하게 거의 1GB정도의 메모리를 사용하고 있었고 시간이 지남에 따라 메모리 사용량이 조금씩 증가함을 알 수 있었는데 이정도로는 메모리 누수가 발생한다고 확신하기 힘들었다. 오히려 시간이 많이 지나면서 GC가 동작하고 메모리 사용량은 700MB까지 줄어들기도 했다.



📌 Eclipse Memory Analyzer ( MAT )


결국 메모리 분석 툴을 사용하는편이 가장 확실하게 누수를 확인할 수 있겠다고 생각했다. JAVA 메모리 분석툴은 보통 Eclipse Memory Analyzer(MAT)나 VisualVM을 사용한다는 관련글들을 보고 MAT를 설치해서 사용해보고자 했다.

일반적으로 JAVA 프로세스에서 메모리 누수가 의심된다면 Heapdump 파일을 생성하고 분석도구를 통해 누수 원인을 찾을 수 있다. 근데 Heapdump파일을 생성하는 것부터 애를 먹었다.. 해봤어야 알지..

먼저 실행중인 자바 프로세스 ( DCS 서버 )의 PID를 확인해야 한다. cmd 명령어를 통해 확인할 수 있다.

jps -lvm

위의 명령어를 통해 실행중인 자바 프로세스를 전부 확인할 수 있었고, dcs서버의 PID를 알아냈다.

이후 heapdump파일을 생성하는 명령어를 입력해줬다.

jmap -dump:format=b,file=/dump파일을 저장할 경로/dump파일명.hprof

heapdump파일을 이렇게 생성하면 생성하는 순간의 heap 메모리 영역에 대한 데이터를 뽑아주는 hprof 파일이 만들어진다.

MAT는 이렇게 원형차트로 메모리 누수로 의심되는 요소를 보여주고 얼마나 heap 메모리 영역을 차지하고 있는지, 어떤 클래스를 참조하고 있는지 등 다양한 정보를 보여준다.

위 그림처럼 MAT에서 누수가 발생한 세부 클래스를 확인할 수도 있다. HashMap 객체들에서 누수가 가장 많이 발생하고 있음을 확인할 수 있었는데, HashMap은 일반적으로는 메모리 누수를 일으키지 않도록 설계되어 있긴하다. 그러나 HashMap을 사용한 후, 필요없는 엔트리를 제거하지 않으면 강한참조가 걸려 메모리가 GC를 통해 자동으로 해제되지 않을 수 있다. 아마 많은 HashMap에서 더이상 객체를 사용하지 않지만 강한 참조가 걸려있어 메모리 누수를 일으키는 것으로 보인다.

그런데 hprof 파일을 분석한 결과가 조금 이상하다. DCS서버가 차지하고 있는 Heap 메모리 영역은 25.7MB에 불과하고 그중에서 누수로 인해 차지하고 있는 영역은 15.3MB정도 밖에 안된다.

분명 리소스 모니터와 성능 모니터를 통해 확인한 메모리 사용량은 1GB에 육박하는데 MAT에서 분석한 메모리 누수는 굉장히 미미하다고 나왔다.

내가 제대로 분석한건지 확신이 안들었다. 다른 프로세스의 heap dump를 분석했나? MAT를 제대로 못보고 있나? 감을 잡기 어려웠다.. 아니면 혹시 메모리 누수가 아니라 메모리를 비효율적으로 사용하고 있는 것은 아닐까? 하는 의문이 강하게 들었다.



📌 VisualVM


메모리 누수가 원인이 아닐거라는 생각이 들긴 했지만 다른 분석툴로 한번더 확인해보고자 했다. JDK를 설치하면 함께 설치되는 VisualVM은 Heap 메모리 사용 현황을 대시보드 형태로 실시간으로 보여주는 장점이 있어 한눈에 메모리 사용량을 확인하기 좋아보였다.

근데 VisualVM으로 확인해도 크게 다르진 않았다. ( 코드 리팩토링 전 VisualVM 이미지를 저장하진 못했다 .. ) 메모리 누수 문제가 아니였다. 메모리 누수가 없다고 볼 순 없지만, 메모리 누수량은 현저히 적어 서버의 성능에 영향을 줄 정도가 아니였다.

문제는 메모리 누수가 아니라, 메모리가 최적화되지 않은것 으로 확실시 됐다.







✅ 메모리 낭비가 왜 발생할까

이제 메모리를 최적화 하는 작업이 필요했다. 그러기 위해선 어디서 왜 메모리 낭비가 발생하는지를 알아봐야 했다. 먼저 메모리 낭비를 일으키는 일반적인 원인을 찾아봤다.

첫째, 불필요한 객체 생성이 잦아질 때 둘째, 대용량 데이터 처리를 위해 부적절한 자료 구조 사용 시 셋째, JVM의 GC 옵션이 적절하지 못할 시

보통 위의 세가지 이유정도로 메모리가 낭비되곤 한다. 이때 첫번째, 두번째 이유는 코드를 어떻게 작성하느냐에 따라 메모리 낭비를 최소화 할 수 있지만, 세번째 이유는 코드나 로직과는 별개로 GC의 동작 주기나 Heap 메모리 용량을 조절함으로써 해결할 수 있기 때문에 근본적인 해결방법이 되지 못한다.

예를들어 컴퓨터에서 사용 가능한 메모리가 8GB 뿐인데, Heap 메모리 최대 사용 용량을 6GB정도로 설정 해버린다면 해당 프로그램은 잘 돌아갈 수 있지만, 이를 제외한 나머지 프로그램들은 2GB의 메모리밖에 사용할 수 없기 때문에 조삼모사일 뿐이다. 따라서 먼저 코드 리팩토링을 통해 서버 자체적으로 메모리 최적화를 진행하는 방식이 필요해보였다.







✅ 어디부터 리팩토링 해야 할까

이제 코드의 어느 부분이 잘못 설계되었는지 확인해볼 필요가 있었다. 그런데 너무 막연했다. 내가 만든 프로젝트도 아니고, 클래스도 한두개가 아닌데 어디부터 보고 고쳐야 할까..

먼저 인과적으로 문제를 바라보고자 했다. 기존에 발생하고 있는 문제와 이를 어떻게 해결해왔는지를 보면 갈피가 잡힐 것 같았다. 크게 문제가 발생하는 원인은 두가지 정도로 볼 수 있다.

  1. DCS 서버 자체의 메모리 문제
  2. DCS 서버가 가동중인 가상 서버 자체의 문제

DCS서버가 제대로 동작하지 않아 사내시스템에 문제가 생길 때마다 DCS서버만 재시작 해주면 정상적으로 기능이 수행됐기에 DCS서버 자체의 메모리 문제임을 확신할 수 있었다.

그럼 다음으로 DCS 서버에서 어느 클래스 혹은 패키지가 문제의 주범일까?

DCS서버 로그를 보니 주로 수행하는 기능은 다음과 같았다.

  1. 문서 병합
  2. 문서 변환
  3. 문서에 도장 이미지 추가

위의 3가지 작업이 전체작업의 90%이상을 차지하고 있었고, 이 작업들을 수행하는 과정에서 메모리 낭비가 발생하고 있음을 직관적으로 알 수 있었다. 그래서 먼저 위 3개 기능들의 관련 패키지를 리팩토링 하고자 했다.







✅ 리소스 해제가 안돼있네



📌 try-catch finally


주로 수행하는 작업이 문서 병합, 변환 등의 문서관련 작업이기 때문에 문서 처리를 위한 객체를 어떻게 생성하고 처리했나 살펴봤다. 그런데 예외처리가 제대로 되어있지 않았다. 작업이 정상적으로 완료되면 객체를 clear()하지만 예외가 발생하면 객체가 그대로 남아있는 구조였다. 문서 처리 작업을 한번 수행할 때마다 객체를 하나씩 생성하기 때문에 예외가 발생하는대로 메모리 낭비가 발생하는 구조였다.

그래서 PDFDoc 객체를 try-catch 구문 바깥으로 뺀 후 초기값을 null로 설정하고, 예외처리를 통해 PDFDoc 객체를 해제하는 코드를 추가해줬다. 원래 try-with-resource 구문을 사용해서 close() 없이 리소스를 해제하려고 했지만 프로젝트가 JDK 1.6버전을 사용하고 있어서 해당 구문을 지원하지 않아 try-catchfinally 구문을 추가했다.

PDFDoc doc = null;
		
try {
		PDFNet.initialize("라이센스");

		...
			
		doc = new PDFDoc((new StringBuilder(String.valueOf(input_path))).append(inputFileName).toString());
		doc.initSecurityHandler();
			
		...
			
		try{
			doc.save((new StringBuilder(String.valueOf(output_path))).append(outputFileName).toString(), 16L, null);
		} catch(PDFNetException e){
			System.out.println("Failed to save document: " + e.getMessage());
		}
		
		result = "Success";
			
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		closeDocument(doc);
		PDFNet.terminate();
	}

위와 비슷한 방식으로 PDF객체에 대한 생성주기 코드를 전부 리팩토링 했다. 예외상황에 발생하는 메모리 낭비와 미처 삭제되지 않은 PDF객체로 인해 발생하는 메모리 낭비에 대한 최적화를 기대했다.



📌 HashMap 객체 해제


코드를 쭉 살펴보니 상당히 많은 HashMap을 사용하고 있었다. 그런데 HashMap을 사용 후 참조를 해제하는 clear() 메소드는 전혀 작성돼있지 않았다. DCS서버가 문서 관련 작업을 수행할 때 수많은 HashMap이 생성되지만 전부 map.put("key","value") 처럼 강한 참조를 가지고 있고, GC에 의해서 해제되지 않고 있는 상황이다.

이 부분이 상당한 메모리 낭비를 일으키고 있을 것으로 예상했다. 서버에서 주로 수행하는 작업이 문서 병합, 문서 변환, 직인 도장 이미지 첨부 3가지 작업이기 때문에 이를 수행하는 클래스들 위주로 리소스를 해제해주면 해결 될 것으로 예상하고 작업을 수행했다.

HashMap<String, Object> rtnMap = docMergeService.docMerge(inputList, output_path, merge_file_name);
List<HashMap<String, Object>> inputList = new ArrayList<HashMap<String, Object>>();
List<JobDtlVo> jobDtlInsertList = new ArrayList<JobDtlVo>();

...

rtnMap.clear();
inputList.clear();
jobDtlInsertList.clear();

방식은 간단했다. HashMap을 선언하고 사용한 뒤 코드 마지막 부분에 clear() 처리 해줬다. 주의할 점은 HashMap에 들어있는 객체를 이후에도 사용해야 하거나, return을 해줘야할 때 clear()를 해버리면 값이 전부 사라지기 때문에 반드시 전부 사용한 뒤, 혹은 더이상 필요하지 않은 HashMap만 해제해야 한다. 이렇게 꽤 많은 코드에 대한 리팩토링을 수행했다.



📌 String을 StringBuilder로


코드를 또 살펴보니 작업을 수행할 때마다 그 결과를 String객체로 출력하고 있었다. log도 아니고 println 으로 매번 결과를 출력하는게 어떤 의도인지는 모르겠지만 일단 작성되어 있었으니 기조는 유지하기로 했다.

다만 String 객체에 대한 + 연산이 빈번하게 있었기 때문에 String 객체가 계속 추가되는 낭비가 있을것 같아 StringStringBuilder로 변경했다. 사실 JDK 1.5 이후 버전부터는 String 객체가 StringBuilder 객체로 컴파일 되도록 변경되어 큰 문제가 없을 수 있지만 혹시모를 메모리 낭비까지도 방지해보기 위해 변경했다.

그리고 여러 API를 쏴서 동시에 작업을 수행하는 멀티쓰레드 환경의 서버였지만, 동기화를 지원하는 StringBuffer를 사용하지 않은 이유는 단순 결과 출력을 위한 String 객체에 대한 수정 작업 이였고, 출력문을 굳이 자세히 확인하지 않는다는점, 그리고 출력문에 대한 동기화가 필수요소가 아니라는 점을 고려하여 성능이 더 좋은 StringBuilder를 사용했다.







✅ 결과를 확인해보자..!

이 자료가 VisualVM을 통해 확인한 최초의 DCS서버 HeapDump 파일이다. Instances 수치는 인스턴스의 개수를 의미하고 Size는 차지하고 있는 메모리 용량을 의미한다. 단위는 Byte이다. Char, Int, Byte, String, HashMap 순으로 메모리를 차지하고 있다. 이들의 메모리 사용량의 합은 대략 23MB 정도이다. 이제 리팩토링 전후를 비교하기 위해 최근 DCS 서버의 HeapDump 파일을 보자.


리팩토링 후 15일정도 운영한 DCS 서버의 Heap 메모리 사용량이다. Char, Byte, String, Int, HashMap 순으로 메모리를 많이 차지하고 있고 약 17~18MB정도를 사용중이다. 이전 상태와 비교하면 약 5MB정도의 Heap 메모리 사용량이 줄어든 것을 확인할 수 있다. 좀 더 확실한 비교를 위해 VisualVM의 기능을 활용해 두 덤프파일을 직접 비교해봤다.


두 덤프파일을 비교하여 Size가 가장 많이 줄어든 순서대로 클래스를 나열한 이미지이다. int[] 클래스가 가장 크게 메모리 사용량이 감소했고 char[], String, HashMap이 그 뒤를 이루고 있다. int[] 클래스가 가장 크게 감소한 이유는 HashMap 리소스를 해제하면서 HashMap 내부에서 사용되던 int배열이 함께 해제되었기 때문이라고 생각한다. 전체적으로 보면 메모리 사용량은 감소된 것을 볼 수 있다.


다음은 Size가 증가한 순서대로 클래스를 나열한 이미지이다. WeakReference 클래스가 크게 증가했음을 볼 수 있는데 이는 리팩토링이 성공적으로 이루어 졌음을 의미한다.

WeakReference는 가비지 컬렉터가 객체를 더 쉽게 회수할 수 있도록 돕는 참조 유형이다. 코드 리팩토링 과정에서 강한 참조를 약한 참조로 변경했다면, 가비지 컬렉션이 더 효율적으로 작동하여 메모리 누수를 줄일 수 있다.

본인은 try-catch-finally 구문을 통해 불필요 객체를 해제했기 때문에 weakReference 객체가 증가했고 GC가 더 효율적으로 작동함으로써 Heap 메모리 사용량이 줄어든 것으로 보인다.



📌 역시 Heap 메모리 문제가 아니야


위의 결과를 보면 리팩토링을 통해 Heap 메모리 사용량을 줄이긴 했지만 ( 물론 비율로 따지면 상당히 큰 감소 ) Java 서버 자체가 사용하고 있는 전체 메모리 양에 비하면 작은 수치이기 때문에 아직 문제가 해결됐다고 판단할 수는 없다. 적어도 Heap 메모리 수치만 확인해서는 안되고, 서버 자체의 메모리 사용량을 확인할 필요가 있어보인다.

그렇지만 기존 서버에 문제가 굉장히 잦아 매일 서버를 재시작 해주던 것과는 달리 15일정도 유지됐다는 것은 리팩토링으로 인한 메모리 최적화 작업이 효과가 있었다는 것은 분명해 보인다.

리팩토링 이후 DCS서버의 메모리 사용량이다. 기존 900MB~1GB까지 사용하던게 약200MB까지 확 줄어든 것을 볼 수 있다. 메모리 최적화 작업이 효과가 있었다..!

서버가 이전에 계속 터진이유는 아무래도 Heap 메모리 문제가 아니라 DCS 서버 자체가 차지하고 있는 메모리 용량이 지나치게 컸기 때문에 생긴게 아닐까 예상해본다.







✅ 또 서버가 터졌다..

회의를 하던 도중 서버가 터졌다. 당장 서버를 복구해야 하기에 다른 분이 서버를 복구해주시긴 했지만 에러 로그에 대한 설정이 전혀 없었기에 서버가 어떤 이유로 터진건지 전혀 알 수 없었다. 확인할 수 있는건 켜놨던 visualVM 하나.. Heap 메모리 영역에 문제가 있었는지 정도는 확인 가능했다.

메모리가 급등급락하고 있긴하지만 Max 영역을 벗어난다던가 OOM이 발생한 흔적은 보이지 않는다. 아무래도 OOM이 발생하면 에러로그를 남기는 작업이 필요해 보인다. 바로 톰캣서버에 환경변수들을 추가해줬다.



📌 OOM 로그 기록


OOM 발생시 로그 기록 argument 추가

...
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=C:\Users\Administrator\Desktop\heapdump\OOMHeapDump 
-XX:ErrorFile=C:\Users\Administrator\Desktop\heapdump\OOMErrorLog\hs_err_pid%p.log 
-Xloggc:C:\Users\Administrator\Desktop\heapdump\OOMGCLog\gc.log 
-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps

위와같은 환경변수들을 추가해줬다. 하나씩 설명해보면

  • -XX:+HeapDumpOnOutOfMemoryError : OOM 발생시 힙 덤프 생성
  • -XX:HeapDumpPath=<경로> : 힙 덤프 파일 저장 경로
  • -XX:+PrintGC: GC 이벤트를 출력
  • -XX:+PrintGCDetails: GC 이벤트의 상세 정보를 출력
  • -XX:+PrintGCTimeStamps: GC 이벤트의 타임스탬프를 출력
  • -Xloggc:<path>: GC 로그 파일이 저장될 경로 지정
  • -XX:ErrorFile=<path>: JVM 오류가 발생했을 때 오류 로그 파일이 저장될 경로 지정

이제 무슨 문제가 발생해도 로그가 다 남도록 설정해뒀으니 서버가 터지면 원인을 파악하기 쉬울 것이다. 서버가 안터지는게 제일 좋지만 일단 차분히 기다려봤다.







✅ 임시파일 생성 실패

이번에는 서버를 새로 킨 뒤 얼마 지나지 않아 오류가 발생했다. 한 4~5일정도 운영했는데 오류가 터졌다. 그렇지만 이번에는 에러 로그 설정을 해 둔 덕에 모든 오류 로그를 확인할 수 있었다..! 근데 OOM에러가 발생할거라는 예상과는 달리 전혀 다른 문제가 발생했다.


Exception: 
	 Message: Could not open a temporary file. Make sure to close unused documents.
	 Conditional expression: m_stream != NULL
	 Filename   : TempFile.cpp
	 Function   : trn::SDF::TempFile::OpenTempFile
	 Linenumber : 147
	 Error code : 0

	at pdftron.PDF.Stamper.StampImage(Native Method)
	at pdftron.PDF.Stamper.stampImage(Stamper.java:110)
	at kccworld.info.dcs.rest.api.stamp.service.StampSealServiceImpl.stampSeal(StampSealServiceImpl.java:59)
	at kccworld.info.dcs.rest.api.stamp.ApiStampSealController.stampSeal(ApiStampSealController.java:96)

	... (생략)


Exception: 
	 Message: An error occurred during page import: Could not open a temporary file. Make sure to close unused documents.
	 Conditional expression: false
	 Filename   : PDFDoc.cpp
	 Function   : trn::PDF::IMPL_TRN_PDFDocImportPages
	 Linenumber : 1291
	 Error code : 0

	at pdftron.PDF.PDFDoc.ImportPages(Native Method)
	at pdftron.PDF.PDFDoc.importPages(PDFDoc.java:829)
	at pdftron.PDF.PDFDoc.importPages(PDFDoc.java:809)
	at kccworld.info.dcs.rest.api.merge.service.DocMergeServiceImpl.mergepdf(DocMergeServiceImpl.java:147)
	at kccworld.info.dcs.rest.api.merge.service.DocMergeServiceImpl.docMerge(DocMergeServiceImpl.java:93)
	at kccworld.info.dcs.rest.api.merge.ApiDocMergeController.docConvert(ApiDocMergeController.java:80)

에러 로그를 보니 stamp 이미지를 적용하는 과정에서 임시파일을 찾을 수 없다는 에러가 보인다. 일단 임시파일을 생성한다는 사실 조차 몰랐기에 임시파일이 어느작업을 할 때 생성되고 임시파일이 왜 생성되지 않는지 확인할 필요가 있었다.

일단 임시파일이 어느 작업을 수행할 때 생성되는지 확인하기 위해 PDFtron의 jar파일을 뜯어 class파일을 확인해봤다. 그러나 class 파일이 너무 불친절해서 어느시점에서 임시파일을 생성하는지는 알기 힘들었다. 그래서 임시파일은 일단 어딘가에 생성된다고 가정하고 더이상 임시파일이 생성되지 않는 이유를 찾아봤다.

여러가지 이유 중 가장 합리적으로 판단한 경우는 “운영체제 내에서 하나의 프로세스가 연결할 수 있는 핸들러의 수가 제한되어있다.” 라는 것이었다. PDFtron의 어떤 객체를 생성할 때 임시파일이 생성되고 이 임시파일이 DCS 프로세스와 연결된 채 해제되지 않아 문서 변환,병합 등의 작업을 수행하면 수행할 수록 DCS에 연결된 핸들러의 수가 점점 증가하여 나중에는 가용할 수 있는 양을 넘어서기에 더이상 임시파일을 생성하지 못한다고 생각했다.

바로 리소스 모니터를 통해 DCS 프로세스에 연결된 핸들러를 확인해봤더니 굉장히 많은 임시파일이 연결되어 있었다. 분명 리팩토링을 하면서 PDFtron객체를 전부 사용 후 해제했다고 생각했는데 아직 해제되지 않은 무언가가 존재하는듯 했다. 일단 어느작업을 수행할 때 임시파일이 생성되는지 실시간으로 확인하기 위해 리소스 모니터를 켜둔채 문서 병합, 변환, 도장 작업을 한번씩 해봤다. 그런데 오직 도장찍는 작업을 수행할 때만 DCS에 임시파일 핸들러가 연결됐다. 드디어 찾았다!

위와 같은 임시파일들이 작업 수행 후 삭제가 안되고 DCS서버 프로세스의 핸들러로 계속 연결되어 있었다. 이제 stamp 작업을 수행할 때 왜 임시파일이 해제가 안되고 계속 연결상태를 유지하는지 원인만 찾으면 같은 오류가 반복되지 않을것 같다..!

그래서 문서 병합, 변환, 도장 작업을 수행하면서 동시에 연결된 핸들러가 어떻게 구성되는지 확인해봤는데 병합, 변환, 도장 작업 수행시에 연결되는 핸들러와는 별개로 임시파일도 함께 연결되어 있었다. 내 예상으로는 PDFTron 객체가 리소스 해제가 되지 않아 임시파일이 연결되어 있을거란 것과는 달리 객체에 대한 리소스 해제는 리팩토링 한대로 원활히 수행되고 있었고 알 수 없는 임시파일이 지속적으로 연결되어 있는 상태였다.

이 말은 내 코드의 문제가 아니라 PDFTron 라이브러리에서 제공하는 API를 사용할 때 라이브러리 내부적으로 임시파일을 생성하는 로직이 있는데 이를 자동으로 해제하지 못하고 있음을 의미했다. DCS 서버가 앞서 말했다시피 엄청 오래된 서버이고, 이 때 사용된 PDFTron 라이브러리 역시 굉장히 구버전이기 때문에 충분히 오류가 있을 수 있겠다 생각했다. 어쩔수 없이 PDFTron의 공식문서를 죄다 확인하는 수밖에 없다..







✅ 드디어 해결방법 발견..!

드디어 해결방법을 찾았다! PDFTron 객체를 사용할 때 다음과 같은 코드를 추가하면 된다.

PDFNet.initialize("라이센스 키");
PDFNet.setDefaultDiskCachingEnabled(false);

기본적으로 PDFNet(PDFTron)은 디스크캐싱을 true로 유지하는데, 이를 false로 변경하면 더이상 임시파일을 생성하지 않고, 작업 수행에 필요한 파일을 디스크에 저장한다고 한다. 디스크에 직접 저장한다는게 좀 꺼림칙 하긴 했지만 일단 해당 코드를 추가해준뒤 다시 서버를 돌려보니 더이상 임시파일이 물려있지는 않았다. 해당 코드가 추가되면서 어떤 문제를 또 일으킬지는 시간이 지나면서 확인해봐야겠지만 일단 내가 발견한 모든 오류는 해결된 셈이다.

태그:

카테고리:

업데이트:

댓글남기기