2024
05.27

 

서론

 유니티에서 개발할 때 암호화용도로 안티치트의 ObscuredInt, ObscuredString 등의 클래스를 사용하고 있다.

또한 Obscured 클래스들을 사용하여 테이블 데이터를 스크립터블 오브젝트로 캐싱해 놓고 사용하고 있다.

 

 그런데 이 클래스들은 원본 값이 바뀌지 않았어도 시리얼라이즈 할 때마다 페이크 값이 바뀌기 때문에 파싱 할 때마다 데이터가 언제나 변경한 것처럼 보이게 된다.

 

 때문에 암호화가 되었어도 바뀐 부분만 업데이트가 이루어 질 수 있게 Equals로 값을 비교한 다음, 바뀌었을 때만 대입하는 식으로 해결하고 있다. Obsucred 타입들은 내부에 Equals 오버라이드를 구현해 놔서 기본형들과 비교 가능하기 때문.

 

 단일 객체를 비교하는 경우는 기본형 타입들이나 Obscured타입들은 이미 같음을 확인할 수 있게 구현이 되어있으므로 바로 비교하면 되지만 데이터의 컨테이너끼리 비교를 해야되는 경우 조금은 주의를 해야 한다.

 

데이터 객체들을 비교하는 방법을 테스트해보도록 하자.

 

1. 기본 구현의 같음 테스트

테스트 1번 : 클래스

우선 데이터 클래스는 다음과 같이 준비해 본다. (클래스)

public class MyClass
{
    public int num1;
    public int num2;
    public MyClass(int num1, int num2)
    {
        this.num1 = num1;
        this.num2 = num2;
    }
}

 

테스트는 다음과 같이 준비했다.

public void Test()
{
    var a = new MyClass(1, 2);
    var b = new MyClass(1, 2);

    Debug.Log("---------테스트------------");
    Debug.Log("== : " + (a == b));
    Debug.Log("objectEquals : " + (a.Equals(b)));
}

당연하지만 클래스는 별도의 구현이 없다면 레퍼런스 같음을 사용하기 때문에 ==나 Equals가 false이다.

 

테스트 2번 : 구조체

public void Test1()
{
    var a = new MyData(1, 2);
    var b = new MyData(1, 2);

    Debug.Log("---------테스트------------");
    Debug.Log("objectEquals : " + (a.Equals(b)));
}

별도 구현이 없다면 == 를 사용할 수 없다. Equals는 true를 반환한다. 

 

테스트 3번 : 레코드

public void Test1()
{
    var a = new MyData(1, 2);
    var b = new MyData(1, 2);

    Debug.Log("---------테스트------------");
    Debug.Log("== : " + (a == b));
    Debug.Log("objectEquals : " + (a.Equals(b)));
}

레코드는 별도 구현 없이도 ==를 사용할 수 있으며, ==와 Equals는 둘 다 True를 반환한다.

 

2. IEuatable<T>의 구현 테스트

System 네임 스페이스에서 제공되는 IEquatable<T> 인터페이스를 구현해 보자.

객체 참조여서 같음이 될 수 없었던 클래스 타입에서 IEuatable<T>를 구현해 보면 다음과 같다.

public class MyData : IEquatable<MyData>
{
    public int num1;
    public int num2;
    public MyData(int num1, int num2)
    {
        this.num1 = num1;
        this.num2 = num2;
    }

    public bool Equals(MyData other)
    {
        if(other == null) return false;
        return (this.num1 == other.num1) && (this.num2 == other.num2);
    }
}

 

이제 실행해 보면 아까와는 결과가 다르다.

object의 Euqals는 같음을 나타내지만 == 는 같지 않다고 한다.

 

이제 == 까지 override 해서 구현해 보자. ==를 재정의하면 != 또한 재정의 해야 한다.

public class MyData : IEquatable<MyData>
{
    public int num1;
    public int num2;
    public MyData(int num1, int num2)
    {
        this.num1 = num1;
        this.num2 = num2;
    }

    public bool Equals(MyData other)
    {
        if(other == null) return false;
        return (this.num1 == other.num1) && (this.num2 == other.num2);
    }

    public static bool operator ==(MyData a, MyData b)
    {
        if (a is null)
        {
            if (a is null)
                return true;

            return false;
        }
        return a.Equals(b);
    }

    public static bool operator !=(MyData a, MyData b)
    {
        return a != b;
    }
}

이제 어느 쪽을 사용하던 클래스도 구조적 같음으로 비교할 수 있게 되었다. 

 

구조체의 경우 null을 허용하지 않기 때문에 null 체크 부분만 삭제해 주면 된다.

public struct MyData : IEquatable<MyData>
{
    public int num1;
    public int num2;
    public MyData(int num1, int num2)
    {
        this.num1 = num1;
        this.num2 = num2;
    }

    public bool Equals(MyData other)
    {
        return (this.num1 == other.num1) && (this.num2 == other.num2);
    }

    public static bool operator ==(MyData a, MyData b)
    {
        return a.Equals(b);
    }

    public static bool operator !=(MyData a, MyData b)
    {
        return a != b;
    }
}

 

 

마치며..

 커다란 데이터를 가지고 있고, 게임 내내 지속되는 테이블 데이터의 경우 class에 Equals를 구현해서 사용하곤 한다. 그런데 최근 추가된 record 형식은 구조적 같음을 별도 구현할 필요 없다. 때문에 불변 객체로 사용해야 하는 부분에 조금씩 적용해보고 있다.

 

 

COMMENT