관리 메뉴

HAMA 블로그

예외 처리에 대한 6가지 화두 본문

소프트웨어 사색

예외 처리에 대한 6가지 화두

[하마] 이승현 (wowlsh93@gmail.com) 2016. 11. 25. 12:02

 

예외 처리에 대한 6 가지 화두..

 

 일단 예외에 대한  글을 쓰려고 마음은 먹고 편집기를 연후 리얼타임으로 생각하면서  손가락을 움직거려 본다. 따라서 생동감은 넘치는 글이 될거 같긴한데 오류도 있을 수 있겠고 내 밑천이 그닥 많지 않아서 높은 수준의 글은 되지 못할것이다.  그리고 문법을 말하는 글이 아니며 무엇이 옳고 무엇은 안되~라는 글도 아니다.  이런것도 있고 같이 생각해보자는 글이다. 

 예외는 아시다시피 try ~catch 이다. 예외를 잡고 싶은 부분을 try 로 감싸고 예외를 잡았을 경우 catch 문 안에서 처리해주는 방식이다. 어떤 예외를 잡을지와 어떤식으로 처리 할 지에 대한 고민이 들어가야하는 부분이다.  처리는 catch 안에서 직접 할 수 도 있고 catch 안에서  처리할 책임을 그 함수를 호출한 이전 함수에게 공을 돌릴 수 도 있다. 그때 이전 함수는 그 예외를 처리 할 수도 있고 무시할 수도 있고 다시 그 이전 함수에게 공을 또 다시 돌릴 수도 있다. 어떤 언어는 예외라는게 없기도 하고 어떤 언어는 예외처리를 강제하기도 하며 어떤 언어는 예외를 예외가 발생한 근처에서 처리하길 유도하며 어떤 언어는 예외가 발생한 최대한 먼 지점에서 전지적인 시점에서 처리하게 유도하기도 한다. 또한 '고장나게 납둬라' 라는 전략도 있다. 물론 광고성 멘트이다. 어떻게든 고장난것에 대한 처리는 해야하니깐..

이러한 예외의 매커니즘들을 이 글에서는 6가지로 나누어서 화두를 던져본다.

 

1. 에러인가 예외인가? (처리 할 수 있는 것? 없는 것?) 

이런 말장난을 구분하는 것부터 문제이다. (언어에 따라서 exception키워드가 따로 없는 것도 있다. 그냥 error)  내가 당신에게 1부터 10까지의 숫자중 하나를 말해주세요 할때 100을 말하면 에러이며 갑자기 누군가 당신의 목을 졸라서 아무 말 안하거나 '으악' 이라는 소리를 듣게 되면 예외인가? 좀 더 SW 스러운 예제를 생각해보면 어떤 메소드가 있다고 하자. 파일을 열어서 그 안의 IPaddress: 이후의 값을 리턴하는 계약이 맺어진 메소드라고 할때 값이 192.168  ~ 이런게 아니라 "길라임" 이라면 이게 에러인가 예외인가?  혹은 파일을 누가 지워서 아예 아무것도 못하면 이게 에러인가? 예외인가?  그 메소드를 호출하는 호출자와 그 메소드의 계약관계에  '파일이 거기 있을 수도 있고 없을 수도 있다' 는 것이 포함되어 있다고 할 수 있을까 없을까? 

나는 전자 즉 길라임이 들어 있으면 에러라고 보고 , 후자라면 즉 파일이 없으면 예외라고 본다.  이 함수에서 나타날 수 있는 모든 예상 가능한것들은 예외라고 보며 그렇지 않은 경우 즉 '길라임' 일지 '순시리' 일지  알 수 없는 것 일때 에러라고 말하기도 한다. (물론 '길라임' 이 나오는 것도 예상가능하에 있다고 님은 본다면 이것도 예외이다.)  

더보기

