Coding Memo

UnityWebRequest 코드 본문

Unity

UnityWebRequest 코드

minttea25 2024. 7. 16. 22:40

이 글은 해당 에러를 확인하면서, 정리한 글이다.

 


먼저 내가 사용했던 코드를 다시 한번 살펴보자.

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);
    }
}

 

특별히 문제가 없어보이는 코드이고, 실행하면 정상적으로 작동하며, 올바르게 response도 받아온다.

그러나, 프로그램을 종료하는 등의 메모리 정리 동작에 들어갈 시에 높은 확률로 아래 에러가 나타난다.

 

해당 에러에 나와있는 대로, 스택까지 full로 해놓고 확인해보았지만, 이 에러에 대한 내용이 나타나지 않았다. 그래서 직접 코드를 살펴보았다.

 

처음에는 request가 제대로 Dispose가 되지 않나? 생각을 했다. 

(물론 그럴리는 없을 것이다. 제대로 using을 사용하고 있기 때문이다.)

 


아래 코드 나타나는 코드는 Visual Studio 2022에서 디컴파일된 유니티 엔진 코드의 일부 (Assembly UnityEngine.UnityWebRequestModule)이다.

 

UnityWebRequest.Post

//
// Summary:
//     Creates a UnityWebRequest configured to send form data to a server via HTTP POST.
//
//
// Parameters:
//   uri:
//     The target URI to which form data will be transmitted.
//
//   postData:
//     Form body data. Will be URLEncoded prior to transmission.
//
// Returns:
//     A UnityWebRequest configured to send form data to uri via POST.
public static UnityWebRequest Post(string uri, string postData)
{
    UnityWebRequest request = new UnityWebRequest(uri, "POST");
    SetupPost(request, postData);
    return request;
}

 

 

UnityWebRequest.SetupPost

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";
    }
}

 

 

내 코드에서는 내부적으로 위와 같은 코드들이 실행이 되고 있었던 것이다.

 

하지만 여전히 어느 부분에서 메모리 누수가 나는지 알 수 없었다.

 

따라서 다소 의심스러운 downloadHandler와 uploadHandler를 살펴보기로 했다.

 

코드를 보면, 내 코드와 SetupPost 메서드 내부에서 똑같이 DownloadHandlerBuffer 클래스와 UploadHandlerRaw 클래스를 사용하여 각각 downloadHandler와 uploadHandler에 할당하고 있다.

(여기서도 하나 알 수 있는데, 불필요하게 request.downloadHandler에 동일하게 다시 new DownloadHandlerBuffer()를 넣을 필요가 없었던 것이다.)

 

이 두 클래스는 각각 DownloadHandler와 UploadHandler를 상속하고 있는 핸들러이며, IDisposable 인터페이스를 가지고 있다. 따라서 UnityWebRequest와 같이 using으로 자원을 관리할 수 있고, Dispose()호출이 가능한 클래스라는 것이다.

더보기
// decomplied DownloadHandlerBuffer
...
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/UnityWebRequest/Public/DownloadHandler/DownloadHandlerBuffer.h")]
public sealed class DownloadHandlerBuffer : DownloadHandler
{ ... }
// decompiled DownloadHandler
...
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/UnityWebRequest/Public/DownloadHandler/DownloadHandler.h")]
public class DownloadHandler : IDisposable
{ ... }
// decompiled UploadHandlerRaw
...
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/UnityWebRequest/Public/UploadHandler/UploadHandlerRaw.h")]
public sealed class UploadHandlerRaw : UploadHandler
{ ... }
// decompiled UploadHandler
...
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/UnityWebRequest/Public/UploadHandler/UploadHandler.h")]
public class UploadHandler : IDisposable
{ ... }

 

 

그렇다면 UnityWebRequest가 소멸될 때, 해당 클래스의 Dispose에서 각 핸들러들이 Dispose가 제대로 되지 않는 것은 아닐까?

 

