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에 대해 알게되었다. 되도록 런타임예외처리를 해야겠다.