Dev/Debug

[트러블 슈팅] 메모리 누수

隣のプログラマー君 2025. 3. 29. 19:58
반응형

멈추지 않고 차오르는 메모리 사용량.

 

얼마 전에 작업해서 배포를 하고 주말이 지난 뒤 회사에 가보니, 내가 만든 시스템이 안된다는 문의가 폭주하고 있더라. 그래서 컨테이너 상황을 확인해봤더니, 서버가 제기능을 못하고 죽어있었음. 다행히도 문제 확인은 쉬웠던게, 그 전 주에 개발한 내용이 Input/Output Stream 을 쓰는 이미지 동기화 시스템이었어서 "아.. 이거 분명 메모리 누수다.." 라는 확신이 들었다. 삽질은 아니지만 같은 실수를 반복하지 말고, 다른 초급 개발자들 역시 이런 실수는 하지 말자는 뜻에서 한번 남겨본다.

 

1. 메모리 누수 (Memory Leak)란?

메모리 누수(Memory Leak)는 말 그대로 메모리가 샌다는 뜻이다. 직관적인 말이라기보단 살짝 물이 새는거에 비유한거라 도대체 그게 무슨 말입니까? 라고 말을 하게 될텐데, 대개 객체지향 언어를 사용하는 프로그램은 인스턴스를 만들어서 메모리에 올려두고 그 인스턴스들이 다른 인스턴스를 호출한다던가, 새로운 또다른 인스턴스를 만들어낸다던가 하는 식으로 상호작용하며 동작하는게 기본적인 원리다. 근데, 이게 원래 일반적으로 Java로 만든 시스템의 경우 JVM 내에 만들어져있는 GC (가비지 콜렉터) 라는 녀석이 자동으로 안쓰는 인스턴스들을 긁어다 폐기처분하는 역할을 해주는데, 사용하지 않는 상태임에도 불구하고 사용하지 않는 상태라고 판단이 되지 않아 계속 메모리를 점유하는 문제가 생긴다. 이게 바로 메모리 누수임. 즉, 뭔가 계속 샌다라기보단 뭔가 계속 있어서 사용할 공간이 점점 줄어든다고 생각하면 되겠다.

 

Java 의 경우엔 디폴트 설정 메모리 값이 256MB고, 애초에 실행할 때 메모리 값을 지정해서 실행하는 식이다보니 브라우저가 동적으로 메모리를 갖다쓰는 프론트엔드와는 다르게 이 메모리 사용에 대해서 신중하게 고려해 개발해야한다. 

 

2. 이번 사례

이번 사례는 서두에서도 말했듯 Input/Output Stream을 사용하는 로직이어서 발생하는 메모리 누수였다. 이 Input/Output Stream을 사용하는 경우에 메모리 누수가 발생할 확률이 높아지는데 그 이유가 얘네는 사용하면 프레임 워크를 사용하지 않고 단순하게 JDBC만을 사용하는 것 처럼 꼭 사용 후엔 close() 해줘야 메모리를 점유하지 않는 애들이기 때문이다.

나도 당연히 이 사실을 알고 있기 때문에 잘 처리 했다고 생각했으나, 제대로 확인하지 못해 InputStream만 처리하고 OutputStream은 처리하지 않은데다, List내 다건 처리 로직이었던 터라 Loop 안에서 Stream 객체를 계속 생성해서 미친듯한 메모리 점유끝에 서버가 폭발하는 사고가 났던거임.

 

3. 해결

솔직히 해결방법이 새로운건 아니다. 원래도 InputStream은 정상적으로 처리하고 있었고, OutputStream만 적용이 안되어있었을 뿐인거라 똑같이 추가만 해주면 되는 문제였다. 하지만 내 블로그는 주로 신입개발자, 초급 개발자들이 보기 때문에 해결방안도 코드로 작성해주도록 하겠다. 

 

public void streamExample() {
	InputStream inputSteam = new ByteArrayInputStream();
	try {
		// 각종 로직 처리
    } catch (Exception e) {
    	// 예외 처리
    } finally {
    	// close() 사용하여 사용 종료
    	inputStream.close()
    }
}

 

일반적으로 알고 있는 방식은 위의 코드와 같은 방식으로 사용하게 되어있다. 근데 이제 최근엔 저 finally 블럭 안에서 처리하지 않고, try-with-resource 라는 구문을 사용해 처리하는 방식을 많이 씀.

public void streamExample() {
	try (InputStream inputSteam = new ByteArrayInputStream();) {
		// 각종 로직 처리
    } catch (Exception e) {
    	// 예외 처리
    }
}

 

위와 같은 코드처럼 작성하게 되는데, 이 경우 로직이 실행완료되든 예외가 발생하든 간에 무조건 close()를 자동으로 호출해주게 되어있어서, 메모리 누수의 위험을 줄여주어 개발자가 신경쓰지 않아도 되게끔 해준다. 

나 역시도 try-with-resource 구문을 사용했는데, 난 Input/Output Stream 두개 쓰고 있으니 괄호 안에 두개 넣어주면 끝나는 문제였음.

 

public void streamExample() {
	try (InputStream inputSteam = new ByteArrayInputStream();
    	OutputStream outputStream = new ByteArrayOutputStream();
    ) {
		// 각종 로직 처리
    } catch (Exception e) {
    	// 예외 처리
    }
}

 

이런 식으로 작성하면 된다.

LIST

'Dev > Debug' 카테고리의 다른 글

[MyBatis] Resources 찾기 대소동  (0) 2024.04.19
[AgGrid] Client Side Data, Server Side Data 분리 문제  (0) 2023.04.19