예외 처리는 메커니즘이 정상 반환 값과 잘못된 반환 값을 구별한다는 점에서 반추상적 문제를 해결한다. C와 같은 내장 예외 처리가 없는 언어에서 루틴은 공통 반환 코드 및 오류 패턴과 같은 다른 방식으로 오류를 알려야 한다.[5] 넓은 관점에서, 오류는 예외의 적절한 부분 집합으로 간주될 수 있으며, 오류와 같은 명시적 오류 메커니즘은 예외 처리의 (상세한) 형태로 간주될 수 있다.[5] "예외"라는 용어는 어떤 것이 잘못되었다는 것을 의미하지 않기 때문에 "오류"보다 선호된다 - 한 프로시저나 프로그래머에 의해 오류로 간주되는 조건은 다른 프로시저에 의해 그렇게 보이지 않을 수 있다. "예외"라는 용어조차도 오해의 소지가 있을 수 있는데, 그 이유는 "특이"라는 전형적인 의미의 "특이"는 드문 일이나 특이한 일이 일어났다는 것을 의미하기 때문이다.[7] 예를 들어 키에 연결된 값이 없는 경우 연관 배열에 대한 조회 함수가 예외를 발생시킨다고 가정할때 컨텍스트에 따라 이 "키 없음" 예외가 성공적인 조회보다 훨씬 더 자주 발생할 수 있다. [8]  from 위키

 

그냥 자바언어에서는 에러를 무엇으로 보고 있는지 확인해보자.

자바에서는 Exception와 Error는 구분되는데 차이는 처리 할 수 있는 문제 인가? 없는 문제인가이다. 뭐 굉장히 명쾌한듯 보이지만 
처리 할 수 없는게 도대체 무엇인걸까? 자바에서 에러는 주로 시스템 리소스 부족으로 인해 발생하는 문제이다. 그것은 문제를 알아 바짜 어떻게 개발자가 할 수 없다고 보는 것인데, 실행 시간에 발생한다.  에러의 예로는 OutOfMemoryError, LinkingError, AssertionError 등이 있다.

비교점   Error Exception
패키지명 Java.lang.error Java.lang.exception
회복가능 회복될 수 없다. (돌이킬수 없다)  회복 될 여지가 있다. 
발생상황 실행시간에 발생된다. 실행시간과 컴파일 시간 모두 발생된다.
클래스명  OutOfMemoryError, IOError. NullPointerException, SqlException

 

2. 무조건 처리하고 넘어가야?  가능하면 처리 하고 넘어가야?  (Checked vs UnChecked) 

자바 창조자가 생각하는 반드시 처리해야하는 것은 무엇인고? 가능하면 처리 해야 한다고 생각하는 것은 무엇일까?

 

즉 강제적으로 처리 해야 하는것은 무엇인가에 관한 문제이다.

이 문제는 예외 처리의 아주 전통적인 시각차가 존재하는 화두이다. 가장 많이 사용되는 언어인 자바와 C# 의 창시자들도 서로 다른 시각을 가지고 있는데  C# 의 창시자 앤더스 하일스버그의 경우 자바의 체크드 예외 (강제적으로 호출한 함수에서  처리하게 만드는) 는 여러모로 문제점을 가지고 있는데 확장성이 그 중 하나로 본다. 즉 라이브러리를 사용하는 개발자의 코드가 라이브러리의 체크드 예외의 변화에 따라서 에러가 생길 여지가 있다는 것이다. 라이브러리에서 B 라는 예외를 추가했는데 사용자 코드에는 그것이 없으니 말이다. 이런 문제는 상속에서 생기는 것과 비슷한데  상위 클래스에 추상메소드를 함부러 넣는게 힘든 이유와 같다. (자바 같은 경우는 가상함수의 상속구현에 대한 강제적인 키워드가 없어서 뒤통수 맞을  소지가 많다.)  하일스버그는 또한 그런 강제적인 예외처리는 개발자에게 예외에 대한 피로감을 주게되어 그냥 모든 예외를 하나의 catch문에 때려박고 로깅한번하고 잊어버리자. 라는 유혹에 빠지게 한다고 말한다.개인적으로 자바의 체크드예외 시스템은 불필요 하다고 본다. Unchecked 예외들도 반드시 처리 해야할 예외라고 생각 할 수 있기 때문이다. 차라리 모두 checked 예외인게 더 나아보이기도 한다.  체크/언체크드로 구분되어 있는 자바에서는 웬지 언체크드는 신경쓰지 말아라라는 잘못된 시각으로 비추어 질 수도 있겠고..

