본문 바로가기

Programming

예외처리 및 프로그램 디버깅 : 쉽고 재미있는 C# Programming 의 기본

 

예외처리 (Exception Handling)

 

코드를 작성하다보면 예외적인 상황이나 에러가 발생할 수 있습니다.

예를 들면 파일을 열려고 할 때 파일이 존재하지 않거나, 0으로 나누는 등의 상황이 발생할 수 있습니다. 이런 예외상황을 처리하기 위해 try, catch, finally 블록이 사용됩니다.

 

1. try 블록

  • try 블록은 예외가 발생할 수 있는 코드를 감싸는 부분입니다.
  • 예외가 발생하면 try 블록의 실행이 중단되고, 제어는 가장 가까운 catch 블록으로 이동합니다.
  • try 문은 한꺼번에 많은 코드를 감싸려고 하는 것보다는, 문제점이 야기될 가능성이 있는 코드 라인에 코드블록으로 감싸줍니다.
try
{
    // 예외가 발생할 수 있는 코드
}

 

2. catch 블록

  • catch 블록은 예외를 처리하는 데 사용됩니다.
  • 여러 개의 catch 블록을 사용하여 다양한 종류의 예외를 다르게 처리할 수 있습니다.
  • 예외가 발생하면 해당 catch 블록이 실행되고, 예외 객체가 제공됩니다.
  • catch 문은 여러 개의 형식으로 나누어 중첩 구현할 수 있습니다.
catch (ExceptionType1 ex1)
{
    // 예외 처리 코드
}
catch (ExceptionType2 ex2)
{
    // 다른 예외 처리 코드
}

 

3. finally 블록

  • finally 블록은 예외가 발생하든 안 하든 항상 가장 마지막에 실행되는 코드를 포함합니다.
  • 주로 리소스 해제나 정리 작업을 위해 사용됩니다.
  • finally 블록은 선택적이며 생략 가능합니다.
finally
{
    // 항상 실행되어야 하는 코드
}

 

4. 예시

try
{
    // 예외가 발생할 수 있는 코드
    int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
    // 0으로 나누기 예외 처리 코드
    Console.WriteLine("Error: Division by zero.");
}
catch (FormatException ex)
{
    // 잘못된 형식 예외 처리 코드
    Console.WriteLine("Error: Invalid format.");
}
finally
{
    // 항상 실행되는 코드 (예외가 발생해도 실행)
    Console.WriteLine("Finally block executed.");
}

 

5. 활용

 

catch 블록의 인수는 예외를 처리하는 데 사용되는 부분으로, 이룰 통해 특정 유형의 예외에 대한 처리를 구체화할 수 있습니다. catch 블록의 괄호 안에 들어가는 인수는 처리하고자 하는 예외의 유형을 나타냅니다.

try
{
    // 예외가 발생할 수 있는 코드
}
catch (ExceptionType ex)
{
    // 예외 처리 코드
}

 

여기서 ExceptionType은 처리하고자 하는 특정 예외의 유형을 나타냅니다. 이렇게 특정 예외 유형을 명시하면 해당 유형의 예외가 발생했을 때만 해당 catch 블록이 실행됩니다.

try
{
    int result = 10 / int.Parse("abc"); // FormatException 발생
}
catch (FormatException ex)
{
    Console.WriteLine("Error: Invalid format.");
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("Error: Division by zero.");
}

 

이 경우, catch (FormatException ex) 블록은 abc를 정수로 변환하는 과정에서 발생하는 FromatException 을 처리하고, catch (DivideByZeroException ex) 블록은 0으로 나누는 과정에 발생하는 DivideByZeroException 을 처리합니다.

 

FormatExceptionDivideByZeroException은 .NET 프레임워크에서 제공하는 시스템 예외 유형입니다. 이러한 예외 유형들은 특정 예외 상황에 대한 내장된 메커니즘을 제공하며, 사용자는 이러한 시스템 예외 유형을 활용하여 예외 처리를 구현할 수 있습니다.

 

  1. Exception:
    • 모든 예외의 기본 클래스로서, 대부분의 시스템 예외들은 Exception 클래스를 상속합니다.
  2. SystemException:
    • 모든 CLR(공용 언어 런타임) 예외의 기본 클래스입니다.
    • CLR과 관련된 예외들이 이 클래스를 상속합니다.
  3. ApplicationException:
    • 모든 응용 프로그램 예외의 기본 클래스입니다.
    • 응용 프로그램에서 발생한 예외를 나타냅니다.
  4. ArithmeticException:
    • 산술 연산 예외의 기본 클래스입니다.
    • 예를 들어, DivideByZeroException와 OverflowException이 여기에 속합니다.
  5. DivideByZeroException:
    • 0으로 나누기 연산 시 발생하는 예외입니다.
  6. OverflowException:
    • 연산 결과가 해당 형식의 최대값을 초과할 때 발생하는 예외입니다.
  7. FormatException:
    • 형식이 잘못된 경우 발생하는 예외로, 주로 문자열을 숫자로 변환할 때 사용됩니다.
  8. IndexOutOfRangeException:
    • 배열 또는 컬렉션의 인덱스가 범위를 벗어날 때 발생하는 예외입니다.
  9. ArgumentNullException:
    • 메서드에 null을 전달할 때 발생하는 예외입니다.
  10. NullReferenceException:
    • null 참조를 역참조할 때 발생하는 예외입니다.
  11. FileNotFoundException:
    • 파일을 찾을 수 없을 때 발생하는 예외입니다.
  12. IOException:
    • 입출력 동작 중에 발생하는 일반적인 예외 클래스입니다.