// decompiled UnityWebRequest
...
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/UnityWebRequest/Public/UnityWebRequest.h")]
public class UnityWebRequest : IDisposable
{
...
[NonSerialized]
internal DownloadHandler m_DownloadHandler;

[NonSerialized]
internal UploadHandler m_UploadHandler;
...
//
// Summary:
//     If true, any DownloadHandler attached to this UnityWebRequest will have DownloadHandler.Dispose
//     called automatically when UnityWebRequest.Dispose is called.
public bool disposeDownloadHandlerOnDispose { get; set; }

//
// Summary:
//     If true, any UploadHandler attached to this UnityWebRequest will have UploadHandler.Dispose
//     called automatically when UnityWebRequest.Dispose is called.
public bool disposeUploadHandlerOnDispose { get; set; }
..

public UploadHandler uploadHandler
{
    get
    {
        return m_UploadHandler;
    }
    set
    {
        ...
        m_UploadHandler = value;
    }
}
public DownloadHandler downloadHandler
{
    get
    {
        return m_DownloadHandler;
    }
    set
    {
        ...
        m_DownloadHandler = value;
    }
}
...
private void InternalSetDefaults()
{
    disposeDownloadHandlerOnDispose = true;
    disposeUploadHandlerOnDispose = true;
    disposeCertificateHandlerOnDispose = true;
}
...
public UnityWebRequest(string url, string method)
{
    m_Ptr = Create();
    InternalSetDefaults();
    this.url = url;
    this.method = method;
}
...
~UnityWebRequest()
{
    DisposeHandlers();
    InternalDestroy();
}

public void Dispose()
{
    DisposeHandlers();
    InternalDestroy();
    GC.SuppressFinalize(this);
}

private void DisposeHandlers()
{
    if (disposeDownloadHandlerOnDispose)
    {
        downloadHandler?.Dispose();
    }

    if (disposeUploadHandlerOnDispose)
    {
        uploadHandler?.Dispose();
    }

    ...
}
...
}

 

코드가 좀 길지만, 분석을 해보면 Post에서 사용하는 생성자는 disposeDownloadHandlerOnDispose와 disposeUploadHandlerOnDispose 값이 기본적으로 true로 되어 있는 것을 알 수 있다. 그리고 UnityWebRequest가 소멸이 되거나 Dispose()가 호출되면 DisposeHandler()가 호출되면서 downloadHandler와 uploadHandler의 Dispose()도 각각 호출되는 것을 확인할 수 있다.

 

실행 할 때에는 전혀 문제가 없었으므로, 실행중에는 해당 메서드가 제대로 실행되는 것을 알 수 있다. 

using 이 끝나면 Dispose가 호출이 될테니까!

 

DownloadHandler와 UploadHandler에서 발생할 수 있는 문제가 있는지 확인해보자.

 

먼저 DownloadHandler 클래스에는 아래 코드와 같이 nativeData라는 필드가 있는 것을 확인할 수 있다. 메모리 누수가 발생할 수 있는 NativeArray.ReadOnly 구조체이다. 하지만 virtual 메서드인 GetNativeData를 통해 값을 get하고 있으므로 DownloadHandlerBuffer 클래스를 확인해 보아야 할 것이다.

// decompiled DownloadHandler
public class DownloadHandler : IDisposable
{
...
public NativeArray<byte>.ReadOnly nativeData => GetNativeData().AsReadOnly();
...
protected virtual NativeArray<byte> GetNativeData()
{
    return default(NativeArray<byte>);
}
...
~DownloadHandler()
{
    Dispose();
}
}

 

다음은 UploadHandler이다. UploadHandler 클래스는 NativeArray를 반환하는 필드나 이 타입의 멤버가 없다. 대신 byte[] 타입의 data와 가상 메서드인 GetData가 있는 것으로 보아, 이 클래스를 상속하는 클래스에게 해당 역할을 맡기고 있는 것으로 보인다.

// decompiled UploadHandler
public class UploadHandler : IDisposable
{
...
public byte[] data => GetData();
...
internal virtual byte[] GetData()
{
    return null;
}
...
}

 

 

그렇다면 문제가 발생하는 클래스는 DownloadHandlerBuffer나 UploadHandlerRaw, 혹은 두 클래스 모두일 것이다.

 

따라서 각각의 코드도 좀 더 자세히 살펴보자.

 

먼저, DownloadHandlerBuffer이다.

// decompiled DownloadHandlerBuffer
...
public sealed class DownloadHandlerBuffer : DownloadHandler
{
    private NativeArray<byte> m_NativeData;

    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern IntPtr Create(DownloadHandlerBuffer obj);

    private void InternalCreateBuffer()
    {
        m_Ptr = Create(this);
    }

    public DownloadHandlerBuffer()
    {
        InternalCreateBuffer();
    }

    protected override NativeArray<byte> GetNativeData()
    {
        return DownloadHandler.InternalGetNativeArray(this, ref m_NativeData);
    }

    public override void Dispose()
    {
        DownloadHandler.DisposeNativeArray(ref m_NativeData);
        base.Dispose();
    }
...
}

 

해당 클래스에 NativeArray 타입의 멤버, m_NativeData가 존재하지만, GetNativeData 함수를 확인해보면 이 값은 Http요청으로 받은 응답을 다운로드 했을 때 값이 생기는 것 같다. 따라서 Dispose에서도 동일하게 DownloadHandler의 static 메서드를 이용하여 ref 값으로 해당 데이터를 Dispose하고 있는 것으로 보인다.

 

무엇보다도, UnityWebRequest.Post에서 사용되는 default 생성자는 부모 클래스인 m_Ptr 값만 생성하고 정작 m_NativeData는 그대로 냅두고 있는 것을 알 수 있다. 따라서 네트워크를 통해 다운로드 된 데이터가 없는 이상, 이 클래스의 Dispose가 호출되지 않아도 m_NativeData는 여전히 메모리 할당되어 있는 상태가 아니라는 것이다. 즉, 이 클래스에서는 내 코드의 시나리오에서 NativeArray에 대한 메모리 누수가 날 가능성이 없다.

 

마지막으로 UploadHandlerRaw 클래스이다.

// decompiled UploadHandlerRaw
...
public sealed class UploadHandlerRaw : UploadHandler
{
private NativeArray<byte> m_Payload;
...
public UploadHandlerRaw(byte[] data)
    : this((data == null || data.Length == 0) ? default(NativeArray<byte>) : new NativeArray<byte>(data, Allocator.Persistent), transferOwnership: true)
{
}
...
public override void Dispose()
{
    if (m_Payload.IsCreated)
    {
        m_Payload.Dispose();
    }

    base.Dispose();
}
}

 

드디어 문제를 찾은 것 같다.

UnityWebRequest.Post에서 사용하는 생성자에서 data가 유효한 값일 경우 그 값으로 NativeArray를 생성하고 있는 것을 알 수 있다!!!

Post 메서드에서 해당 클래스를 new를 통해 생성하고 Dispose()의 명시적 호출 없이 다시 new를 통해 request.uploadHandler에 새로운 값을 넣었기 때문에 기존 클래스(UploadHandlerRaw)의 m_Payload가 메모리 할당 해제가 되지 않아서 메모리 누수가 발생한 것임을 짐작할 수 있다!!!

심지어 소멸자가 명시적으로 작성되어 있지도 않았다.

 

Note: using을 사용하면 IDisposable 객체에 대해 Dispose()가 자동으로 호출이 되겠지만, 단순히 소멸이 된 경우는 그렇지 않다. 소멸자에 Dispose()를 명시적으로 작성하는 등의 방법을 사용해서 메모리를 잘 관리해야 한다. (물론 소멸자는 C++과 다르게 언제 호출 될지 모르지만!)

 

다음을 참고해도 좋다!

 

 

 


뭔가 천천히 하나하나 짚어가면서 문제가 될 수 있는 코드를 확인해 보았다. 시간이 오래걸렸지만, 나름(?) 스스로 찾아봐서 재밌었다.