참고)

자바에서의 체크드 예외에 대한 불편함과 불필요성을 느낀 코틀린 언어에서는 모든게 언체크드 예외이다. 만약 실행중인 함수에서 그냥 예외를 던저버리면, 그 함수를 사용하는 측에서는 날벼락 맞기 싶상이다. 소스 내부를 일일이 확인하지 않고서는 말이다. 그래서 함수를 사용하는 측에서 문제상황을 강제로 처리하게 하기 위해 (강제로 한다는건 실수를 줄이는 기법이기도 하다. 체크드 예외를 만든 철학처럼) 최근 언어에서 이름은 달리하여 대부분 지원하는 Result를 이용하여 리턴해주면 호출자측에서는 
onSuccess / onFailure, getOrNull, exceptionOrNull, getOrDefault, getOrElse 등을 통해서 요리 하면 된다. link to documentation.

 

3. 가까운 곳에서 즉시 처리할 것인가? 상위로 위임할 것인가?

2번에서 연장되는 얘기인데, A 라는 메소드에서 예외가 발생했다고 하자.  그 예외에 대해 그 스스로 처리할 것인가? 혹은 그 메소드를 호출한 근접메소드가 처리해야하나? 혹은 콜스택의 가장 상위에서 처리해야하나? 예외가 일어난 곳에서 가장 가까운 놈이 처리해야하는게 순리에 맞나? 가장 멀리 즉 전지적 시점에서 처리해야하는게 순리에 맞는것일까?  

예를들어 국내 원자력 발전을 통제하는 프로그램이 있다고 하자. A 발전소의  내일  발전량을  10->100 으로 바꾸는 기능이 있는데 100으로 바꾼후 Apply 버튼을 눌렀다고 했을때   a->b->c->d->d 라는 일들이 일어 난다고 하자.  근데 c 에서 무슨 문제가 생겼다.  c 에서만  처리해야하면 되나? a 를 호출하는 시점으로 거슬러 올라가서 전체적으로 관리해야하나?

문법적으로 보면 모든 개별 하위에서 catch 를 처리 할것인가 아니면 하위는 모두 finally 로 리소스에 대한 종결만 짓고 예외를 상위로 전파해서  상위에서만 각각의 예외에 대해서 구분 처리 하자에 대한 것이다. 각각 모든 메소드들에서 먼가를 처리 한다는것은 너무 복잡해진다는것을 의미하며 현실적으로 불가능하며 개발자에게 피로함만 가중시킨다고 보여 진다. 하지만 예외는 발생된 곳에서 처리하는게 분명하지 않겠나? 하는 입장도 맞을 수 있다. 혼자 개발하는게 아니라면 누가 상위를 개발하며 하위에서 일어난 혹은 일어날 일을 콘트롤 가능할까?

결국 프로그램의 특성에 따라서 달라 질 수 있는데 웹백엔드 개발에서 상당수는 상위로 이전하는게 나아 보인다.
그렇게 되면 서비스코드들은 깔끔해지며, 컨트롤러에서 AOP로 모두 묶어서 처리 할 수 있다. 

 

4. 죽일 것 인가? 살릴 것 인가? 

최근 개발에서 나는 예외에 대해 고민하길 포기해버렸다.  따라서 예외를 단순히 프로그램이 죽지 않게 하기 위해 예외처리를 한다. 잘죽는 부분은 이렇다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

뭐 더 많겠지만 요즘 내가 짜는 프로그램은 이런거 같다. 이렇게 써 놓고 보니 예외를 예외로 생각하지 않고 프로그램 시퀀스의 일부분으로 좀 더 오버해서 즉 if 문 처럼 생각하고 쓰는게 아닌가 싶기도 한데 좀 더 고찰해 볼 시간은 없다. (리턴 값을 if 문으로 거르는것이 나쁜건 아니다. go언어는 이렇게 한다) 

