Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) try-catch문 ( 예외처리 )

새벽_글쓴이 2025. 1. 15. 15:37
반응형

try-catch문이란?

프로그램 실행 중 발생할 수 있는 오류(예외)를 처리하기 위한 구문

 

예상치 못한 상황이 발생했을 때 프로그램이 강제로

종료되는 것을 방지하고, 적절한 대응을 할 수 있게 해준다


기본 구조

try 
{
    // 예외가 발생할 수 있는 코드
} 
catch (예외종류) 
{
    // 예외가 발생했을 때 실행할 코드
} 
finally 
{
    // 예외 발생 여부와 관계없이 항상 실행되는 코드
}

예외처리를 사용하는 이유

1. 프로그램의 안정성 확보

public class FileManager : MonoBehaviour
{
    // 예외처리가 없는 경우
    public void SaveWithoutTryCatch(string data)
    {
        // 파일 저장 실패시 프로그램이 강제 종료될 수 있음
        File.WriteAllText("save.txt", data);
    }

    // 예외처리가 있는 경우
    public void SaveWithTryCatch(string data)
    {
        try 
        {
            File.WriteAllText("save.txt", data);
        }
        catch (IOException ex)
        {
            // 오류 발생해도 프로그램은 계속 실행
            Debug.LogError("저장 실패: " + ex.Message);
            ShowErrorPopup();
        }
    }
}

 

2. 오류 원인 파악과 디버깅

try 
{
    ProcessGameData();
}
catch (Exception ex)
{
    // 스택 트레이스를 통해 오류 발생 위치와 원인을 정확히 파악
    Debug.LogError($"에러 메시지: {ex.Message}");
    Debug.LogError($"스택 트레이스: {ex.StackTrace}");
}

try-catch문 사용 예시

public class SaveManager : MonoBehaviour
{
    public void LoadGameData()
    {
        try
        {
            // 파일을 읽으려고 시도
            string data = File.ReadAllText("savedata.json");
            Debug.Log("데이터 로드 성공!");
        }
        catch (FileNotFoundException)
        {
            // 파일이 없을 때
            Debug.LogError("저장된 데이터를 찾을 수 없습니다.");
        }
        catch (IOException ex)
        {
            // 파일을 읽는 도중 오류 발생
            Debug.LogError($"파일 읽기 오류: {ex.Message}");
        }
        finally
        {
            // 성공하든 실패하든 실행됨
            Debug.Log("로드 시도 완료");
        }
    }
}

예외 처리시 if문, switch문을 사용 안하는 이유

1. 가독성

// ==== if문 사용 ====
public void LoadGameData()
{
    if (!File.Exists("save.txt"))
    {
        Debug.LogError("파일이 없습니다.");
        return;
    }
    
    string fileContent = File.ReadAllText("save.txt");
    if (string.IsNullOrEmpty(fileContent))
    {
        Debug.LogError("파일이 비어있습니다.");
        return;
    }
    
    PlayerData data = JsonUtility.FromJson<PlayerData>(fileContent);
    if (data == null)
    {
        Debug.LogError("데이터 변환 실패");
        return;
    }
}

// ==== try-catch 사용 ====
public void LoadGameData()
{
    try
    {
        string fileContent = File.ReadAllText("save.txt");
        PlayerData data = JsonUtility.FromJson<PlayerData>(fileContent);
        ProcessPlayerData(data);
    }
    catch (FileNotFoundException)
    {
        Debug.LogError("파일이 없습니다.");
    }
    catch (JsonException)
    {
        Debug.LogError("잘못된 데이터 형식입니다.");
    }
}

 

2.예외상황 전파

// ==== if문 사용 - 오류를 매번 체크해야 함 ====
public bool Method1()
{
    if (!ValidateData())
        return false;
    return true;
}

public bool Method2()
{
    if (!Method1())
        return false;
    return true;
}

// ==== try-catch 사용 - 오류는 자동으로 상위로 전파됨 ====
public void Method1()
{
    if (!ValidateData())
        throw new ValidationException();
}

public void Method2()
{
    try
    {
        Method1();
    }
    catch (ValidationException ex)
    {
        // 필요한 지점에서 한 번만 처리
        HandleError(ex);
    }
}

 

3.처리 가능한 상황의 범위

// ==== switch문 - 예상 가능한 상황만 처리 ====
switch (errorCode)
{
    case 404:
        HandleNotFound();
        break;
    case 500:
        HandleServerError();
        break;
    default:
        HandleUnknown();
        break;
}

// ==== try-catch - 예상치 못한 상황도 처리 가능 ====
try
{
    ConnectToServer();
}
catch (HttpRequestException ex)
{
    // 모든 종류의 네트워크 오류 처리
    HandleNetworkError(ex);
}
catch (Exception ex)
{
    // 예상치 못한 모든 오류 처리
    HandleUnexpectedError(ex);
}

 

예외처리는 주로 예상치 못한 오류 상황이나 외부 리소스 접근 시에 사용하고,

if문이나 switch문은 일반적인 프로그램 로직의 흐름 제어에 사용하는 것이 바람직하다


주요 예외 타입

System.Exception : 모든 예외의 기본 클래스
ArgumentException : 잘못된 인수를 메서드에 전달할 때
NullReferenceException : null 객체를 참조할 때
IndexOutOfRangeException : 배열의 범위를 벗어난 접근
FileNotFoundException : 파일을 찾을 수 없을 때
IOException : 입출력 관련 오류
FormatException : 문자열 형식이 잘못되었을 때

실제 활용

세이브 데이터 로드
public class GameDataManager : MonoBehaviour
{
    [Serializable]
    public class PlayerData
    {
        public int level;
        public float health;
    }

