업무_메모

스택 풀기: 예외가 발생한 위치를 제대로 추적하려면

shine94 2025. 4. 1. 16:37

💡 예외 처리와 스택 풀기, 그리고 내가 뒤늦게 이해한 피드백 이야기

예외 처리에 대한 피드백을 받았던 건 꽤 오래전 일이다

컨트롤러에서 try-catch로 예외를 한꺼번에 처리하기보다는 서비스 단에서 예외를 나눠서 처리하는 게 더 낫다는 얘기였다

 

그 때 설명을 들었지만, 왜 굳이 그렇게 해야 하는지 이해가 잘 되지 않았다
하지만 최근에 C++의 스택 풀기(stack unwinding) 개념을 공부하면서 그 피드백의 의미가 처음으로 제대로 와닿았다

 

 

📌 스택 풀기란?

C++에서 throw로 예외가 발생하면, 함수 호출 스택을 따라 올라가며 지역 객체들이 소멸된다
이 과정을 스택 풀기(stack unwinding)라고 한다
문제는, 이 과정이 끝나고 catch 블록에 도달했을 때는 이미 호출 스택의 맥락이 다 사라진 상태라는 점이다

즉, catch에 도달했을 땐 "어디서", "왜" 예외가 발생했는지를 직접적으로 알기 어렵다
그래서 C++에서는 보통 throw할 때 예외 객체에 상황 정보를 담아 던지는 방식을 사용한다

 

 

📎 Java도 마찬가지

이건 C++만의 이야기가 아니다 Java에서도 예외가 발생하면 비슷하게 스택을 타고 올라가면서 예외를 전파한다
그리고 catch 시점에는 호출 스택이 이미 풀려 있기 때문에, 그 위치에서는 자세한 정보가 남아있지 않을 수 있다

 

 

🤯 다시 떠오른 피드백

이 부분을 공부하면서 예전에 일하면서 받았던 피드백이 떠올랐다
당시엔 컨트롤러 단에서 try-catch로 한 번에 예외를 처리하는 게 더 간단해 보였고,
서비스 단에서 굳이 예외를 나눠서 처리해야 할 필요성을 잘 이해하지 못했다

하지만 스택 풀기 개념을 공부하면서 그 이유를 조금씩 알게 됐다


C++에서는 예외가 발생하면 호출 스택을 따라 지역 객체들이 소멸되고,
catch 지점에 도달했을 땐 이미 스택이 다 풀린 상태다
즉, 예외가 발생한 위치의 정보는 사라지고, 어디서 어떤 상황이 벌어졌는지 catch 안에서는 제대로 알기 어렵다
그래서 예외를 던질 때 상황 정보를 함께 담는 게 중요하다는 걸 알게 됐다

 

Java도 비슷한 구조다
예외 발생 시 스택 트레이스가 예외 객체 안에 보존되긴 하지만,
서비스 단에서 의미 없이 그냥 예외를 던져버리면, 컨트롤러에선 그걸 받아도 상황을 명확히 파악하기 어렵다
게다가 처리 방식도 일관되지 않게 되기 쉽다

 

Spring에서는 @ControllerAdvice나 @ExceptionHandler를 활용해서 전역적으로 예외를 처리할 수 있기 때문에,
서비스 단에서는 상황에 맞는 예외를 명확히 던지고, 컨트롤러나 예외 처리기는 그걸 받아서 응답만 만들어주면 된다

 

예외는 발생한 위치에서 최대한 구체적으로 처리하고, 그 위 계층에서는 흐름을 정리하는 식으로 역할을 나누는 것이
결국 더 좋은 구조라는 걸 이번에 다시 느꼈다

 

 

✅ 마무리

예외 처리라는 건 단순히 try-catch로 감싸는 기술적인 문제가 아니었다
예외가 어디서 발생했고, 어떤 맥락에서 생겼는지를 "전달 가능한 형태로 남기는 구조"가 중요하다는 걸 알게 됐다
C++의 스택 풀기를 통해 언어 차원의 메커니즘을 이해했고,
그걸 바탕으로 실무에서 들었던 조언이 왜 의미 있었는지도 비로소 연결됐다
실무 피드백과 이론 공부가 맞닿는 지점에서 얻는 이해는 역시 가장 깊게 남는다