암튼 위의 리스트에서 느껴지듯이 쓰레드,IO 등에서 문제가 생길 소지가 많은데 그래서 웹개발/서버개발에 있어서는 죽이지 않기 위해 예외처리를 하는 경우가 많은거 같다. 일처리 프로세스 과정이 복잡하지 않고 하나의 시퀀스에 대해 완료하지 못하더라도 큰 타격을 받지 않으며 어차피  다음에 시도하면 되는 것은 그냥 생활의 일부분이야 ~~그런 사소한 이유로 프로그램을 죽일 순 없어. 프로그램 죽는것은 내가 죽는것!!!  이런거다.

근데 그냥 빨리 죽여서 문제를 빨리 찾으려는 응용프로그램도 많으며 예러 발생해선 절대 안되는 프로그램이 있으며 , 예외도 거의 없어야 하는 프로그램도 있다. 공장이나 의료쪽에서 사용되는 응용프로그램들이 그러한데 이런 프로그램에서는 저 위의 예처럼 모든 경우에 대해 예외처리를 하여 자신도 모르게 넘어가면 안된다. 예외가 로그에 남겠지만 그것만으로 부족하다. 개발자에게  문제가 생겼다는것을 즉시 각인 시켜야한다. 프로그램을 그 즉시 죽임으로써 말이다. (개발시에는 즉시 죽게, 실제 상황에서는 상황에 따라서..)  그리고 최대한 에러/예외가 나지 발생하지  않도록 설계를 해야하며 오류를 잡아야한다. 사실 이런 프로그램 경우 웹이나 게임처럼 외부와의 잦은 커뮤니케이션이 없고 안정된 하드웨어 등 시스템  때문에  예외가 발생할 가능성은 크게 적다.  

 

5. 모두 되돌릴 것인가? 이미 벌어진 일은 무시,포기할 것인가? 

 그냥 살리는게 중요 포인트라면 예외 발생시 그냥 로그정도 출력하고  지나치면 된다. 그 메소드를 호출한 사람은 아마 한번 더 호출해 보겠지. 아니면 그 사람은 망하더라도 다른 모든 사람들에게는 서비스가 진행 될 수도 있고.. 하지만 되돌려야한다는 문제의식을 갖게되는 어플리케이션이라면 정말 거대한 도전이 된다. 트랜잭션,Redo,Undo,커맨드패턴 같은 단어들을  떠올려야 하기때문이다.

자 갤럭시 7 을 생산하는 공장에서 갤럭시 7 에 들어가는 전자기판(pcb)을 검사하는 프로그램이 있다고 해보자.  그 프로그램은 기판을 검사하기 위해 많은 지식이 사용자에 의해 학습되어야 한다. 그 기판에는 어떤 부품들이 있고 어떻게 생겼고 어떤 방식으로 불량이 아닌지를 확인하는등.. 이것을 학습된 검사조건들 이라고 하자.

이 검사조건 중 하나의 값을 30-> 50 으로 올릴때  발생하는  시나리오가 아래와 같이  흘러 간다고 하자.

a. 50을 입력받아서 
b. 50을 포함한 새로운 임시 객체를 만들어서 적용해 나간다.
c. 변경 된 데이터를 많은 뷰에 적용시키고 (즉 화면뷰에도 50으로 바뀌고 , 리스트뷰에도 바뀌고) 
d. 네트워킹을 통해 중앙저장소에도 바꾸어주고 
e. 검사조건을 모아놓은 메모리 자료구조를 수정한다. 
f. 로컬 xml 파일에 새로운 50을 써준다.
g.완료 (버튼 활성화)

이렇게 메소드들이 호출된다고 할때 갑자기  d에서 문제가 생기면 어떻게 될까?  메모리에는 이 어플리케이션이 갖는 최종적인 마지막 값이 50으로 갱신되지 못한 채 예외가 생겨서 30으로 남겨져 있다. 그리고 검사조건들을 저장해놓은 파일도 갱신되지 못하여 나중에 프로그램을 시작할때 30으로 읽어들일 것이다. 하지만 화면뷰와 중앙저장소의 값은 50인데??

먼가 문제가 생겼을때 돌이키는 방법으로는 메멘토패턴과 커맨드패턴을 주로 사용하는데 커맨드 패턴인 경우 undo / redo 를 처리 할때 execute 와 unexecute 함수를 가지고 내부에 이전정보,이후정보를 속성으로 가진 커맨드 객체를 이용한다. 근데 이때 unexecute 에서 처리해 줘야할 것들이 간단치 않다는게 문제이다.  저 전체를 한 묶음으로 묶어서 처리하는 트랜잭션 처리시  데이타베이스처럼 상대적이고 일관되고 한정된 로직에 있는 것도 아니고 메소드의 역할에 따라 다른 행동에 대해 어떻게 트랜잭션 처리를 일관되게 할 수 있을까?  맨붕이다..

 

6.  리턴으로 처리 또는 예외  도입 ?  좀 더  명시적인(강제적인) Optional , Try, Result 도입? 

지금까지 예외가 모든 언어의 필수발가결한 요소인듯 글이 작성 되었지만 사실 예외 시스템이 없는 언어도 있으며 대표적으로 C, 예전 파스칼언어, 현재 go 언어이다. 우리는 보통 즉 A 에서 B 라는 메소드(JDBC 를 이용하여 DB에서 데이터를 쿼리해서 결과를 돌려주는) 를 호출 했을때 , SQL예외가 당연히 일어날 것을 예상해서 컴파일하기전에 SQLException 에 대처하는 코드를 추가 해준다. 근데 이미 코딩시점에 db 관련 문제가 생길 수도 있음을 인지하고 있다면 , 그 문제가 생겼을때  return "disconnection fail"  혹은 return -1 이라고 리턴해주는 함수로 만들거나 리턴값 리스트를 조사해보면 되지 굳이 예외라는 키워드등을 배울 필요가 없지 않을까? 사용자가 필요로 하는 데이터는 모두 파라미터 (inout) 을이용해서 처리하고 리턴값은 단지 성공했는지 실패했는지 혹은 어떤식으로 실패했는지를 표시해서 처리하면 되지 않느냐는 말이다. 

 즉 예외 이전의 처리 방식은   (The C++ Programming Language 참고) 

* 프로그램을 종료하거나
* '에러' 를 나타내는 값을 반환하거나
* 정상적인 값을 반환하고 비정상적인 상태로 프로그램을 끝내거나
* '에러' 의 경우에 호출되도록 만들어 둔 함수를 호출한다.

이었는데 이거면 충분하지 않을까 하는 문제이다. 사실 자신이 하는 프로젝트에 따라서 충분할 수도 있다.세상의 모든것이 케이스바이케이스이지 않겠는가. 

예외처리는 저런 정해진 규칙이 모호한 개념보다는 좀 더 명확한 개념을 개발자에게 제공하므로써 좋은 코딩 스타일, 잘 구조화된 훌륭한 제품을 만들도록 유도하는데 도움을 준다. (스택풀기 같은 기술적인 부분은 중요치 않다) 자바의 인터페이스 처럼 말이다. C++ 은 그 비슷한것을 할 수 있긴 하지만 추상클래스를 사용해야한다. 추상클래스를 사용하다보면 상속과 인터페이스를 혼동하기 쉽다. 인터페이스는 상속과 전혀 상관이 없는 주제이며 일종의 프로토콜인데 말이다. (Swift 언어에서는 아예 그런 역할을 하는 키워드가 프로토콜. 스칼라는 trait 이다) 

 
 

마무리 

이렇게 예외는 try catch 문법을 공부하는것으로 그치지 않는 아주 어렵고 다양한 스토리가 있다. 기회가 될 때 전체 시스템에 대한 예외 처리 전략에 대해 종이에 스케치해보고  작성도 해본다면 단단한 프로그램을 만드는데 도움이 될 것이다. 물론 그것을 알아주는 사수와 갑을 만나길 바란다. 

개인적으로 try ~ catch 는 별로 안좋아하며, Result나 Try같은 오류내용을 포함한 명시적인 리턴을 우선적으로 고려한다. 

 

Comments