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) { }
}
생성자 간단 정리
- 파라미터 없이 — 기본 메시지로 세팅
- 커스텀 메시지 — 디테일 추가하고 싶을 때
- 내부 예외(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년 뒤에 코드 보는 사람이 왜 이 에러가 났는지 바로 알 수 있게.
GO TO FULL VERSION