
개요에러와 예외의 차이점예외란? 예외처리란?예외예외처리잘못된 예외처리란?예외를 catch 하고 로그만 찍는다.에러가 발생할 수 있는 곳인데 try-catch 문을 작성하지 않는다.예외를 catch만 하고 아무것도 하지 않는다.기약 없는 재귀 호출을 한다.재귀호출은 왜 OOM을 유발할까?올바른 예외처리란?유의미한 처리를 할 수 있는 곳에서 에러를 잡는다.에러가 발생할 수 있는 곳만 try-catch로 묶고, catch에서 유의미한 행위를 한다.복구 로직(재시도) / 횟수제한이 있으며, 재귀가 필요하지 않다면 반복문으로 구성로그만 남길거라면, 필요한 중간 처리 후 다시 에러를 re-throw 한다.try-catch 내부 로직 이해C++ SEH 체인윈도우 OS자바스크립트 V8 엔진결론Next Step (커스텀 에러)활용 예시
개요
- 영양제는 매일 섭취해야 하며, 식품이기에 고객에게 제품에 대한 안정성과 신뢰도를 주는 것이 매우 중요한 프로덕트.
- 콘솔로그만 찍거나, 올바르게 핸들링하지 못할 곳에서 catch 하는 등 챕터 내 예외처리 관련 이해도가 낮아서 역량을 맞춰나가기 위해 진행.
에러와 예외의 차이점
이 둘을 혼용해서 사용하는 경우가 많은 거 같다.
사실 둘의 의미는 조금 다르다.
에러는 단순히 에러 객체일 뿐이고, 에러가 throw 되어야 비로소 예외가 된다.
예외란? 예외처리란?
예외
예외는 우리가 의도한 대로 동작하지 않는 예외적인 상황을 뜻한다.
이 말은 다시말해 “정상적인 흐름이 불가능한 상황”이다.
- 서버로부터 유저 목록을 받아와야 하는데, api 요청에 실패했다.
- 서비스 예약 버튼을 눌렀는데, 이미 누가 예약해서 예약에 실패했다.
- 데이터를 복사하려고 하는데, 메모리 할당에 실패했다.
- 자원을 사용해야하는데, 이미 점유중인 상황이라 당장 이용할 수 없다.
- 메모리칩에 데이터를 써넣었는데, 잘못된 값으로 writing 되었다.
앱, 웹의 경우 대부분 네트워크 요청에서 나는 에러가 대부분
예외처리
예외처리란 “정상적인 흐름이 불가능한 상황”에 대처를 해주는 행위를 말한다.
즉, 예외처리는 정상적으로 다시 흘러갈 수 있도록 만들어주는 행위여야 한다.
잘못된 예외처리란?
일반적으로 정말 많이 실수하는 경우들이다.
- 예외가 발생할 수 있는 곳이지만 try catch 문을 넣지 않는다. 예외에 대한 고려를 아예 하지 않는다.
- try-catch문은 넣었으나, catch에서 복구를 위한 작업이 아니라 단순히 로그만 남긴다.
- 또는 catch로 예외를 잡은 후 아무것도 하지 않는다.
예외를 catch 하고 로그만 찍는다.
정상적인 흐름으로 이어갈 수 있도록 문제를 해결하는 코드가 들어가야 하는데 로그만 찍고 없으므로, 정상 흐름으로 복구될 수 없다.




에러가 발생할 수 있는 곳인데 try-catch 문을 작성하지 않는다.

codePushStatuDidChange
콜백이 동작하면서 UP_TO_DATE
에 해당되어야지만 hasCodePushUpdate=false
가 되면서 코드푸시 업데이트 컴포넌트가 제거된다.하지만
sync
함수에 대한 예외처리가 없어서, sync 함수가 실패할 시 UP_TO_DATE로 갈 수 있는 방법이 없고, 따라서 화면에 갇히게 된다.예외를 catch만 하고 아무것도 하지 않는다.

