본문 바로가기
유니티 최고/유니티 구현

[Unity] 유니티 데이터 저장하기[1] - (PlayerPrefs + 암호화)

by Lee_story_.. 2024. 5. 21.
728x90

 

유니티를 이용해서 게임/ 어플리케이션을 제작할 경우 생성되는 데이터들...

저 역시 개발을 진행 할 수록 어플리케이션이 종료되어도 유지되는 데이터들을

어딘가에 저장해야하는 상황이 발생해버렸습니다...!

(최대한 간단하게 만들어 나갈려고 했는데 ㅎㅎ)

 

 

 

 

물론 Firebase 나 aws같은 서버를 이용해서 데이터를 저장한다면! 

완벽한 구조가 되겠지만, 비용도 발생하고, 아직까지는 데이터 베이스에 저장할 정도의 양이 아니기에

 

다른 방법들을 찾아 보았습니다!

 

 

 

가장 먼저 찾을 수 있었던 방법은 PlayerPrefs을 이용한 데이터 저장 방법입니다!

 

 

PlayerPrefs!


어플리케이션이 동작하는 기기의 특정 위치에, 데이터와 관련된 파일을 저장하여,

갱신, 수정, 불러오기 등의 역할을 수행합니다. 

 

 

Unity - Scripting API: PlayerPrefs

Unity stores PlayerPrefs in a local registry, without encryption. Do not use PlayerPrefs data to store sensitive data. Unity stores PlayerPrefs data differently based on which operating system the application runs on. In the file paths given on this page,

docs.unity3d.com

 

유니티에서 기본적으로 제공하는 라이브러리를 이용해서 보다 쉽게 제작할 수 있고,

저장한 데이터에 대해 따로 관리해줄 필요가 없다는 장점이 있습니다!

 

 

저장 방법도 아래처럼 값을 지정해주고 Save해주는게 끝...!

PlayerPrefs.SetString(keyName, "저장할 값");
PlayerPrefs.Save();

 

 

기본적으로 int, string, bool 형식의 데이터만 저장 가능하다는 단점이 있지만,

다른 변수의 경우 저장할때  ToString()함수를 통해 문자열 형식으로 저장하거나, 

PlayerPrefs.SetString("LastPlayTime", DateTime.Now.ToString());

 

 

구조체, 클래스의 경우

JsonUtility.ToJson을 이용해서 저장가능합니다.

public class PlayerData
{
    public int level;
    public float health;
    public string playerName;
}


PlayerData data = new PlayerData()


...

 

// 데이터를 JSON 문자열로 변환
        string jsonData = JsonUtility.ToJson(data);
        
        // JSON 문자열을 PlayerPrefs에 저장
        PlayerPrefs.SetString("PlayerData", jsonData);

 

 

 

 

저장위치


 

저장된 값은 

 

컴퓨터에서는 아래위치에서 변경, 확인 할 수 있습니다.

레지스트리 편집기 => HKEY_CURRENT_USER  / SOFTWARE /  Unity / UnityEditor
                             /  "지정한 회사이름파일" / "프로젝트 이름"

 

 

 

 

모바일에서는 아래 경로의 파일을 통해 확인 할 수 있습니다. 

 

Android

/data/data/"프로젝트 이름"/shared_prefs/~~.xml

 

 

IOS

\Library\Preferences\[bundle identifier].plist

 

 

 

 

 

 

 

단점.....


 

하지만 이방법에는 크나큰 단점이 존재하는데 

데이터에 대한 접근입니다. 

 

위 방법처럼 저장하게되면 데이터는 특정위치에 접근가능한 파일에 저장되게 됩니다. 

이는 사용자가 접근 가능한 파일로, 악의적인 수정이 가능합니다.....

 

그렇기에 재화, 개인정보 등에 대한 보안 처리가 불가능한 데이터 저장 방식..... 이지만 이에 대한 대책 또한 존재합니다. 

 

바로 암호화 방식인데 

저장할 데이터에 대해서 Encoding, Decoding을 이용해서 암호화 저장시키는 방식입니다. 

 

 

 

암호화 방법에도 여러가지가 존재하는데 오늘은 " AES" 대칭키 암호화 알고리즘 방식으로 데이터를 저장해 보겠습니다!

 

 

 

 

데이터 암호화


가장 먼저 AES 대칭키 암호화 알고리즘에 대해서 간단하게 요약하자면

"동일한 키를 가지고 데이터를 가지고 암호화, 복호화 시켜 데이터를 관리하는 방식" 입니다. 

 

