Coding Memo

UnityWebRequest.Post Memory Leak Error 본문

Unity

UnityWebRequest.Post Memory Leak Error

minttea25 2024. 7. 16. 22:41

UnityWebRequest.Post()를 사용하여 웹 서버에 응답을 요청했을 때, 다음과 같은 에러가 나타날 수도 있다.

(나타나지 않을 때도 있어서 이유를 찾는데 오래 걸렸다...)

 

A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.

 

 

에러에 나와있듯이, stack traces를 full로 설정하고 editor log나 player log를 확인을 했지만, 아래 에러 이상의 내용이 전혀 나타나지 않았다.(...)

또한, 어떤 컬렉션이 누수를 발생시키고 있는지도 알 수가 없었다....

 

이 문제를 해결하면서 살펴본 코드는 다음 포스트에 있다.

https://minttea25.tistory.com/161

 

 

에러 코드

public IEnumerator PostCo<T>(string url, T data, Action<UnityWebRequest.Result, string> callback)
{
    string json = Newtonsoft.Json.JsonConvert.SerializeObject(data);

    using (UnityWebRequest request = UnityWebRequest.Post(url, json))
    {
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();

        callback.Invoke(request.result, request.downloadHandler.text);
    }
}

 

내부 코드를 확인하고 또 확인해본 결과, 원인을 알아 낸 것 같았다.


문제점

 

먼저 결론을 말하자면, 이 에러가 발생한 이유는 다음과 같다.


이미 UploadHandlerRaw가 할당되어 있는 request.uploadHandler에 다른 객체(UploadHanlderRaw)를 새롭게 할당 할 때, 이전 객체의 멤버 변수, m_Payload(NativeArray type)에 대한 메모리 해제가 이루어지지 않았기 때문이다.

 

UnityWebRequest.Post 함수 내부적으로 request의 uploadHandler에 new UploadHandlerRaw(array)가 할당되는 코드가 포함되어 있다. 이후 request.uploadHandler를 다룰 때 주의해야한다.

// UnityEngine.UnityWebRequestModule (decompiled by VS2022)
// This code was written by Unity.


...
public static UnityWebRequest Post(string uri, string postData)
{
    UnityWebRequest request = new UnityWebRequest(uri, "POST");
    SetupPost(request, postData);
    return request;
}

...
private static void SetupPost(UnityWebRequest request, string postData)
{
    request.downloadHandler = new DownloadHandlerBuffer();
    if (!string.IsNullOrEmpty(postData))
    {
        byte[] array = null;
        string s = WWWTranscoder.DataEncode(postData, Encoding.UTF8);
        array = Encoding.UTF8.GetBytes(s);
        request.uploadHandler = new UploadHandlerRaw(array);
        request.uploadHandler.contentType = "application/x-www-form-urlencoded";
    }
}
...

 

Note: downloadHandler의 DownloadHandlerBuffer 클래스도 똑같이 새롭게 객체를 할당하는데 문제가 없나?

=> 이부분도 실행시켜보면 알수 있는 부분이기도 하다. DownloadHandlerBuffer 클래스도 NativeArray 타입의 멤버변수를 가지고 있지만, UnityWebRequest.Post 함수 내부에서 사용하는 생성자는 이 변수에 대해 메모리 할당이 이루어 지지않기 때문에, 단순히 소멸만 되어도 문제없다.

 

따라서 다음의 해결방법을 사용할 수 있다.


해결 방법

 

1. uploadHandler에 새로운 객체를 할당하기 전에, 기존 객체에 대한 Dispose() 명시적 호출

 

UnityWebRequest.Post로 생성한 UnityWebRequest에 대해, uploadHandler에 새로운 값을 다시 할당하기 이전에, 이전 객체에 대해 Dispose()를 명시적으로 호출하여, 이전 객체(UploadHandlerRaw 클래스)의 멤버 변수, m_Payload을 메모리 해제시킨다.

 

Note: UploadHandlerRaw의 소멸자에 Dispose가 포함되어 있지 않으므로, 명시적으로 Dispose()를 호출하지 않는 이상 내부의 동적할당된 멤버가 release되지 않아, 누수가 발생한다. (이 또한 아래에 소개해 놓았다.)