이 외에도 다양한 예외 유형이 존재하며, 각각의 예외 유형은 특정한 예외 상황을 나타냅니다. 개발자는 이러한 예외 유형들을 적절히 활용하여 코드를 안전하게 구현하고 예외 처리를 수행할 수 있습니다.

또한 개발자는 필요에 따라 직접 예외 유형을 만들어서 사용할 수 있습니다. 사용자 정의 예외 클래스를 만들면 특정 프로젝트나 라이브러리에서 발생할 수 있는 독자적인 예외를 처리하는 데 도움이 됩니다.

 

사용자 정의 예외를 만들려면, 새로운 클래스를 작성하고 해당 클래스를 Exception 클래스 또는 Exception 의 파생 클래스로 만들어야 합니다. 보통 예외 클래스는 기본 생성자와 메시지를 전달할 수 있는 생성자를 가지도록 작성됩니다.

 

using System;

public class CustomException : Exception
{
    // 기본 생성자
    public CustomException() : base("Custom exception occurred.")
    {
    }

    // 메시지를 받는 생성자
    public CustomException(string message) : base(message)
    {
    }
}

 

위 코드에서 CustomException은 Exception 클래스를 상속하고 있습니다.

Exception 클래스는 .NET 프레임워크에서 제공하는 기본 예외 클래스로서, 모든 예외 클래스의 기본 클래스입니다.

사용자 정의 예외 클래스를 만들 때에는 이 Exception 클래스나 그의 파생 클래스를 상속하여 만드는 것이 일반적입니다.

  1. public CustomException() : base("Custom exception occurred."):
    • CustomException의 기본 생성자입니다.
    • base 키워드는 기본 클래스인 Exception 클래스의 생성자를 호출합니다.
      base 키워드는 파생 클래스에서 기본 클래스의 멤버를 호출할 때 사용됩니다.
      base 키워드 뒤에는 기본 클래스의 멤버(메서드, 속성등)가 올 수 있습니다.
    • Exception 클래스의 생성자는 예외 객체에 대한 기본적인 초기화를 담당합니다.
      *생성자(constructor)는 클래스의 인스턴스가 생성될 때 자동으로 호출되는 특별한 종류의 메서드입니다. 생성자의 주요목적은 클래스의 인스턴스를 초기화하고 객체의 상태를 설정하는 것입니다.
      C#에서 생성자는 클래스와 동일한 이름을 가지며, 반환 값이 없습니다. 일반적으로 클래스의 필드를 초기화하거나 초기화 코드를 실행하는 데 사용됩니다. 생성자는 클래스 내부에 정의되며, 인스턴스를 생성할 때 자동으로 호출되어 객체를 초기화합니다.
    • : base("Custom exception occurred.") 부분은 Exception 클래스의 생성자에 전달될 예외 메시지를 설정합니다. 즉, 이 생성자는 "Custom exception occurred."라는 기본 메시지를 가지는 예외 객체를 생성합니다.
  2. public CustomException(string message) : base(message):
    • 매개변수를 받는 생성자로, 사용자가 원하는 메시지를 제공할 수 있도록 합니다.
    • 다시 한번, base 키워드를 사용하여 Exception 클래스의 생성자를 호출합니다.
    • 이 생성자는 사용자가 전달한 메시지를 사용하여 예외 객체를 초기화합니다.

