CodeGym /행동 /C# SELF /콜 스택과 직접 만드는 예외

콜 스택과 직접 만드는 예외

C# SELF
레벨 13 , 레슨 3
사용 가능

1. 콜 스택(Call Stack)과 친해지기

지난 강의에서 콜 스택을 잠깐 언급했었지? 이제 좀 더 자세히 파헤쳐보자.

상상해봐: 사용자가 앱에서 버튼을 눌러. 이 버튼이 OnClick() ("버튼 눌림") 메서드를 호출하고, 그 메서드는 LoadData() (데이터 불러오기)를, 그리고 그건 또 ReadFromFile() (파일에서 읽기)을 호출해.

갑자기 ReadFromFile()에서 에러가 터졌어 — 파일을 못 찾았대. 누가 잘못한 거지?

이걸 파악하려면, 프로그램이 "발자국 따라 뒤로" 가는 거야: ReadFromFile()LoadData()OnClick() 순서로. 이 경로가 바로 콜 스택이야 — 접시를 쌓아놓은 것처럼, 맨 위에 올린 게 제일 먼저 떨어지는 거지.

프로그램은 이 스택을 아래로 따라가면서, 적당한 catch를 찾을 때까지 내려가. 근데 그 과정에서 경로에 있는 모든 finally도 다 실행해 — 리소스 정리, 닫기, 청소 이런 거 다 하려고.

프로그래밍에서 콜 스택은 누가 누구를 호출했는지 기억하는 리스트야. 그래서 뭔가 꼬였을 때, 이 "요청 경로"를 거꾸로 따라가서 원인을 찾을 수 있지.

어떻게 동작하는지

프로그램이 실행될 때, 메서드(혹은 함수) 하나 호출할 때마다 콜 스택(리스트)에 새로운 "줄"이 추가돼. 실행 중에 예외가 발생하면, .NET의 Exception 클래스가 이 스택 정보를 저장해: 어디서, 어떤 순서로 메서드가 호출됐는지, 그게 에러로 이어졌는지.

2. 콜 스택이 왜 필요한가

콜 스택은 복잡한 에러 디버깅할 때 진짜 든든한 친구야.

  • 단순히 무슨 일이 일어났는지 뿐만 아니라, 어디서 그리고 누가 문제를 일으켰는지까지 보여줘.
  • 가끔 콜 스택을 보면, 프로그램이 어떻게 이런 상태까지 왔는지 신기할 때도 있어 (특히 누가 실수로 이상한 값 넘겼을 때).

흔한 상황: 예를 들어, 프로젝트가 엄청 커서 메서드가 서로 열 번씩 호출하는 구조야. 어느 순간 NullReferenceException이 뜨는데, 도대체 어떻게 여기까지 왔는지 감이 안 잡혀. 콜 스택을 열어보면 호출 체인이 쫙 보여서, 어디서부터 파봐야 할지 훨씬 쉬워져.

예시:

class MyClass 
{
    public void MethodA()     { MethodB(); }
    
    public void MethodB()     { MethodC(); }
    
    public void MethodC()     {
        throw new Exception("에러!");
    }    

    public void Main() 
    {
        try 
        { 
            MethodA(); 
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }        
    }
}

3. 직접 예외 만들기

기본 예외만으론 부족할 때가 있어

.NET에는 기본 예외가 엄청 많아 (ArgumentNullException, InvalidOperationException 등등), 근데 가끔 이걸로는 부족할 때가 있지:

  • 앱에만 적용되는 "룰"이 있을 때: 예를 들어, 사용자가 한 번에 10개 이상 상품을 못 산다거나, 비즈니스 로직상 마이너스 금액이 나오면 안 된다거나.
  • 앱 로직 에러랑 "시스템" 에러를 구분하고 싶을 때.

이럴 때 "내꺼"를 만들고 싶어지지 — 할 수 있으니까!


using System;

// 직접 만든 예외: 사용자 못 찾음
public class UserNotFoundException : Exception
{
    // 기본 생성자
    public UserNotFoundException() : base("사용자를 찾을 수 없습니다.") { }

    // 메시지 커스텀 생성자
    public UserNotFoundException(string message) : base(message) { }

    // 메시지와 내부 예외 포함 생성자
    public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
직접 만든 예외 UserNotFoundException 예시

생성자 간단 정리