public IEnumerator PostCo<T>(string url, T data, Action<UnityWebRequest.Result, string> callback)
{
    string json = Newtonsoft.Json.JsonConvert.SerializeObject(data);

    using (UnityWebRequest request = UnityWebRequest.Post(url, json))
    {
    	// 기존에 할당되어 있던 uploadHandler의 객체에 대해 Dispose()를 명시적으로 호출
        // 그 객체의 UploadHandlerRaw.m_Payload의 메모리가 release 되어 메모리 누수 방지
    	request.uploadHandler.Dispose();
    
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();
        callback.Invoke(request.result, request.downloadHandler.text);
    }
}

 

 

 

2. UnityWebRequest.Post 함수의 postData:string 인자에 string.empty나 null 값 사용

 

UnityWebRequest.Post 내부적으로 postData인자는 string.IsNullOrEmpty(postData)를 이용하여 값이 유효한지 확인을 하고, request.uploadHandler에 해당 값에 대한 핸들러를 할당한다. 따라서 이 값을 무효(null or empty)로 하여 UnityWebRequest.Post를 호출하면 uploadHandler에 객체가 할당 되는 것을 막을 수 있을 것이다.

 

public IEnumerator PostCo<T>(string url, T data, Action<UnityWebRequest.Result, string> callback)
{
    string json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
	
    // string.Empty 값을 넣어, 생성자에서 request.uploadHandler에 객체가 할당되지 않도록 한다.
    using (UnityWebRequest request = UnityWebRequest.Post(url, string.Empty))
    {
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw); // 첫 번째 할당
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();

        callback.Invoke(request.result, request.downloadHandler.text);
    }
}

 

Note: 그렇다면 이후에 bodyRaw를 직접 넣는 것 대신에 아예 UnityWebRequest.Post 호출 시에 데이터를 넣으면 되지 않을까?

=> 그렇지 않다. 그 이유는 기존 코드는 json을 파싱하여 string으로 나타내는데, Post 내부에서는 WWWTranscoder.DataEncode 메서드로 파싱을 하기 때문이다. ProtocolError가 발생할 수 있다.

 

 

3. UnityWebRequest.Post 대신 직접 UnityWebRequest 생성자를 이용

 

UnityWebRequest 생성자에는 downloadHandler와 uploadHandler를 직접 인자로 넣는 생성자 외에는 직접적으로 downloadHandler와 uploadHandler를 새로 생성하지 않는다. 따라서 처음부터 핸들러를 지정하든지, 아니면 아래 코드처럼 method만 지정한 후에, downloadHandler와 uploadHandler를 직접 지정하면 될 것이다.

public IEnumerator PostCo<T>(string url, T data, Action<UnityWebRequest.Result, string> callback)
{
    string json = Newtonsoft.Json.JsonConvert.SerializeObject(data);

	// 아래 생성자에서는 내부적으로 uploadHandler와 downloadHandler에 객체를 넣지 않는다.
    using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
    {
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
        
        // 따라서 uploadHandler에 직접 값을 할당해야 한다.
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        
        // 마찬가지로 downloadHandler에도 직접 값을 할당해야 한다.
        request.downloadHandler = new DownloadHandlerBuffer();
        
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();
        callback.Invoke(request.result, request.downloadHandler.text);
    }
}

 


결론

 

내부 코드를 확인해 보니, Dispose()가 어떻게 호출되는지, 왜 호출이 되지 않았는지 알 수 있었고, 아무 의미 없이 request.downloadHandler에 객체를 다시 할당하고 있음을 알 수 있었다.

 

코드 길이를 줄여주는 편리한 메서드들을 무분별하게 사용하지 말자.

그 메서드를 사용하기 전에, 자신이 작성하려는 코드의 의도가 해당 메서드의 내부로직과 충돌되는 부분이 없는지 확인을 한 후 사용하자!

 


 

해당 코드를 좀 더 자세하게 살펴보았다!

https://minttea25.tistory.com/161

 

UnityWebRequest 코드

이 글은 해당 에러를 확인하면서, 정리한 글이다. 먼저 내가 사용했던 코드를 다시 한번 살펴보자.public IEnumerator PostCo(string url, T data, Action callback){ string json = Newtonsoft.Json.JsonConvert.SerializeObject(dat

minttea25.tistory.com

 

 

 

'Unity' 카테고리의 다른 글

UnityWebRequest 코드  (0) 2024.07.16
Unity에서 UnityWebRequest 사용 시, 에러 (Curl error60) (localhost, loopback)  (2) 2024.07.16
Coroutine 실행 에러  (0) 2023.12.22
[AddressableAssets] SBP ErrorError  (0) 2023.09.06
SerializedObject  (1) 2023.05.12