2020-5-23 토비의 스프링 4장 예외처리

4장 예외처리

  • 기존 JdbcContext -> JdbcTemplate으로 바뀌면서 달라진 코드. throw SQLException이 사라졌다.

    • // Before
      public void deleteAll() throws SQLException {
        this.jdbcContext.executeSql("delete from users");
      }
      // After
      public void deleteAll() {
        this.jdbcTemplate.update("delete from users");
      }
      
  • 문제

    • 예외에 대해 try-catch를 쓰는건 좋다. 하지만 아무런 처리를 안하면 정말 위험하다.

    • // Bad
      catch (SQLException e) {
        System.out.println(e);
      }
      // Bad
      catch (SQLException e) {
        e.printStackTrace();
      }
      // 아주 조금 개선
      catch (SQLException e) {
        e.printStackTrace();
        System.exit(1); => 프로그램 종료
      }
      
    • 둘 다 메세지가 생기지만 운영서버에 올라가면 어차피 노답이다. 위의 코드는 예외처리를 한게 아니라 그냥 보여준거다.
  • 예외가 발생하면 복구되던지 아니면 작업을 중단하고 운영자, 개발자에게 통보되어야한다.

예외종류와 특징

자바에서는 크게 3가지 종류의 예외가 있다.

  • Error
    • java.lang.Error의 서브클래스들. 시스템에 비정상적인 상황이 발생할때. 주로 자바 VM에서 발생. ex) OutOfMemory, ThreadDeath. 이런 에러는 catch로 잡아도 대응안된다.
  • Exception과 체크예외
    • java.lang.Exception클래스와 그 서브클래스에서 정의된 예외. Exception클래스는 체크예외와 언체크예외로 구분
    • 체크예외는 Exception클래스의 서브클래스이면서, RuntimeException클래스를 상속하지 않은것들. 후자는 RuntimeException클래스를 상속한 클래스.
    • 체크예외가 발생할 수 있는 메소드를 사용할때 예외처리 코드를 반드시 작성해야한다.
  • RuntimeException과 언체크/런타임예외
    • java.lang.RuntimeException클래스를 상속한 예외들. 명시적으로 예외처리를 강제하지 않아서 언체크 예외라 한다.
    • 런타임예외는 catch로 잡거나 throws로 선언하지 않아도 된다. 주로 프로그램의 오류가 있을때 의도된것들이다.
    • NullPointException, IllegalArgumentException 등이 있다.
    • 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하는 예외.

예외처리 방법

  • 예외복구

    • 기본작업흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도.

    • 예외처리되면 기능적으로 사용자에게 예외상황이여도 애플리케이션에서는 정상적으로 설계된 흐름으로 진행 되어야한다.

    • int maxretry = MAX_RETRY;
      while(maxretry -- > 0) {
        try {
      	...
      		return;
      	catch(SomeException e) {
        	... 작업
        }
      	finally {
        }
      }
      throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외발생
      
  • 예외처리 회피

    • 예외처리를 직접처리하지 않고 호출한쪽으로 예외를 다시 던진다.

    • public void add() throws SQLExeption {
        // ...
      }
      
    • 무조건 예외를 던지면 무책임한 메서드가 된다. 이렇게하면 DAO에서 발생한 에러를 서비스나 컨트롤러 레이어에서 발생할 수 있다.
  • 예외전환

    • 전절한 예외로 전환해서 밖으로 던진다.

    • 내부에서 발생한 예외가 적절한 의미부여를 못하는 경우, 상황적인 예외로 만들어서 던진다. SQLException을 그대로 던지는 것이 아니라. DuplicateUserIdException과 같은 예외로 만들어서 던지는게 좋다. 이렇게하면 서비스 계층에서 적절한 복구작업을 할 수 있다.

    • public void exAdd() throws SQLException, DuplicateUserIdException {
        try {
          // jdbc
        } catch (SQLException e) {
          if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
            throw DuplicateUserIdException(e);
          else
            throw e;
        }
      }
      
    • 원래 발생한 예외를 담아서 중첩예외로 만드는것이 좋다.

예외처리 전략

  • 되도록 런타임 예외를 써라

    public class DuplicateUserIdException extends RuntimeException {
      public DuplicateUserIdException(Throwable cause) {
        super(cause);
      }
    }
    
    public void add(final User user) throws DuplicateUserIdException {
      try {
        this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                                 user.getId(),
                                 user.getName(),
                                 user.getPassword());
      } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
          throw new DuplicateUserIdException(e); // 예외전환
        else
          throw new RuntimeException(e); // 예외포장
      }
    }
    
  • 애플리케이션 예외

    • 시스템적인 예외가 아닌 애플리케이션 자체의 로직에서 의도적으로 예외를 발생시키고 처리할 수도 있다.

    • ex) 은행에서 잔액출금. 잔액이 부족하면 0, -1같은 특별한 값 리턴. 이때 리턴값이 명확하게 관리되지 않으면 혼란이 있다. 리턴값을 해주는게 아니라 InsufficientbalanceException과 같은 커스텀 예외를 던져라.

    • 이때는 체크예외를 던져라. 강제로 구현해줘야 안전하다.

    • try {
        BigDecimal balance = account.withdraw(amount);
        ...
          logic...
      } catch (InsufficientBalanceException e) {
        // InsufficientBalanceException에 담긴 잔고금액 정보를 가져옴..
        .. 잔고부족 안내메세지를 준비하고 출력해라.
      }
      

**결론 : SQLException은 99% 코드레벨에서 복구할 방법이 없다. DB커넥션 예외나, 네트워크 예외와 같은 시스템 예외는 애플리케이션 레벨에서 복구할 방법이 없다. 빨리 전달하는수밖에… 입력단계에서 검증을 강화하는 수 밖에 없다. throws 선언 남발하지 말고 언체크/런타임 예외로 전환해라. **

JdbcTemplate 내부적으로 메서드에 SQLException예외 대신 DataAccessException를 던지게 되어있다. 뿐만 아니라 스프링API의 예외는 대부분 런타임 예외로 되어있다.

예외전환

  • DB별로 에러가 코드가 다르기 때문에 OpenGroup의 XOPEN SQL스펙에 정의된 SQL코드를 따르게 되어있다.

  • SQLException의 종류도 많은데, DB별로 에러의 종류가 다 다르다. SQLException은 예외의 상태정보를 getSQLState()를 통해 가져올 수 있다. => DB연결실패는 08S01, 테이블이 없으면 42S02와 같이 DB독립적인 코드들을 담고있다. 하지만 이 역시도 신뢰할만한 데이터는 아니다.

  • 스프링에서는 DataAccessException에서 세분화된 예외클래스들을 정의하고 있다. BadSqlGrammarException, DataAccessResourceFailureException과 같은.

  • DataAccessException은 추상클래스로 구현되어있는데, 구현체들을 보면 SQL에서 발생할 수 있는 다양한 예외클래스들을 구현되있는것을 확인할 수 있다.

  • 스프링에서는 SQLException을 DataAccessException으로 전환하는 다양한 방법들을 보장한다. 주로 DB에러코드를 이용하는걸 많이 사용한다.

DAO인터페이스와 구현의 분리

  • DAO를 따로 만든 이유는 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리시키기 위함.
  • 분리된 DAO는 전략패턴을 적용해 구현방법을 변경해서 사용할 수 있다.
  • DAO 인터페이스를 기술에 완전히 독립적으로 만들려면 예외가 일치하지 않는 문제도 해결해야 한다. 그렇다고 public void add(User user) throws Exception;을 선언하면 무책임하다.
  • JDBC보다 늦게 등장한 Hibernate, JPA의 경우 런타임예외를 사용해서 throws 선언을 안해도 된다.
public interface UserDao {
    void add(User user);
    User get(String id);
    List<User> getAll();
    void deleteAll();
    int getCount();
}
  • setDataSource의 경우 UserDao구현방법에 따라 변경될 수 있으므로 포함시키지 않는다.

DataAccessException계층구조

  • DataAccessException은 계층구조로 추상화를 잘했다.
    • 데이터 엑세스 기술을 부정확하게 사용하면 InvlidDataAccessResourceUsageException이 던져지는데, JDBC에서는 BadSqlGrammarException, 하이버네이트에서는 HibernateQueryException 등의 형태로 구분된다.
    • JPA, 하이버네이트 같은 ORM에서는 낙관적Locking 에외가 발생할 수 있다. ObjectOptimisticLockingFaulureException을 공통적으로 발생시킨다.

    느낀점

    • 예외처리에 대한 개념이 크게 없었다. 단순히 throw를 이용해서 처리만 해주면 된다라는 생각이 있었는데, 이번장을 학습하면서 그동안 잘 몰랐던 RuntimeException에 대해 알게되었다. 되도록 런타임예외처리를 해야겠다.
Written on May 23, 2020