어떠한 데이터를 저장하게 되면, 특정 길이의 문자열 키를 가지고 데이터를 암호화하여 관리 하기 때문에

키를 알지 못하면 이 값에 대해서 알아 볼 수 없도록 하는!

 

 

그럼이제 한번 구현 해봅시다. 

 

초기에는 암호화 키에 대해서 생성해 줍시다 .

(getbytes 내부의 문자열은 이를 토대로 키를 생성한다는 것! 직접 무작위로 설정해 주시면 됩니다!)

    // 암호화 키 (16바이트)
    private static readonly byte[] key = Encoding.UTF8.GetBytes("Dhw7pY3F4R2o9234");

 

이렇게 무작위 문자열을 통해 암호화 키를 생성하였다면,

 

 

다음은 이를 좀 더 강력하게 사용하기 위한 초기화 벡터를 생성해줍시다.

    // 암호화 IV (16바이트)
    private static readonly byte[] iv = Encoding.UTF8.GetBytes("Dtt7oG3F424o5555");

 

 

초기화 벡터...? 이건 뭐지...?

>> 만약 동일한 데이터가 여러번 저장되는 상황이 발생하는 상황을 가정해 보았을때

암호화 키만 사용한다면 동일한 암호화문자열이 생성될 것입니다. 

하지만 초기화 벡터를 사용해준다면, 같은 "0"을 저장할때, 저장할때마다 다른 문자열을 생성하는!

 

암호화 강화를 위한 초기화 벡터!

 

그럼 이어서 데이터를 저장해줍시다 .

 

 

 

먼저 암호화를 진행하여 저장하는 함수 SaveEncryptedData

public static void SaveEncryptedData(string keyName, string data)
    {

        using (Aes aesAlg = Aes.Create())
        {
            aesAlg.Key = key;
            aesAlg.IV = iv;
            
            // 암호화 키와 초기화 벡터를 이용하여, 암호화를 진행할 encryptor 생성
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
            byte[] encryptedData = null;

            // 일반 데이터를 암호화
            byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(data);
            encryptedData = encryptor.TransformFinalBlock(bytesToEncrypt, 0, bytesToEncrypt.Length);

            // 암호화 데이터를 문자열로 변환하여 저장 
            string encryptedString = Convert.ToBase64String(encryptedData);
            PlayerPrefs.SetString(keyName, encryptedString);
            PlayerPrefs.Save();
        }
    }

 

 

 

그리고 암호화 데이터를 복호화 하여 로드할 함수 LoadEncryptedData

 public static string LoadEncryptedData(string keyName)
    {
        string encryptedString = PlayerPrefs.GetString(keyName);
        if (!string.IsNullOrEmpty(encryptedString))
        {
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = key;
                aesAlg.IV = iv;
                
                //암호화 키와 초기화 벡터를 이용하여 복호화를 진행할 decryptor 생성
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                // 데이터 복호화
                byte[] encryptedData = Convert.FromBase64String(encryptedString);
                byte[] decryptedData = decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length);

                // 복호화된 데이터를 이용하여 저장된 데이터 반환
                return Encoding.UTF8.GetString(decryptedData);
            }
        }
        else
        {
            return null;
        }
    }

 

 

 

 

여기까지가 핵심이였고! 

다음은 위 함수를 이용하여 예시 데이터를 저장해봅시다. 

 

 

실습!


 

일단 저는 아래와 같은 클래스를 저장해보겠습니다.

public class TimeData
{
    public int playCount=5;

    public string LastPlayTime = DateTime.Now.ToString();
    public string NextTicketTime = DateTime.Now.ToString();
}

 

 

저장을 진행할 함수!

    public void SaveTimeDataFun()
    {
        SaveTime = GameManager.instance.TimeData_Str;

        SaveEncryptedData("playCount", SaveTime.playCount.ToString());
        SaveEncryptedData("LastPlayTime", SaveTime.LastPlayTime.ToString());
        SaveEncryptedData("NextTicketTime", SaveTime.NextTicketTime.ToString());
    }

 

위처럼 하나하나 저장해도 되지만 JsonUtility를 이용해서 클래스 자체를 json데이터로 만들어 저장하여도 상관없습니다!

 

 

데이터를 로드할 함수!