예외가 발생할 수 있는 부분에서 try-catch 구문을 작성했으나 catch 후 아무것도 하지 않는다.
즉 예외는 잘 발생했으나, 예외를 잡은 후 아무런 동작을 하지 않으므로 정상적인 흐름으로 복구될 수 없다.
위 예시에서는
dynamicLink
값에 따라 화면이 달라진다.-
‘’
가 아니라면QRCode
를 보여준다.
- 초기값인
‘’
이라면로딩 화면
을 보여준다.
문제는 유저는 에러가 발생했을 때 마저도 로딩 컴포넌트를 보게 된다.
에러가 발생했다면 로딩 컴포넌트에서 바뀔 여지가 아예 없는데, 유저는 로딩 중이라고 생각하고 계속 기다리게 된다.
에러를 잡기만 하고 아무것도 안 할 거라면, 차라리 에러를 잡지 않는게 낫다!
- 크래시 모니터링이 되어 있다면 적어도 정보수집이 가능하고 대응 로직을 넣을 수 있다.
- 그런데 catch로 예외를 잡은 후 아무것도 하지 않는다면 문제 발생 원인을 파악하기 어렵고, 에러 로그마저 남기지 않은 경우에는 에러가 발생했는지 조차 인지할 수 없다.
- 유저가 이상하다고 제보하기만을 기다려야 한다.
기약 없는 재귀 호출을 한다.
계속 재시도를 하더라도, 계속 실패하는 경우가 있을 수 있다.
예를 들면 네트워크 연결이 끊긴 상태인데 api 요청을 계속 보내봐야 의미가 없다.
이런 상황에서 반복문도 아니고 재귀문으로 기약없는 재귀 처리를 할 경우
OOM(Out Of Memory)
에러가 발생한다.실패할 수 있는 행위를 재시도하도록 되어있는 예외처리는, 무한정 반복되지 않도록 그 횟수를 정해야 하며, 모든 횟수에 실패했을 경우에 어떻게 처리할 것인지에 대해서도 정의되어 있어야 한다.

재귀호출은 왜 OOM을 유발할까?
스택에 쌓인다는 개념과, 메모리를 계속 점유한다는 것을 이해하고 계신가요?



올바른 예외처리란?
- 예외처리 정의에서 언급한 대로, 정상적인 흐름으로 흘러갈 수 있도록 만들어주는 행위를 해야한다.
- 예외는 유의미한 처리를 할 수 있는 곳에서 catch해서 처리한다.
- 프론트엔드에서 유의미한 에러처리란
- 정상 흐름으로 흘러갈 수 있도록 내부 로직에서 자체적으로 복구 로직을 시도한다.
- 내부 로직 상에서 자체적으로 처리할 수 없는 이슈라면, 사용자에게 이슈 사실을 알린다. (사용자가 행위를 취할 수 있도록)
- 나아가 사용자에게 행동 가이드까지 줄 수 있다면 더 베스트!
유의미한 처리를 할 수 있는 곳에서 에러를 잡는다.
간편인증_인증완료
와, 간편인증_나머지_다건요청
의 경우 에러가 발생하거나, 에러로 간주해야 하는 상황에서 throw Error만 하며, 내부에서 try catch로 감싸고 있지 않다.왜냐하면 인증실패 화면으로 이동시키는 유의미한 행위를 해야하는 책임을 해당 함수들이 갖고 있지 않기 때문에, 유의미한 처리가 가능한 곳에서 에러를 잡을 수 있도록 하기 위해 의도된 행위다.

에러가 발생할 수 있는 곳만 try-catch로 묶고, catch에서 유의미한 행위를 한다.
- 유의미한 행위
- 실패 시 재시도 또는 그 밖의 복구 프로세스를 태워서 정상적으로 흐름이 다시 이어질 수 있도록 한다
- 사용자에게 사실을 알리며 행동 가이드를 준다.


복구 로직(재시도) / 횟수제한이 있으며, 재귀가 필요하지 않다면 반복문으로 구성
재시도를 통해 충분히 해결될 수 있는 것은, 사용자에게 알림을 주기 전 최대한 복구를 위한 노력을 가한다.
다만 실패할 수 있는 것을 재시도 하는 경우 계속 실패하는 경우도 배제할 수 없으므로, 반복 횟수에 제한이 있어야하며, 반복횟수 모두 실패한 경우에 대한 방안도 마련되어야 한다.