6. 사용 예

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Enter an integer within the range:");
        
        try
        {
            int userInput = GetValidInteger();
            Console.WriteLine($"You entered: {userInput}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        finally
        {
            Console.WriteLine("Program execution completed.");
        }
    }

    static int GetValidInteger()
    {
        string input = Console.ReadLine();
        
        try
        {
            int number = int.Parse(input);
            
            // 예외 처리: 정수 범위 체크
            if (number < 0 || number > 100)
            {
                throw new ArgumentOutOfRangeException("Input must be within the range of 0 to 100.");
            }
            
            return number;
        }
        catch (FormatException)
        {
            // 예외 처리: 정수가 아닌 문자열 입력
            Console.WriteLine("Error: Please enter a valid integer.");
        }
        catch (OverflowException)
        {
            // 예외 처리: 정수 범위를 벗어난 입력
            Console.WriteLine("Error: Integer out of range.");
        }
        catch (ArgumentOutOfRangeException ex)
        {
            // 예외 처리: 정수 범위를 벗어난 입력
            Console.WriteLine($"Error: {ex.Message}");
        }

        // 예외가 발생하면 -1을 리턴
        return -1;
    }
}

 

  1. Main 메소드:
    • 사용자에게 정수를 입력하도록 안내하고, GetValidInteger 메소드를 호출하여 입력된 정수를 가져옵니다.
    • try 블록에서는 GetValidInteger 메소드 호출 결과를 출력합니다.
    • 만약 예외가 발생하면 catch 블록에서 해당 예외의 메시지를 출력합니다.
    • finally 블록은 예외 발생 여부와 관계없이 항상 실행되어 "Program execution completed."을 출력합니다.
  2. GetValidInteger 메소드:
    • Console.ReadLine()을 사용하여 사용자로부터 문자열을 입력받습니다.
    • try 블록에서는 입력된 문자열을 int.Parse를 사용하여 정수로 변환합니다.
    • 변환된 정수가 0에서 100 사이의 범위에 속하지 않으면 throw new ArgumentOutOfRangeException를 통해 예외를 발생시킵니다.
    • catch 블록에서는 다양한 예외 유형에 대한 예외 처리를 수행합니다.
    • 예외가 발생한 경우 해당 메시지를 출력하고, -1을 반환합니다.
    • 마지막으로 finally 블록은 항상 실행되며, 예외가 발생하더라도 -1이 반환됩니다.

이 코드는 사용자로부터 정수를 입력받아 해당 범위에 속하는지 확인하고, 예외 처리를 통해 사용자에게 적절한 메시지를 출력하는 간단한 예제입니다.

 

[참고]

문자열 보간 (string interpolation) : $ 기호를 사용해서 문자열 중간에 변수나 표현식의 값을 삽입하여 편리하게 문자열을 만들 수 있습니다.

$"{변수 또는 표현식}"

//=================================================

int number = 42;
string result = $"The value is: {number}";

// result 변수에 The vale is 42 라는 문자열 할당

//=================================================

catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

// 예외 객체의 Message 속성 값이 문자열에 삽입되어 출력

//=================================================

int x = 5;
int y = 10;

// 표현식 사용
string result = $"The sum of {x} and {y} is: {x + y}";

Console.WriteLine(result);

// {x + y} 부분은 표현식이며, 변수 x와 y의 합계를 나타냅니다.
따라서 출력 결과는 "The sum of 5 and 10 is: 15"가 됩니다.

 

 

 

특정 라인에서 F9키를 누르거나, 코드 라인 옆에 회색으로 된 빈 공간을 마우스로 클릭하면 빨간색 둥근 원이 생성됩니다.

 

F5 키 혹은, 상단 메뉴에서 [디버그] > [디버깅 시작]을 클릭하면 프로그램이 실행되고, 해당라인에서 멈추는 것을 확인할 수 있습니다. 이를 브레이크 포인트(Break Point)라고 합니다. 참고로 [Shift + F5] 를 누르면 모든 디버깅이 중지됩니다.

 

브레이크 포인트가 연결된 후에는 아래와 같은 작업을 수행할 수 있습니다.

일단 초보자들이 디버깅을 할 때 가장 기본이 되는 세가지 키를 알려드리겠습니다.

 

F10을 클릭하면 코드 라인이 한 줄씩 수행됩니다.

 

그리고 F11을 크릭하면 코드 라인에서 수행되는 세부 동작 (메소드 호출과 같은)이 수행됩니다.

 

F5를 클릭하면 다음 브레이크 포인트까지 수행됩니다.

만약 브레이크 포인트가 없으면 프로그램이 종료됩니다.

 

전체적인 흐름을 보기 위해서는 IDE 툴의 조사식에 변수를 대입하고 나서 자동지역 탭에서 전체 변수의 값들을 확인할 수 있습니다.

 

아래 화면과 같이 조사식에 weathers 변수면을 입력하니, 해당 배열에 들어 있는 값을 확인할 수 있습니다.

(대부분 디버깅 단계의 흐름에 따라, 지정된 변수 혹은 객체는 자동으로 조사식에 추가됩니다.)

 

브레이크 포인트는 어느 지점에 두는 것이 좋을까요?

 

정답은 없습니다. 확인하고자 하는 변수에 대해 데이터 핸들링(값을 집어넣는 작업) 시점 혹은 핸들링이 끝났을 시점에 브레이크 포인트를 주는 것이 좋습니다.

 

디버깅의 중요성은 큰 프로그램에서 데이터의 흐름 혹은 버그를 찾는 데 정말 중요한 역할을 합니다.

처음 배울때부터 익숙해지는 것이 좋습니다.