    public PlayerData LoadPlayerData()
    {
        string path = Application.persistentDataPath + "/playerData.json";
        
        try
        {
            // 파일에서 데이터 읽기
            string jsonData = File.ReadAllText(path);
            
            // JSON을 객체로 변환
            PlayerData data = JsonUtility.FromJson<PlayerData>(jsonData);
            
            if (data == null)
                throw new System.Exception("데이터 변환 실패");
                
            return data;
        }
        catch (FileNotFoundException)
        {
            Debug.Log("저장된 데이터가 없습니다. 새 데이터를 생성합니다.");
            return new PlayerData();
        }
        catch (Exception ex)
        {
            Debug.LogError($"데이터 로드 중 오류 발생: {ex.Message}");
            return null;
        }
    }
}

 

리소스 로드
public class ResourceLoader : MonoBehaviour
{
    public GameObject LoadPrefab(string prefabPath)
    {
        try
        {
            GameObject prefab = Resources.Load<GameObject>(prefabPath);
            
            if (prefab == null)
                throw new System.ArgumentException($"프리팹을 찾을 수 없음: {prefabPath}");
                
            return Instantiate(prefab);
        }
        catch (System.ArgumentException ex)
        {
            Debug.LogError($"프리팹 로드 실패: {ex.Message}");
            return null;
        }
        catch (Exception ex)
        {
            Debug.LogError($"알 수 없는 오류: {ex.Message}");
            return null;
        }
    }
}

 

파일 저장 시스템
public class SaveSystem : MonoBehaviour
{
    public void SaveGame()
    {
        string savePath = Application.persistentDataPath + "/save.json";
        
        try
        {
            // 1. 데이터 준비
            GameData data = PrepareGameData();
            if (data == null)
            {
                Debug.LogError("데이터 준비 실패");
                return; // 예외를 발생시키지 않고 일반적인 흐름 제어
            }

            // 2. 파일 저장 (예외가 발생할 수 있는 작업)
            string jsonData = JsonUtility.ToJson(data);
            File.WriteAllText(savePath, jsonData);
        }
        catch (UnauthorizedAccessException ex)
        {
            // 구체적인 예외 처리
            Debug.LogError("저장 권한이 없습니다: " + ex.Message);
            ShowErrorToUser("저장 권한이 없습니다. 관리자 권한으로 실행해주세요.");
        }
        catch (IOException ex)
        {
            // 더 일반적인 입출력 예외 처리
            Debug.LogError("저장 실패: " + ex.Message);
            ShowErrorToUser("게임 저장에 실패했습니다. 디스크 공간을 확인해주세요.");
        }
        catch (Exception ex)
        {
            // 가장 일반적인 예외 처리
            Debug.LogError("예상치 못한 오류: " + ex.Message);
            ShowErrorToUser("알 수 없는 오류가 발생했습니다.");
        }
    }

    private void ShowErrorToUser(string message)
    {
        // UI를 통해 사용자에게 오류 메시지 표시
    }

    private GameData PrepareGameData()
    {
        // 게임 데이터 준비 로직
        return null;
    }
}

주의 사항

1. 예외는 더 구체적인 것부터 처리해야 한다

public void LoadGameData()
{
    try 
    {
        string data = File.ReadAllText("save.json");
        ProcessData(data);
    }
    
    // ==== 잘못된 순서 ====
    catch (Exception ex) // 모든 예외를 잡아버림
    {
        Debug.Log("오류 발생");
    }
    catch (FileNotFoundException ex) // 절대 실행되지 않음!
    {
        Debug.Log("파일을 찾을 수 없음");
    }

    // ==== 올바른 순서 ====
    catch (FileNotFoundException ex) // 구체적인 예외 먼저
    {
        Debug.Log("파일을 찾을 수 없음");
    }
    catch (Exception ex) // 일반적인 예외는 나중에
    {
        Debug.Log("오류 발생");
    }
}

 

2. 성능 고려

public class ItemChecker
{
    // ==== 나쁜 예: 예외처리를 통한 로직 처리 ====
    public bool ContainsItem_Bad(List<string> inventory, string itemName)
    {
        try
        {
            // First()는 찾지 못하면 예외를 발생시킴
            inventory.First(item => item == itemName);
            return true;
        }
        catch
        {
            return false; // 예외가 발생하면 false 반환
        }
    }

    // ==== 좋은 예: 일반적인 조건문 사용 ====
    public bool ContainsItem_Good(List<string> inventory, string itemName)
    {
        // Any()는 조건에 맞는 항목이 있는지 확인만 하고 예외를 발생시키지 않음
        return inventory.Any(item => item == itemName);
    }
}

 

예외를 발생시키고 잡는 과정이 필요해서 성능이 좋지 않기

프로그램 로직 흐름 제어일반적인 조건문을 사용하는 것이 좋다

 

3. 예외 다시 던지기(Re-throwing)

public class DataManager : MonoBehaviour
{
    // ==== 나쁜 예: 스택 트레이스 정보가 손실됨 ====
    public void ProcessData_Bad()
    {
        try
        {
            LoadData();
        }
        catch (Exception ex)
        {
            Debug.Log("오류 발생");
            throw ex; // 새로운 예외를 생성하여 던짐
        }
    }

    // ==== 좋은 예: 원래 예외의 정보가 유지됨 ====
    public void ProcessData_Good()
    {
        try
        {
            LoadData();
        }
        catch (Exception ex)
        {
            Debug.Log("오류 발생");
            throw; // 원래 예외를 그대로 다시 던짐
        }
    }

    private void LoadData()
    {
        // 데이터 로드 로직...
    }
}
반응형