로그만 남길거라면, 필요한 중간 처리 후 다시 에러를 re-throw 한다.
“여기선 해결하지 못했어” 라는 의미. 그러면 다음 catch chain으로 이동해서 확인하게 된다.
try{ 에러_유발_가능_로직(); } catch(error){ logging("@@에서 에러발생"); // 중간에 필요하다고 판단되는 행위를 수행해도 된다. throw error; // 다만 해결되지 않은 에러는 다시 예외로 던져야 한다. }
try-catch 내부 로직 이해
C++ SEH 체인
윈도우 OS
TEB
(Thread Environment Block)안에 ExceptionList
링크드리스트가 존재함.
try-catch문이 있다면 함수 진입 시점에 exception_handler를 SEH 최상단에 등록한다.

실행 전에는 SEH 체인이 2개만 있는 상태

함수 진입 후 SEH 체인 등록 과정을 거친다.

SEH Chain을 보면 3개로 늘어있는 것을 확인할 수 있다.

함수의 반환 부근에서 SEH Chain에서 제거하는 모습을 볼 수 있다.

3개 였던 게 다시 2개로 줄어든 것을 볼 수 있다.

예외 처리 과정을 눈으로 확인하고자 일부러 예외를 발생시켰다.

가장 첫번째 SEH handler 주소로 오는 것을 알 수 있다. (가장 최근에 등록한 예외처리 핸들러)
_except_handler()
함수의 리턴 값에 따라 에러 발생지점부터 다시 코드를 실행할 지, try-except 구문 외의 나머지 코드를 수행할지, 다음 SEH로 예외처리를 넘길지가 결정되는데, 이건 생략함.
결론은 예외가 발생했을 때 가장 최근에 등록된 예외처리 핸들러부터 링크드리스트를 순회하며 예외를 처리할 수 있는 핸들러를 찾는다. 모든 핸들러를 돌아도 해결되지 않는 에러라면 “예상치 못한 에러”를 띄우며 프로그램이 크래시 난다.
바깥 범주일수록 더 제너럴한 예외에 대한 행위가 들어가면 좋겠다
안쪽 세부 로직일수록 specific한 내용이 들어가면 좋겠다
- 에러 바운더리가 대표적인 예시
(다만 어떤 에러에 의해, 어떤 에러가 처리되지 않고 에러바운더리까지 오게 됐는 지는 별도로 로깅을 하는 것이 파악하는 데 좋겠죠?)
자바스크립트 V8 엔진
자바스크립트 엔진은 내부적으로 C++로 작성되어 있다.
그렇다고 자바스크립트 처리 시에도 SEH 체인을 직접 사용하나? 그건 아니다.
SEH는 운영 체제의 예외 처리 메커니즘과 밀접한 관련이 있는데, 저수준의 메모리와 하드웨어 사용 중 발생하는 에러를 처리하기에 적합하다.
반면에 자바스크립트는 고수준의 인터프리터 언어로서, SEH와 같은 저수준의 메모리와 하드웨어 접근이 필요하지 않다. 자바스크립트는 가상 머신 환경에서 실행되며, 예외 처리는 런타임 환경과 관련된 내부 메커니즘에 의해 처리된다. 하지만 내부 코드를 살펴보면 처리 방식은 SEH와 거의 유사하다.
추가 팁
- C++은 컴파일 언어이므로 빌드 시점에 해당 함수가 try-catch 구문이 있는지 없는지를 이미 알고 있으므로, 함수 진입과 동시에 세팅을 진행한다.
- 자바스크립트는 인터프리터를 통해 한줄한줄 해석하므로, try-catch 구문을 만났을 때 관련 로직이 수행된다.
마찬가지로 싱글 링크드 리스트로 구성되어 있음.

TryCatch문 생성자 함수
RegisterTryCatchHandler
를 통해 핸들러를 싱글링크드리스트 최상위에 등록한다.
- 링크드리스트이므로 다음 노드를 참조하기 위한
next_
함수가 있다.

TryCatch문 소멸자 함수
UnregisterTryCatchHandler
함수를 호출하여 해당 try-catch문의 에러 핸들링 로직을 링크드리스트에서 제거한다.
정확히 원했던 코드는 아니지만,
싱글링크드리스트를 순회하면서
try_catch_handler_
를 참조하는 모습을 볼 수 있다.
결론
예외가 발생할 것 같은 곳에서 무조건 try-catch를 해야하는 것이 아니다. (더 바깥영역에서 잡을 수도 있으니)
예외가 발생하면 가장 가까운 catch문 부터 돌면서 예외를 처리할 수 있는지 판단하며, 더 큰 범주로 순회하며 검사해 나간다.
따라서 throw Error를 하는 부분과, try-catch문의 구성을 잘 고려하면 효율적인 예외처리가 가능하다.
- 보통 기능별로 묶어서 개발하므로, 기능이라는 범주 안에서 위 내용을 설계해보면 좋다.
- throw Error는 로직 어디에서나 던져도 된다.
- 다만 catch는 예외를 올바르게 핸들링 할 수 있는 곳에서 잡을 수 있도록 설계해야 한다.
- 예외를 잡았다면 반드시 복구를 위한 유의미한 행위를 해야 한다 (로깅만 한다 안됨)
Next Step (커스텀 에러)
class MyError extends Error { constructor(message) { super(message); this.name = "MyError"; // 이거 지정 안하면 그냥 Error로 에러문구 출력됨 (MyError 인스턴스로 식별은 됨) } }
throw MyError("메롱");
try { 뭔가로직() // 에러 발생 시 throw MyError를 하도록 되어 있음 } catch (error) { if (e instanceof MyError){ // Error를 상속했으므로 당연히 instanceof Error에도 잡힌다. // 블라블라 처리 return; } throw error; }
하나의 try-catch문이 너무 많은 에러를 받을 수 있는 상황에서, 각각의 로직별 에러를 구분시키는 용도로 활용할 수 있을까?
활용 예시
message
만 가지고 뭔가 하기 어렵다. 추가 정보를 더 많이 가져야 하는 경우에도 활용할 수 있다.class DispensingError extends Error { constructor(message:string, public cause:any) { super(message); this.name = "DispensingError"; this.cause = cause; } } try{ throw new DispensingError("토출오류", {1:["블라블라"], 2:["블라블라"]}) }catch(e){ if(e instanceof DispensingError){ console.log(e.cause); } }