  • 파라미터 없이 — 기본 메시지로 세팅
  • 커스텀 메시지 — 디테일 추가하고 싶을 때
  • 내부 예외(inner) 포함 — 에러가 "다른 에러 안에서" 터졌을 때, 중요한 정보 안 잃으려고.

직접 만든 예외 쓰는 법

예를 들어, 우리 할 일 관리 앱에서 사용자 찾기 메서드를 만든다고 해보자:

using System;

public class UserService
{
    public string FindUserNameById(int userId)
    {
        // "사용자 찾기", 못 찾으면 예외 던짐
        if (userId != 42)
            throw new UserNotFoundException($"id가 {userId}인 사용자를 찾을 수 없습니다.");

        return "막심";
    }
}

메인 프로그램에서는:

// Main에서
var service = new UserService();
try
{
    string name = service.FindUserNameById(17);
    Console.WriteLine("사용자 이름: " + name);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine("사용자 찾기 문제: " + ex.Message);
    // 콜 스택도 ex.StackTrace로 볼 수 있음
}

id가 42가 아니면 이렇게 나와:

사용자 찾기 문제: id가 17인 사용자를 찾을 수 없습니다.

4. 직접 예외를 만드는 이유

로깅과 에러 분리

앱에서 여러 종류의 에러가 있을 때, 각각 다르게 처리해야 할 때가 많아. 예를 들어, DB 에러는 "치명적"으로 로깅하고, 사용자 에러는 빨간 글씨로 보여주고, 네트워크 에러는 재시도한다든가. 예외 타입별로 그룹핑하면 이런 게 딱 좋아.

OOP와 상속

도메인에 맞는 에러 계층 구조도 만들 수 있어:

public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }

이제 MyAppException만 잡으면 모든 도메인 에러를 처리할 수 있고, "주문이 너무 많음" 같은 특수한 경우엔 더 구체적으로 잡을 수도 있지.

5. 직접 예외 만들 때 기억할 점

  • 예외를 남발하지 말자
    직접 예외를 만들 때는:
    • 코드가 더 이해하기 쉬워질 때
    • 호출하는 쪽에서 잡을 가능성이 있을 때
    • 호출자에게 더 많은 정보를 주고 싶을 때(필드/프로퍼티로)
  • 좋은 습관: 직렬화
    .NET 기본 예외는 직렬화를 지원해(네트워크로 보낼 때 등). 간단한 앱에선 잘 안 쓰지만, "고급" 케이스에선 [Serializable] 어트리뷰트 달고, 직렬화용 생성자도 만들어야 해(자세한 건 공식 문서 참고). 공부할 땐 안 해도 되지만, 회사에선 팀장한테 물어봐 :)
    직렬화란 객체를 저장이나 전송에 편한 포맷(예: 파일, 네트워크)으로 바꾸는 과정이야. 이건 나중에 더 자세히 배울 거야.

6. 콜 스택의 함정: 헷갈릴 수 있는 부분

콜 스택은 예외가 발생한 지점까지의 경로만 보여줘. 만약 예외를 잡고, "내부" 예외 없이 새 예외를 던지면(Exception(string, Exception inner) 생성자 안 쓰면), 원래 에러의 출처를 잃어버릴 수 있어. 이걸 "스택 숨기기"라고 해.

나쁜 예:

try
{
    // 여기서 에러 발생
}
catch (Exception ex)
{
    throw new Exception("알 수 없는 에러 발생."); // 이전 에러 스택이 사라짐!
}

좋은 예:

try
{
    // 여기서 에러 발생
}
catch (Exception ex)
{
    throw new Exception("알 수 없는 에러 발생.", ex); // 원래 스택도 같이 남김!
}

이렇게 하면 StackTrace에 첫 에러 경로랑 네 메시지 둘 다 남아.

7. 실전 팁 & 자주 하는 실수

  • 일반 로직 제어에 예외를 쓰지 말자 (예: "반복문 탈출" 용으로 — 더 깔끔한 방법 많아!).
  • 딱 필요한 예외 타입만 잡자 — 항상 Exception만 잡으면 안 좋아(놓치면 안 되는 에러까지 "삼켜버릴" 수 있음).
  • 복잡한 에러는 꼭 콜 스택을 로깅하자. 버그 찾을 때 진짜 시간 아낄 수 있어.
  • 내부 예외 활용하자 — 원인 정보 안 잃으려면 필수야.
  • 직접 만든 예외는 설명을 명확하게! — 1년 뒤에 코드 보는 사람이 왜 이 에러가 났는지 바로 알 수 있게.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION