C# 심화: 예외 처리와 디버깅 기법

6 minute read

안녕하세요! 저는 여러분의 코딩 구원투수, 재준봇입니다!

자, 여러분. 지금까지 저와 함께 C#의 험난한 여정을 달려오셨군요. 그런데 말입니다. 코딩을 하다 보면 분명히 내 머릿속에서는 완벽하게 돌아가야 할 코드가, 실행 버튼만 누르면 갑자기 “퍽!” 하고 터지면서 빨간 줄을 뿜어내는 경험, 다들 해보셨죠?

그때의 그 당혹감! 마치 야심 차게 준비한 고백 멘트를 날렸는데 상대방이 “누구세요?”라고 답하는 것과 같은 충격이죠. 하지만 걱정 마세요. 오늘 배울 ‘예외 처리와 디버깅’만 제대로 익히면, 프로그램이 터졌을 때 당황해서 컴퓨터를 끄는 게 아니라 “음, 여기서 문제가 생겼군” 하며 여유롭게 해결하는 ‘진짜 개발자’의 포스를 풍길 수 있게 됩니다.

오늘 강의는 분량이 좀 많습니다. 하지만 제가 아주 찰떡같은 비유로 다 퍼드릴 테니까, 끝까지 집중해서 따라오세요. 이거 모르면 나중에 실무 나가서 진짜 큰일 납니다!


10강: C# 심화: 예외 처리와 디버깅 기법

1. 예외(Exception)란 무엇인가? 왜 배워야 하는가?

먼저 ‘예외’라는 개념부터 잡고 갑시다. 코딩에서 예외란, 프로그램 실행 중에 발생하는 ‘예상치 못한 사고’를 말합니다.

비유를 들어볼까요? 여러분이 아주 정교한 햄버거 주문 키오스크 프로그램을 만들었다고 칩시다.

  • 정상 경로: 손님이 메뉴를 고른다 -> 결제를 한다 -> 주문이 완료된다.
  • 예외 상황: 결제하려는데 카드가 한도 초과다! 혹은 손님이 숫자 입력창에 “배고파요”라고 글자를 입력한다!

이런 상황에서 예외 처리를 안 해두면 어떻게 될까요? 프로그램은 “어? 나 이거 어떻게 처리해야 할지 몰라!”라고 비명을 지르며 그대로 종료됩니다. 사용자 입장에서는 그냥 앱이 강제 종료된 것이죠.

하지만 고수는 다릅니다. 예외 처리를 잘한 프로그램은 이렇게 반응합니다. “손님, 카드 한도가 초과되었습니다. 다른 카드를 사용해 주세요.”

이렇게 프로그램이 죽지 않고 우아하게 대처하는 방법, 그것이 바로 예외 처리(Exception Handling)입니다.


2. 예외 처리의 핵심 무기: try-catch-finally

C#에서 예외를 처리하는 가장 대표적인 방법은 try, catch, finally 세 가지 키워드를 사용하는 것입니다. 이 셋의 관계를 쉽게 설명하면 다음과 같습니다.

  • try: “일단 한번 시도해 봐!” (사고가 날 가능성이 있는 코드 영역)
  • catch: “사고 났다! 내가 수습할게!” (에러가 발생했을 때 실행되는 구급차)
  • finally: “사고가 났든 안 났든, 이건 무조건 마무리해!” (뒷정리 담당)

자, 그럼 이걸 실제로 어떻게 쓰는지 3가지 단계별 예제로 뜯어보겠습니다.

첫 번째: 가장 기본적인 try-catch (단순 수습형)

가장 단순하게 “에러 나면 일단 알려줘!” 수준의 코드입니다.

using System;

class Program
{
    static void Main()
    {
        try
        {
            // 사용자가 숫자를 입력하게 함
            Console.WriteLine("숫자를 입력하세요:");
            int number = int.Parse(Console.ReadLine()); // 여기서 글자를 입력하면 펑! 터짐
            Console.WriteLine("입력한 숫자는 " + number + "입니다.");
        }
        catch (Exception e)
        {
            // 에러가 발생하면 이쪽으로 점프합니다.
            Console.WriteLine("앗! 뭔가 잘못되었습니다. 숫만 입력해 주세요!");
            Console.WriteLine("에러 내용: " + e.Message);
        }
    }
}

[코드 뜯어보기]

  • int.Parse(Console.ReadLine()): 사용자가 “안녕”이라고 입력하면 C#은 정수로 바꿀 수 없어서 FormatException이라는 예외를 던집니다.
  • catch (Exception e): 여기서 Exception은 모든 예외의 조상님입니다. 어떤 종류의 에러가 나든 다 여기서 잡아냅니다. e.Message를 통해 구체적으로 뭐가 문제인지 확인할 수 있죠.

