Framework/예외처리2009. 2. 9. 08:31

밑의 예외 처리에 이어지는 글.

자바의 정설 이론은 checked exception는 정상적인 것이며 runtime exception은 처리할 수 없는 프로그래밍 에러를 가리킨다. 그리고 아래의 글중 관례 2는 자바의 정설에 기반한 관례이다.

사실 checked exception의 간단한 사용 예를 설명하는 예제들을 보면 이는 분명 멋진 생각처럼 보인다. 문제는 간단한 사용 예제가 아닐때이다.
// example
public interface User {
    public boolean login(String userId, String pwd) throws SQLException ;
    .....
}
이를테면 위의 예제와 같이 사용자 정보를 가지고 허가 여부를 확인하는 매소드를 작성하였다고 하자. 그러나 위의 인터페이스는 아주  깨어지기 쉽다. 첫번째로 사용자 정보가 DB에 있다는 가정을 하였고 두번째로 DB라고 해서 SQLException만 리턴하는 것은 아니다.(IOException등도 throw 할 수 있다.) 세번째로 그러함에도 불구하고 SQLException은 충분히 추상적이지도 충분히 세세하지도 못한 예외라른 사실이다.

물론 그런 저런 책들에서 언급한대로
// example
public interface User {
    public boolean login(String userId, String pwd) throws NoUserIdException ;
    .....
}
와 같은 새로운 고수준 사용자 예외로 처리할 방법을 생각했을 수도 있다. 사실 책에서의 글대로 이 아이디어는 아주 멋져보인다. 다만 이생각은 무균의 연구실을 벗어난 순간 아래와 같은 사실들로 오염되어 버린다.


1. 너무 많은 코드

직접 login 메소드의 구현을 담당한다고 생각해 보자.

public boolean login(String userId, String pwd) throws NoUserIdException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NoUserIdException(.....) ;
      } catch(SQLException e){
             throw new NoUserIdException(.....) ;
      }
}

............... 와 같은 식의 메소드를 작성하게 된다. 별 문제 없어 보인다? What Problem ?...

문제는 이와 비슷한 식의 커스텀 예외를 던지는 메소드를 앞으로 수천개는 더 만들어야 한다는 것이다. 그 말은 수천개의 메소드들을 모두 자바의 표준 exception을 잡아서 사용자 예외로 wrapping해서 던지는 구문들만 수만줄은 된다는 얘기다.

단지 코드가 길다는게 좋은 프로그래밍이 아니다라고 할수는 없겠지만 이는 그 효과에 비해서 노력은 지나치게 많이 소모된다. 먼 미래 코드 어딘가에서 위 메소드를 사용했을때 NOUserIdException이라는 그럴듯한 이름을 가진 예외 처리를 할수 있다는 장점에 비해 위 관례를 지키기 위해 소요되는 노력은 지나치게 많다.



2. 깨지기 쉬운 메소드 원형

다시 위의 코드를 보자. login 메소드는 과연 NoUserIdException 만으로 충분한가? 위의 인터페이스로 좀더 코드를 작성하다보면 NotPasswordMatchedException이나 NotPermittedException 등등이 필요해질 것이다.
그러다 보면
public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
....와 같이 인터페이스를 수정해야 한다.

문제는 아직 더 있다. DB가 연결이 되지않을때도 엉뚱하게 그런 사용자가 없다느니 패스워드가 일치하지 않는다는 둥의 엉뚱한 예외가 튀어나올 것이다.

구현 메소드를 수정해서 connect절에 SQLException이 발생하면 다시 coonect DB 구문에 SQL 예외가 발생하면 다시 NotConnecttedUserInfo같은 사용자 예외를 던져야 할까?

그러다 보면

public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
 {
          // 변수 선언
           try {
                // connection DB 
           } catch(SQLException e){
               throw new NotConnecttedUserInfo(...) ;
          }
          // exectue sql 
         try {
           if (rs.next()) {
               if (pwd.equals(rs.getString("pwd") {
                    // 권한을 확인한다
                    try {
                       if ( isPermitted(rs)) {
                          ...
                      } else {
                         .....
                      }
                  } catch (SQLException ex) {
                   }
               } else {
                   throw new NotMatchedException(....) ;
               }
           } else {
              throw new NoUserIdException(.......) ;
           }
         } catch(SQLException e){
              ....               // outer SQLException
         } catch(IOException e) {
              .....             // outer IOException
         }

}

.........
공공장소에서 쓰레기를 버릴때 드는 죄책감이 들게된다. 첫번째 문제인 원래의 본 코드보다 예외 처리하는 코드가 더 복잡해 보인다는 건 일단 넘어가더라도 구현 메소드를 작성하면서 인터페이스를 수정해야 되는 웃기는 상황이 자주 발생하게 된다. 왜냐하면 새로운 Exception 종류가 앞으로도 계속 생겨날 것이기 때문이다. 그리고 그만큼 코드는 예외처리가 더욱 복잡해질 것이다.



3. 예외들의 끝없는 wrapping

바로 위의 예제의 // OuterSQLException 에 있는 예외 처리구문을 보자. 여기다가는 어떤 사용자 예외를 넣어야 할까? 기본적을 RecordSet인 rs의 모든 메소드는 SQLException을 던지기 때문에 안 잡을 수도 없다. 그럼 UnknownedException 같은 웃기는 이름의 exception을 새로 만들어야 할까 ? 아니면 그냥 원래의 SQLException을 던질까?

이 상황을 타계하기 위해 NotLoginnedException이라는 상위 수준으로 추상화된 Exception으로 Wrapping  했다고 하자. 

즉 이와 같이 고쳤다고 해보자.
         } catch(SQLException e){
             throw new NotLoginnedException(.... ) ;               // outer SQLException
         } catch(IOException e) {
             throw new NotLoginnedException(.... ) ;           // outer IOException
         }


그럼 메소드 원형은 public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, NotLogginedUserException etc.... 와 같이 될것이고 이 지저분한 쓰레기들을 보다가 문득 깨닫게 된다.

다른 사용자 예외들의 부모클래스로 NotLogginedUserException를 지정하면
public boolean login(String userId, String pwd) throws NotLogginedUserException ;
와 같이 간편하게 바꿀 수 있다고 ....

그럼 코드는 아래와 같이 수정할 수 있다.
public boolean login(String userId, String pwd) throws NotLogginedUserException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NotLogginedUserException (.....) ;
      } catch(SQLException e){
             throw new NotLogginedUserException (.....) ;
      }
}

사실 지저분한 코드를 잔뜩 썼다가 지웠지만 그 결과문은 맨 처음 예제에서 에외 이름만 바뀐거에 불과하다. 한걸음만 더 생각해보면 우리는 닭질을 하고 있다. 앞의 예에서 우리가 사용자 예외로 처리함으로서 얻을 수 있는 작은 이득은 NoUserIdException이라는 예외 이름 그 자체에 있다고 했다.

그런데 login이라는 메소드에 NotLogginedException이라는 예외 이름이 과연 직관적인가? 그럴바엔 애초에 SQLException이나 별 차이도 없지 않은가 말이다

이렇게 변명할 수도 있다. 물론 메소드 원형에서 던지는 NotLogginedException은 좀 그렇지만 아래와 같이 

void report(UserInfo userInfo) {
       try {
            boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
            .....
       } catch(NotPermittedException ex) {
           // print notPermitted message 
           ....
      } catch(NotLoggingedException ex ) {
      }
      ........
}

와 같이 쓸수 있을거라고 ......

라는 생각은 한번도 상업 프로그램을 짜보지 않은 학자들이나 교생들 혹은 공기가 통하지 않는 곳에서 연구를 하고 있는 연구생들이나 할만한 생각이다.

첫번째로 login이라는 원형 메소드에 있는 NotLoggingedException 도 신경쓰기 귀찮은 판에 그 하위 exception 까지 문서화된 정보를(그런게 있다면 말이다..) 보면서 신경쓰고 싶지 않고

두번째로 신경쓰고 싶더라도 report라는 메소드를 작성하고 있는 프로그래머는 login 메소드 말고도 그 안에서 호출하고 있는 수많은 메소들마다 던지는 사용자 Exception에 아주 질려버릴 것이기 때문이다.

그래서 report 함수의 프로그래머는
void report(UserInfo userInfo) throw Exception{
       boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
       .....
       // 기타 등등의 코드....

}

와 같이 최상위 Exception으로 처리해야 하는 귀찮음을 대충 보자기 Exception 같은걸로 둘둘 싸서 다른 프로그래머에게 던져 버린다.

물론 쓸데없이 부지런한 프로그래머는
void report(UserInfo userInfo) throw ReportException{
       try {
          boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
          .....
          // 기타 등등의 코드....
       } catch(Exception ex){
           throw new ReportException(....) ;
       }

}
와 같이 자신의 쓰레기도 쓸쩍 넣는 치밀함을 보이기도 한다 -ㅅ-;

이를 단순히 개발자의 게으름으로 치부할 수는 없다. 엔트로피는 상승하게 마련이고 사실 작은 코드에서는 멋져보이는 해결책이 실제로는 엄청난 엔트로피의 증가를 가져오는 씨앗이 됐기 때문이다.

작은 메소드에서는 합당해 보이지만 그 메소드들을 호출하여 만드는 다른 프로그래머는 checked exception에 드는 과도한 노력을 피하기 마련이고 이는 결국 속이 보이지 않는 검은색 봉투로 exception을 모두 담아서 wrapping을 해버린다. 물론 이러한 과정에서 유용한 정보를 더한다면 나름 그럴듯한 선택일수도 있다. 그러나 애초에 복구가 가능하지 않는 NotLogginedException같은 경우 그것을 감싸는 것은 아무것도 성취하지 못하고 새로운 쓰레기만 더하게 된다. 그리고 그 쓰레기 만큼이나 stack rewind 하는 비용이 더 들게 된다.


4. 위의 이유들로 checked Exception이 인터페이스에서 잘 동작하지 않는다. 

결국 인터페이스를 작성할때 UnKnownException같은 불분명한 중립적 사용자 예외를 작성하거나 최상위 Exception을 던지는 인터페이스들을 만들다 보면 상황은 더욱 더 심각해진다.

마치 늪처럼 발버둥을 칠수록 더욱 더 깊이 빠져들게 되는 것이다.
이 문제들은 처리할 수 없는 예외들을 잡고, 감싸진 예외들을 다시 던지도록 강요받는 checked Exception의 문제로 속성지어질 수 있다. 이것은 성가시고, 그 자체가 에러를 낳기 쉬우며, 어떤 유용한 다른 목적도 제공하지 않는다.


그럼 다음에는 계속....


'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
예외 처리 격언  (2) 2009.02.09
Posted by bleujin