제일 첫 데이터인 playCount가 있는지 확인후 없으면 초기화 데이터를 저장하고, 있다면 불러오는 방식!

    public TimeData LoadTimeDataFun()
    {
        TimeData LoadTimeData = new TimeData();

        if (PlayerPrefs.HasKey("playCount"))
        {
            LoadTimeData.playCount = int.Parse(LoadEncryptedData("playCount"));
            LoadTimeData.LastPlayTime = DateTime.Parse(LoadEncryptedData("LastPlayTime"));
            LoadTimeData.NextTicketTime = DateTime.Parse(LoadEncryptedData("NextTicketTime"));
        }
        else
        {
            LoadTimeData = InitTimeDataFun();
        }


        return LoadTimeData;
    }

 

 

 

 

 

전체 코드  -  코드가 생각보다 길어졌네요....

더보기
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using static Cinemachine.DocumentationSortingAttribute;

public class TimeData
{
    public int playCount=5;

    public string LastPlayTime = DateTime.Now.ToString();
    public string NextTicketTime = DateTime.Now.ToString();
}


public class DataSave_Scr : MonoBehaviour{

    TimeData SaveTime;
    
    
    // 암호화 키 (16바이트)
    private static readonly byte[] key = Encoding.UTF8.GetBytes("Dhw7pY3F4R2o9tS6");


    // 암호화 IV (16바이트)
    private static readonly byte[] iv = Encoding.UTF8.GetBytes("Dtt7oG3F424o5r91");


    public TimeData InitTimeDataFun()
    {
        SaveEncryptedData("playCount", "5");
        SaveEncryptedData("LastPlayTime", DateTime.Now.ToString());
        SaveEncryptedData("NextTicketTime", DateTime.Now.ToString());


        return new TimeData
        {
            playCount = 5,
            LastPlayTime = DateTime.Now,
            NextTicketTime = DateTime.Now,
        };
    }


    public void SaveTimeDataFun()
    {

        SaveEncryptedData("playCount", SaveTime.playCount.ToString());
        SaveEncryptedData("LastPlayTime", SaveTime.LastPlayTime.ToString());
        SaveEncryptedData("NextTicketTime", SaveTime.NextTicketTime.ToString());
    }


    public TimeData LoadTimeDataFun()
    {
        TimeData LoadTimeData = new TimeData();

        if (PlayerPrefs.HasKey("playCount"))
        {
            LoadTimeData.playCount = int.Parse(LoadEncryptedData("playCount"));
            LoadTimeData.LastPlayTime = DateTime.Parse(LoadEncryptedData("LastPlayTime"));
            LoadTimeData.NextTicketTime = DateTime.Parse(LoadEncryptedData("NextTicketTime"));
        }
        else
        {
            LoadTimeData = InitTimeDataFun();
        }


        return LoadTimeData;
    }
    
    public static void SaveEncryptedData(string keyName, string data)
    {

        using (Aes aesAlg = Aes.Create())
        {
            aesAlg.Key = key;
            aesAlg.IV = iv;
            
            // 암호화 키와 초기화 벡터를 이용하여, 암호화를 진행할 encryptor 생성
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
            byte[] encryptedData = null;

            // 일반 데이터를 암호화
            byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(data);
            encryptedData = encryptor.TransformFinalBlock(bytesToEncrypt, 0, bytesToEncrypt.Length);

            // 암호화 데이터를 문자열로 변환하여 저장 
            string encryptedString = Convert.ToBase64String(encryptedData);
            PlayerPrefs.SetString(keyName, encryptedString);
            PlayerPrefs.Save();
        }
    }
    
    
 public static string LoadEncryptedData(string keyName)
    {
        string encryptedString = PlayerPrefs.GetString(keyName);
        if (!string.IsNullOrEmpty(encryptedString))
        {
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = key;
                aesAlg.IV = iv;
                
                //암호화 키와 초기화 벡터를 이용하여 복호화를 진행할 decryptor 생성
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                // 데이터 복호화
                byte[] encryptedData = Convert.FromBase64String(encryptedString);
                byte[] decryptedData = decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length);

                // 복호화된 데이터를 이용하여 저장된 데이터 반환
                return Encoding.UTF8.GetString(decryptedData);
            }
        }
        else
        {
            return null;
        }
    }
    


}

 

 

이렇게 쉽게 그리고 암호화 하여 데이터를 저장할 수 있었습니다!

하지만 이에 대한 한계도 있으니, 데이터의 보안에 대해서 생각을 해보고 사용해주시면 될 것 같습니다!

 

 

 

 

 

 

틀린점이 있다면 댓 달아주세요!

 

 

댓글