예외처리가이드

10,334 views

Published on

java에서 예외를 처리하기 위한 가이드
요약하면
- 잡아서는 먹지 말자.
- 던질 때는 메시지 충실히
- 로그는 정해진 곳에서만

예외처리가이드

  1. 1. About Me 임도형 딴거 재미없어 오직 개발만 하는 삽질 무쟈게 싫어하는. dh-rim@hanmail.net
  2. 2. 예외? . . .
  3. 3. 모든 일에는 예외가 있다. 모든 시스템에는 예측하지 못하는 상황 이 있다. 작업의 절차를 정의하는 프로그래밍에도 예외가 있을 수 밖에 없다.
  4. 4. 프로그래밍에서의 예외 입력, 출력 그리고 제 3의 당당한 IO . 예외가 없이는 출력의 특정 값을 사용. if(read()==-1) 정보가 빈약하다. 입력 출력 예외
  5. 5. 예외의 종류? 예측 가능한 예외 예측 가능하지 않은 예외
  6. 6. 예측 가능한 예외 예측 가능한 만큼 그 처리 자체가 개발 의 일부이다. 그런 상황을 처리하는 것이 당연하다. 로그인이 실패했다. DB에 레코드가 없다. 파일을 찾을 수 없다.
  7. 7. 예측 가능하지 않은 예외 버그 아니면 시스템 환경에 기인한다. 실시간 처리는 불가능하다. 대신 개선해 야 한다.
  8. 8. 예외 처리가 제대로 되지 않으면? 문제가 발생해도 로그를 보지 않는다. 로그가 중복되거나 누락. 로그에 도움되는 내용이 없다. println(), 혹은 break point에 의지한 디 버깅.
  9. 9. 시스템 개선 한번 발생한 예측하지 못하는 상황이 다 시 발생하지 않도록 하는 것. 발생하지 않도록 한다. 발생한다 해도 예측 가능하게 한다. 오로지 로그에 의존한다. 로그는 예외에 의존한다.
  10. 10. 잡을 때의 가이드. 먹지 말자. 정보를 누락하지 말자.
  11. 11. GUIDE : 예외를 잡았으면 처리하라.
  12. 12. 게으르지 말자. 다음과 같은 코드는 절대 있어선 안 된다. 예외 잡아서 걍 콘솔로 출력하면 될 경우는 거의 없다. 컴파일 된다고 코딩이 끝난 게 아니다. 어찌 처리할 지 모른다면 잡지 말자. try { … } catch(SomeException e) { // TODO Auto-generated catch block e.printStackTrace(); }
  13. 13. Anti Patterns } catch(SomeException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 잡았으면 무언가 해야 한다.
  14. 14. 잡은 후의 유형 처리하고 예외상황 종료 절대적으로 로그를 남겨야 한다. 다시 던진다. 로그를 위해 잡아서 메시지만을 보고 다시 던진다. 메시지를 변경하는 것은 바람직하지 않다. 새 예외로 던진다. 세세한 예외를 더 추상화된 예외로 던질 경우 이 예외를 받은 쪽에서는 cause 예외만으로는 정보가 부족하 다. 추가적인 상황정보 추가는 필수적이다. 무시한다. 뭘 더 해볼 것이 없는 경우. 이 경우 로그조차 필요 없는 경우이다.
  15. 15. GUIDE : 예외를 처리했으 면 로그를 남겨라.
  16. 16. 로그 유형 로그도 없고 처리도 안 함. -> XXX 최악이다. 차라리 안 잡는 것이 낳다. 뭔가 잘못됐다고 리포팅은 되지만, 재현할 방법이 없다. 처리는 했는데 로그가 없다. -> XX 아주 나쁨 예외가 발생한 상황을 먹어버렸다. 시스템 개선할 여지가 없다.
  17. 17. 로그 유형 처리도 하고 로그도 있다. : 다양한 경우가 있다. 한심한 로그(“exception occurred”) -> XX  있으나 마나 한 로그 무심한 로그(“IOException occurred”) -> X  대충 감은 잡힌다. “2번 째 데이터를 못 받았는데, IOException 발생했다면…”  언제까지 감으로 일한 텐가 당연한 로그 -> X 좋은 게 아니다.  예 : “IOException occurred. message=“socket read failed””  메시지 남기는 것은 기본이다. 친절한 로그 -> O 상황을 알려주어야 한다.  예 : “second data reading failed. cause=[IOException, message=“socket read failed.”]”  어떤 상황인지 알려준다. 충분한 로그 -> OK  예 : “second data reading failed. cause=[IOException, message=“socket read failed.”], peer=“10.10.10.13”, port=1234, auth=“base123”, thread=32
  18. 18. Anti Patterns } catch(SomeException e) { // do nothing } // 차라리 잡지 말아야 한다. } catch(IOException e) { doOtherInstead(); } // 정상처리가 되었지만, 그 상황에 대한 로그는 없다. } catch(IOException e) { doOtherInstead(); Log.warn(“exception occurred.”); } // 예외가 발생했다는 것 외에는 정보가 없다.
  19. 19. Anti Patterns } catch(IOException e) { doOtherInstead(); Log.warn(“IOException occurred.”); } // IOException이란 것만 알지 기타 정보가 없다. } catch(IOException e) { doOtherInstead(); Log.warn(e.toString()); } // 예외 클래스와 예외 메시지는 알지만, 상황을 모른다. // 더욱이 예외의 stack 정보조차 누락되었다.
  20. 20. GUIDE : 원 예외의 정보 누락 금지
  21. 21. 정보 누락 금지 예외의 목적이 무엇? : 시스템 개선 절대, 잡은 예외의 정보를 누락하지 말자. 반드 시 cause로 설정하라. 많은 것을 요구하는 것이 아니다. 친절한 메시 지까지는 몰라도 최소한 잡은 예외를 누락하지 말자.
  22. 22. Anti Patterns catch(SomeEception e) { throw new OtherException(“blar”); } catch(SomeEception e) { throw new OtherException(“blar. e=“+e); }
  23. 23. GUIDE : java.lang의 추상 예외로 잡지 말자.
  24. 24. 추상 예외로 잡는다면? 코딩하기는 편하다고 착각할 수도 있다. try – catch 블럭에 어떤 코드를 넣어도 컴파일 된다. 이렇게 잡은 예외의 처리 방법이 오직 한가지라고 확신 하지 못한다면(대부분의 그렇다) 언젠가 세세한 예외로 나누게 될 것이다. 편하게 아니다. try { … } catch(Throwable e){ … }
  25. 25. Anti Patterns public void doSomething() throw SomeException { try { doIt(); doOther(); doMore(); doAgain(); } catch(Exception e) { . . . } }
  26. 26. Anti Patterns try { doIt(); } catch(Exception e) { if(e instanceof SomeExceptoin) { . . . } else if(e instanceof OtherExceptoin) { . . . } else if(e instanceof AnotherExceptoin) { . . . } } // 예외 마다 처리할 방법이 다르다면 편한게 아니다.
  27. 27. GUIDE : 외부와의 interface에서는 RuntimeException도 잡 아라.
  28. 28. 예외 클래스 계층 Throwable Error Exception RuntimeException NullPointerException IllegalArgumentException 일반 예외 클래스 발생했다면 이미 처리할 수 있는 상황이 아니다. 따로 catch하지 않 아도 컴파일 된다.
  29. 29. RuntimeException? RuntimeException은 따로 catch하지 않아도 컴파일된다. 그래서 별로 관심을 두지 않는다. 그런데 RuntimeException이 발생했다는 것 자체는 버그 가 있다는 것을 의미한다. 예 : NullPointerException. 요놈이 발생했다는 것은 null 체크를 제 대로 하지 않은 경우이다. 버그이다. 버그는 발생할 수도 있다. 잡으면 그만이다. 하지만 로그 가 없으면 잡기 힘들다. 외부 인터페이스에서 RuntimeException을 잡아서 로그에 남기자.  여기서 말하는 인터페이스는 외부에서 호출하는 경계를 의미한다. 외부 시스템 시스템, 컴퍼넌트, 패키지
  30. 30. GUIDE : 하나의 try 블록 은 적당한 크기를 유지 하라.
  31. 31. try – catch 블럭이 너무 크다면 catch한 예외를 어디서 던졌는지 눈으로 안 보인다. 혹시 2군데에서 같은 예외를 던진 경우 처리 방법이 틀릴 수도 있다. 유지보수 혹은 코드 파악이 어려워 진다. 하나의 try – catch 블록이 30 line을 넘어가지 않게 하 라. 만약 로직상 어쩔 수 없다면? -> 아마도 중첩된 loop가 있는 경우 일 것이다. 예외와 별개로, 새로운 메소드로 뽑는 리팩터 링을 고려해 보라.
  32. 32. Anti Patterns public void doSomething() throw SomeException { try { doIt(); doOther(); doMore(); doAgain(); } catch(SomeException e) { . . . } catch(OtherException e) { . . . } catch(AnotherException e) { . . . } catch(MoreException e) { . . . } }
  33. 33. 던질 때의 가이드. 메시지 충실. 잡을 곳을 기준으로 예외 객 체 선택. 메시지를 파싱하게 하지 말 자. 추상예외로 던지지 말자.
  34. 34. GUIDE : 예외를 던질 때 는 반드시 message를 설정한다. 충실하게
  35. 35. 메시지를 충실하게. 예외를 왜 던질까? 바라던 행위를 스스로 할 수 없기 때문이다. 밖의 누군가가 처리하라고 던진다. 던진 예외를 잡아서 처리할 수 있을 만 큼 충분한 정보를 제공해야 한다. 정보가 충분하지 않으면 예외를 잡아도 처 리할 수 없다.
  36. 36. 메시지를 꼭 왜? 메시지가 자세하지 않아도 처리할 수는 있다. 사용자에게 알려주기. 기본값 사용. 재시도. 그러나 적지 않은 경우 분석할 필요가 있다. 이 때 로그에 가장 좋은 내용은 예외의 메시지 이다 . 정말로 필요가 없을 수도 있다. 그러나 습관을 위해서라도 설정하라. 대부분인 필요한 경우를 위하여
  37. 37. 메시지를 사칭한 메시지 예 예외 자체만으로도 다음과 같은 정보를 얻을 수 있다. Exception 클래스 이름 발생한 클래스 이름과 메소드 이름 호출한 stack 다음과 같이 던져진 예외는 무슨 정보를 제공하나? runtime이 아닌경우가 있을까? 컴파일 타임을 말하나? 예외라고 말할 것 없다. 이미 예외인 것 안다. occurred라고? 이미 발생한 것 알고 있다. 더군다나 예외 클래스에서도 정보를 얻지 못한다. if(…) { throw new Exception(“runtime exception occurred”); }
  38. 38. 메시지에 따른 유지보수의 차이 메시지가 부실하면 뭐가 안 된다는 보고를 받고 코드 들여다 보면서 상황을 짐작만 하고 그런 짐작을 확인하기 위한 로그 코드를 추가하고 시스템에 배치하고 혹시 짐작이 맞으면 버그 패치하고 시스템에 배치하고 버그 패치가 확인되면 로그 남기는 코딩을 삭제 하여 다시 올리고 짐작 틀리면 계속 반복하고 메시지가 충실하다면. 그리고 로그 남김이 충실하다면 로그 파일로 상황을 파악하고 재현하고 버그 패치하고 시스템에 배치하고 버그 패치만 확인하고.
  39. 39. 충실한 메시지 그 예외만으로도 상황을 파악할 수 있게 하는 메시지 시스템을 개선할 수 있을 만큼 충분한 정보를 제공해야 한다. 단지 뭐가 실패했다는 정도로는 부족하 다. 상황이 포함되어야 한다.
  40. 40. Anti Patterns if(name==null) { throw new IOException(); } // IOException 이름 말고는 정보가 없다. } catch(IOException e) { throw new OtherException(e); } // 상황에 대한 정보가 없다. } catch(IOException e) { throw new OtherException(“runtime exception occurred”); } // 무의미한 메시지, cause 누락, 그리고 상황에 대한 정보가 없다.
  41. 41. GUIDE : 메시지 템플릿 “some tasking failed. name=tom, age=10”
  42. 42. 메시지 템플릿. “TASK_NAME failed.” + (name=value)쌍 반복 • TASK_NAME을 결정할 때는 포함된 메소드 이름 활용 강추. 살짝 읽기 쉽게 풀어 주자. • 만약 메소드 이름이 process13과 같다면 메소드 이름 리팩 토링도 고려해 보자. • 어차피 TASK_NAME은 stack trace를 통해서 알 수 있지만, 고민하지 말고 메소드 이름을 보고 현재 하던 것을 적어주 자. 습관처럼 작성하자. • 예 :“config loading failed.”, “authentication failed”, “message sending failed.”
  43. 43. GUIDE : 외부에서 예외를 catch하여 처리할 상황 을 기준으로 던질 예외 클래스를 선택하라.
  44. 44. 그대로 던질까 하나로 던질까 특정 로직을 수행 중에 다양한 예외가 발생할 경우 어떤 예외를 던져야 하나? 외부에서 어떻게 처리할 지가 기준이다. 각 예외 별로 처리방법이 틀릴 경우 각 예외 그대로 던져야 한다. 예외가 뭐였든 간에 처리방법이 동일하다면 하나의 예외로 던지면 된다.
  45. 45. 그대로 던질까 하나로 던질까 • 예 : xml config파일을 로딩하는 loadConfig() 메소드 – 다양한 예외(file io, xml parsing, config parsing)가 발생한다. – 외부에서는 loadConfig()를 호출 시 단지 로딩 성공이냐 혹은 실패냐 만이 관심이다. – 다양한 예외가 던져진다 하더라도 처리 방법은 동일하다. 로그 를 남기고 시스템을 종료. • 예 : 레코드 insert 시의 실패 – key가 중복되는 경우의 처리와 DBMS와의 통신 실패 시의 처 리 방법이 다르다. – 같은 예외로 던진다면 메시지를 파싱해야 한다.
  46. 46. Anti Patterns try { doSomething(); } catch(AException e) { treatIt(); } catch(OtherException e) { treatIt(); } catch(TheOtherException e) { treatIt(); } catch(AnotherException e) { treatIt(); } // 예외의 종류에 관계없이 처리할 방법이 // 똑같은 경우가 된다면 던지자.
  47. 47. Anti Patterns try { doOther(); } catch(SomeException e) { String message = e.getMessage(); if(message … ) { treatAsThis(); } else if(message … ) { treatAsThat(); } else if(message … ) { treatAsIt(); } } // 처리할 방법이 다양하다면 복수의 예외를 던져야 한다. // 그렇지 않을 경우 메시지를 파싱하게 된다.
  48. 48. GUIDE : java.lang의 추상 적인 예외로 던지지 말 자.
  49. 49. 추상적인 예외? Error, Exception, Throwable, RuntimeException과 같 은 추상적인 클래스로 예외를 던지지 말자. 외부 라이브러리를 가져와서 그 모양새를 봤더니. 타 예외의 경우를 먹은 경우다. 상황에 따른 처리를 하려면 별도의 처리가 불가피하다. 예외 클래스는 그 이름만으로도 정보를 제공한다. 그런 데 그 정보제공자체를 포기하는 것이다. public void someMethod() throws Exception;
  50. 50. Anti Patterns public void doSomething() throws Exception;
  51. 51. GUIDE : 메시지를 파싱하 게 하지 말자.
  52. 52. 메시지 파싱? 메시지는 예외를 처리할 수 있을 만큼 상세하여야 한 다. 그러나 String 메시지를 파싱하여 정보를 추출하게 하 지 말자. 메시지 파싱이 필요한 경우? : 메시지의 내용에 따라 처리할 로직이 달라야 하는 경우 만약 이러한 경우가 발생한다면 예외 클래스에 이러한 정보를 담을 속성을 추가하라. (ex : code) 혹은 별개의 예외로 던져라.
  53. 53. Anti Patterns if(isRecordExist(record)) { throw new InsertFailException(“duplicated id”); } try { insertRecored(record); } catch(SQLException e) { throw new InsertFailException(“sql failed.”); } // 호출하는 쪽에서는 메시지를 파싱해서 처리할 수 밖에 없다.
  54. 54. GUIDE : UI 개발이 아니 라면 message는 오직 디버깅을 위한 것이다. 이에 맞게 message를 작성하라.
  55. 55. 디버깅을 위한 메시지? 예외의 메시지는 사용자에게 보여주기에 적당치 않다. 여기서의 사용자는 개발된 시스템을 사용하는 최종 사용자. 메시지의 목적은 시스템 개선이다. 사용자에게 보여주는 내용은 변경되기 쉽고, 다국어도 고려해야 하고, 상황에 종속적일 수 있다. 예외의 메시지를 작성 시에 사용자가 볼지도 모른다는 우려할 필요 없고, 관련된 시간낭비 할 필요 없다. 따라서 굳이 한국어로 할 필요도 없고, 대화체라든가, 하여간에 시간 낭비할 필요 없다.
  56. 56. Anti Patterns if(someIsFailed) { String message = null; if(language.equals(“korean”)) { message = “소켓 읽기에 실패했습니다.” } else if(language.equals(“english”)) { message = “reading socket failed.”; } throw new SomeException(message); } // 던지는 예외의 메시지를 UI에서 직접 보여주어서는 않된다.
  57. 57. 로그 관련 예외가 처리되었으면 반드시 로 그를 남겨라. 정보를 누락하지 말고 로그로 남겨라.
  58. 58. GUIDE : 예외를 로그로 남길 때 모든 정보를 로 그에 남겨라. stack trace 포함해서.
  59. 59. 정보 출력 새로 정의한 예외 클래스이고, 별개의 정보를 가지고 있는 속성이 추가되었다면, 모든 정보를 출력할 getMessage() 메소드를 override하면 편하다. private String host; private int port; @Override public String getMessage() { StringBuffer messageSb = new StringBuffer(); messageSb.append(super.getMessage()); messageSb.append(“, host=“).append(host); messageSb.append(“, port=“).append(port); return messageSb.toString(); }
  60. 60. stack trace 예외가 발생한 메소드가 호출된 역사. 상황을 파악하는데 아주 중요하다. 사뭇 길어 보이고 로그가 지저분해 보일 수도 있다. 하지만 예외의 목적이 뭐? : 시스템 개선 Writer writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); String stackTrace = writer.toString();
  61. 61. GUIDE : 예외의 로그출력 은 정해진 곳에서만 한 다.
  62. 62. 로그 출력 위치 예외를 처리한 곳 아무 곳에서나 남기면 로그가 중복된다. 외부와의 인터페이스 이곳에서조차 안 남기면 로그가 남을 지에 대하여 보장하지 못한다.
  63. 63. Anti Patterns catch(SomeEception e) { logger.debug(e); throw new OtherException(“blar.”, e); }
  64. 64. 로그 레벨의 의미 • DEBUG – 말 그대로 디버깅을 위한 목적이다. – 아직도 DEBUG로 로그를 출력할 필요가 있다면 아직 안정화가 안된 것이다. • INFO – 시스템 동작에 대한 정보를 제공한다. • WARN – 현재 운영에는 문제가 없지만, 문제가 될 수 있는 사항 • ERROR – 시스템 혹은 기타 오류로 운영에 문제가 있는 사항. – 예외를 잡아서 정상처리한 경우. • FATAL – 시스템 운영이 불가능한 경우. – 예측되지 못한 예외를 잡아서 정상처리 못한 경우.
  65. 65. 로그 레벨 선택 방법 버그나 시스템 문제는 아니고, 단지 운영자에게 정보를 제공 INFO 지금은 로그를 남겨두지만, 좀 그렇다 싶으면 DEBUG 디버깅을 위해서 잠시 속살을 보여주는 것이다. 예측되는 예외가 발생하여 알려 주어야 한다 싶으면 WARN 예측되지 않은 예외가 발생하고 정상처리되면 ERROR, 그렇지 않으면 FATAL
  66. 66. 로그 레벨의 예 • DEBUG – 예: 입력받은 값 – 예 : DB에서 읽어온 값. • INFO – 예 : 시스템 구동 시간, 읽은 설정파일 위치, 캐싱 리프레쉬 사실. • WARN – 예 : 파일 시스템이 10% 남았다. – 예 : 설정 값을 읽었지만 사용되지 않는다. • ERROR – 예 : 설정값이 없어서 default 값을 사용했다. – 예 : DB 접속이 끊겨서 다시 재접속하였다. • FATAL – 설정된 컴퍼넌트를 로딩하지 못했다. – 예 : DB 접속이 되지 않는다.
  67. 67. 다시 강조 예외와 정보를 먹지 말자. 예외 메시지를 충실히.
  68. 68. MORE 예외와 로그도 프로젝트 차원으 로 정책이 마련되어야 한다. 설계 시 입력, 출력과 더불어 예 외도 포함되어야 한다.

×