두 번째: 다중 catch 문 (전문가 맞춤형 수습)

실무에서는 모든 에러를 똑같이 처리하지 않습니다. “돈이 없는 에러”와 “네트워크가 끊긴 에러”는 대처법이 달라야 하니까요.

using System;

class Program
{
    static void Main()
    {
        try
        {
            Console.WriteLine("나눌 숫자를 입력하세요 (분모):");
            int denominator = int.Parse(Console.ReadLine());
            
            int result = 100 / denominator; // 0을 입력하면 DivideByZeroException 발생
            Console.WriteLine("결과: " + result);
        }
        catch (DivideByZeroException e)
        {
            // 0으로 나눴을 때만 실행되는 전용 구급차
            Console.WriteLine("경고: 0으로 나눌 수는 없습니다! 수학 시간에 배우셨잖아요!");
        }
        catch (FormatException e)
        {
            // 숫자가 아닌 문자를 입력했을 때만 실행되는 전용 구급차
            Console.WriteLine("경고: 숫자가 아닌 문자를 입력하셨네요. 숫자만 넣어주세요!");
        }
        catch (Exception e)
        {
            // 위에서 걸러지지 않은 나머지 모든 에러 처리
            Console.WriteLine("알 수 없는 오류가 발생했습니다: " + e.Message);
        }
    }
}

[코드 뜯어보기]

  • catch 문은 위에서부터 순서대로 확인합니다.
  • 만약 0을 입력했다면 DivideByZeroException에서 딱 걸려서 처리되고 종료됩니다.
  • 만약 “abc”를 입력했다면 FormatException에서 처리됩니다.
  • 가장 아래에 있는 Exception e는 “혹시 모르니 다 잡아라”라는 최후의 보루입니다. 반드시 가장 아래에 둬야 합니다.

세 번째: finally를 활용한 완벽한 마무리 (뒷정리형)

파일을 열었거나 데이터베이스 연결을 했다면, 에러가 나든 안 나든 반드시 닫아줘야 합니다. 안 그러면 메모리 누수가 생겨서 컴퓨터가 느려지겠죠? 이때 finally가 등판합니다.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        StreamReader reader = null;
        try
        {
            Console.WriteLine("파일을 읽어옵니다...");
            // 존재하지 않는 파일을 열려고 시도 (에러 유도)
            reader = new StreamReader("non_existent_file.txt");
            Console.WriteLine(reader.ReadToEnd());
        }
        catch (FileNotFoundException e)
        {
            Console.WriteLine("에러: 파일을 찾을 수 없습니다. 경로를 확인하세요!");
        }
        catch (Exception e)
        {
            Console.WriteLine("예기치 못한 오류: " + e.Message);
        }
        finally
        {
            // 에러가 나든 안 나든 무조건 실행되는 구간
            if (reader != null)
            {
                reader.Close();
                Console.WriteLine("파일 리소스를 안전하게 해제했습니다.");
            }
            else
            {
                Console.WriteLine("해제할 리소스가 없습니다.");
            }
            Console.WriteLine("프로그램 처리를 종료합니다.");
        }
    }
}

[코드 뜯어보기]

  • finally 블록은 try 블록이 성공하든, catch 블록으로 튀든 상관없이 무조건 마지막에 실행됩니다.
  • reader.Close(): 파일을 열었다면 닫아주는 것이 예의입니다. 이 과정을 통해 시스템 자원을 낭비하지 않게 됩니다.

초보자 폭풍 질문! 질문: “선생님, 그냥 모든 코드를 try-catch로 감싸버리면 프로그램이 절대 안 터지겠죠? 그렇게 하면 무적 아닌가요?”

재준봇의 답변: “오, 아주 날카로운 질문입니다! 하지만 그건 ‘무적’이 아니라 ‘눈 가리고 아웅’ 하는 거예요. 모든 코드를 감싸버리면 어디서 왜 에러가 났는지 찾기가 너무 힘들어집니다. 에러를 숨기는 게 아니라, ‘예상 가능한 범위’에서 ‘적절하게’ 처리하는 것이 핵심입니다. 과도한 try-catch는 프로그램 성능을 떨어뜨리고 버그를 찾기 어렵게 만드는 ‘독’이 될 수 있으니 주의하세요!”


3. 직접 에러를 던지는 throw와 사용자 정의 예외

지금까지는 시스템이 던지는 에러를 잡는 법을 배웠죠? 그런데 개발자가 직접 “이건 내 기준에서 에러야!”라고 선언하고 싶을 때가 있습니다. 이때 사용하는 것이 throw입니다.

예를 들어, 사용자의 나이를 입력받는데 -5살이라고 입력했다면? 시스템적으로는 정수니까 에러가 아니지만, 상식적으로는 에러죠.

using System;

// 나만의 전용 예외 클래스 만들기
class InvalidAgeException : Exception
{
    public InvalidAgeException(string message) : base(message) { }
}

class Program
{
    static void CheckAge(int age)
    {
        if (age < 0)
        {
            // 내 기준에서 에러니까 직접 던진다!
            throw new InvalidAgeException("나이가 음수일 수는 없습니다. 타임머신 타셨나요?");
        }
        Console.WriteLine("나이 확인 완료: " + age);
    }

    static void Main()
    {
        try
        {
            CheckAge(-5); // 일부러 잘못된 값 전달
        }
        catch (InvalidAgeException e)
        {
            Console.WriteLine("사용자 정의 에러 발생: " + e.Message);
        }
    }
}

[코드 뜯어보기]

  • class InvalidAgeException : Exception: Exception 클래스를 상속받아 나만의 에러 타입을 만든 것입니다.
  • throw new ...: 조건이 맞지 않을 때 강제로 예외를 발생시켜 catch 문으로 보냅니다.

4. 디버깅 기법: 범인은 이 안에 있어! (CSI 코딩편)

코드를 다 짰는데 생각대로 안 돌아간다? 이때 필요한 것이 디버깅입니다. 디버깅은 쉽게 말해 “코드의 시간을 멈추고 내부를 들여다보는 행위”입니다.

필수 디버깅 도구 3가지

  1. 중단점 (Breakpoint):
    • 코드 왼쪽 여백을 클릭하면 빨간 점이 생깁니다. 프로그램 실행 중 여기에 도달하면 프로그램이 일시 정지됩니다. “여기서 멈춰! 이제부터 내가 지켜보겠다”라고 선언하는 것입니다.
  2. 단계별 실행 (Step Over / Step Into):
    • Step Over (F10): 다음 줄로 그냥 넘어갑니다.
    • Step Into (F11): 함수 내부로 깊숙이 들어갑니다. (함수 안에서 무슨 일이 벌어지는지 보고 싶을 때 사용)
  3. 조사식 (Watch) 및 로컬 창:
    • 변수에 현재 어떤 값이 들어있는지 실시간으로 확인하는 창입니다. i가 지금 10인지 11인지 헷갈릴 때, 여기서 확인하면 정답이 딱 보입니다.

실무주의보 주의사항: “디버깅할 때 중단점을 너무 많이 잡으면 실행 흐름을 놓치기 쉽고, 특히 멀티스레드 환경에서는 중단점을 잡는 순간 다른 스레드와의 타이밍이 꼬여서 원래 발생하던 버그가 사라지는 ‘하이젠버그’ 현상이 발생할 수 있습니다.”

대처법: “중단점과 함께 Console.WriteLine이나 Debug.WriteLine을 적절히 섞어서 로그를 남기세요. 로그는 거짓말을 하지 않습니다. 특히 서버 환경에서는 중단점을 잡을 수 없으니 로그 기록 습관을 들이는 것이 매우 중요합니다!”


마무리하며

자, 오늘 우리는 C#의 심화 과정인 예외 처리디버깅에 대해 아주 깊게 파헤쳐 보았습니다.

정리하자면:

  • try는 시도, catch는 수습, finally는 뒷정리!
  • 예외는 구체적인 것부터 일반적인 것 순으로 잡아야 한다!
  • throw를 통해 나만의 규칙(사용자 정의 예외)을 만들 수 있다!
  • 디버깅은 중단점과 단계별 실행을 통해 범인(버그)을 잡는 과정이다!

처음에는 이 내용들이 복잡해 보일 수 있습니다. 하지만 기억하세요. 좋은 개발자는 코드를 한 번에 완벽하게 짜는 사람이 아니라, 터진 코드를 얼마나 빠르고 정확하게 고치느냐로 결정됩니다. 오늘 배운 기술들은 여러분을 ‘그냥 코딩하는 사람’에서 ‘문제를 해결하는 엔지니어’로 만들어 줄 것입니다.

자, 이제 직접 코드를 짜보고 일부러 에러를 내보세요. 그리고 그걸 우아하게 잡아보세요. 그 쾌감이 정말 짜릿하거든요!

지금까지 재준봇이었습니다. 다음 강의에서 더 트렌디하고 강력한 내용으로 돌아오겠습니다! 열공하세요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: