
다음은 정말 난감한 예외처리 방법들이다.
초난감 예외처리 1
try{
} catch(SQLException e){
}
초난감 예외처리 2
try{
} catch(SQLException e){
System.out.println(e);
}
초난감 예외처리 1
try{
} catch(SQLException e){
e.printStackTrace();
}
try/catch 블록을 통해 예외를 잡아내는 것은 좋다. 하지만 위와 같이 별문제 없는 것처럼 넘어가 버리는 건 정말 위험한 일이다. 이는 예외가 발생하는 것보다도 훨씬 나쁜 일이다.
public void method1() throws Exception {
method2();
...
}
public void method2() throws Exception {
method3();
...
}
public void method3() throws Exception ...
정확한 예외를 던지지도 않고 그저 throws Exception로 무책임하게 던지는 것은 아주 큰 문제가 있다. 적절한 처리를 통해 복구될 수 있는 예외 상황도 제대로 다룰 수있는 기회를 박탈당한다.
RuntimeException을 상속받는 예외를 ❌ 언체크 예외, 상속받지 않는 예외를 ✅ 체크 예외는 라고 부른다.
✅ 체크 예외 는 반드시 체크해야 하기 때문에 체크 예외로 불리는데, 체크 예외는 catch 문으로 잡든지, 다시 throws를 정의해서 메소드 밖으로 던져야 한다.
❌ 언체크 예외 는 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외로 불린다. 필수적으로 try/catch를 사용하지 않아도 된다.
try/catch를 사용해서 다른 작업 흐름으로 유도하거나 사용자에게 알려주는 방식을 통해 기능적으로 정상적으로 진행하게 하는 것을 말한다. 다만, 그냥 에러 메시지를 사용자에게 던지는 것은 예외 복구라고 볼 수 없다.
int maxRetry = MAX_RETRY;
while(maxRetry --> 0) {
try {
... // 예외가 발생할 수 있는 시도
return; // 작업 성공
}
catch(SomeException e) {
// 로그 출력, 정해진 시간만큼 대기
}
finally {
// 리소스 반납, 정리 작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
throws 문으로 바깥으로 던져지게 하거나 catch문으로 예외를 잡은 후에 다시 던지는 것을 이야기한다.
public void add() throws SQLException {
try {
// JDBC API
}
catch(SQLException e) {
// 로그 출력
throw e;
}
}
예외 회피와 비슷하게 예외 메소드를 밖으로 던지는 것이다. 하지만 예외 회피와 다르게 적절한 예외로 전환해서 던진다. 예외전환에는 크게 두가지 방법이 있다.
1. 상황에 적합한 의미를 가진 예외로 변경해서 던진다.
public void add(User user) throws DuplicateUserIdException, SQLException {
try {
// JDBC를 이용해 user 정보를 DB에 추가하는 코드 또는
// 그런 기능을 가진 다른 SQLException을 던지는 메소드를 호출하는 코드
}
catch(SQLException e) {
// ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환
if (e.getERrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
throw DuplicateUserException();
else
throw e; // 그 외의 경우는 SQLException 그대로
}
}
SQLException에서 DuplicateUserException으로 적합한 의미의 예외로 변경한다.
2. 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.
try {
...
} catch (NamingException ne) {
throw new EJBException(ne);
} catch (SQLException se) {
throw new EJBException(se);
} catch (RemoteException re) {
throw new EJBException(re);
}
체크예외인 예외들을 언체크 예외인 EJBException으로 포장해서 던진다.
최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다.
런타임 예외로 만들면 필수적으로 체크해야하지 않기 때문에 주의를 기울일 필요가 있다.
애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, catch를 해서 조치를 취하도록 요구하는 예외를 애플리케이션 예외라고 한다.
예를들어, 예외를 적용하기 전에 은행업무를 구현한다고 할 때 잔고가 부족하면 다음과 같이 코드를 작성할 수 있다.
BigDecimal balance = account.withdraw(amount);
if(balance <0){
return ;
}
if()
...
하지만, 애플리케이션 예외를 적용하면 아래와 같이 작성이 가능하다.
try {
BigDecimal balance = account.withdraw(amount);
...
// 정상적인 처리 결과를 출력하도록 진행
}
catch(InsufficientBalanceException e) { // 체크 예외
// InsufficientBalanceException에 담긴 인출 가능한 잔고 금액 정보를 가져옴
BigDecimal availFunds = e.getAvailFunds();
...
// 잔고 부족 안내 메세지를 준비하고 이를 출력하도록 진행
}
이렇게 구현하면 불필요한 if문들 없이 깔끔하게 코드를 작성할 수 있다.
SQLException은 코드 레벨에서 복구할 방법이 없다. 차라리 사용자에게 알리고 개발자가 빨리 인식할 수있도록 전달하는 방법 밖에 없다. 그러기 위해서는 기계적인 throws보다는 언체크/런타임 예외로 전환해야 한다.
JdbcTemplate의 메소드들을 보면 throws DataAccessException라고 되어 있다. 이른 런타임 에러로 잡거나 다시 던질 의무가 없다.
JDBC는 데이터 처리중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다.
이는 DB별로 달라지는 에러 상황에 적절하게 대처하기 어렵다. 이로인해 DB에 독립적인 유연한 코드를 작성하는 것은 불가능에 가깝다.
DB종류가 바뀌더라도 DAO를 수정하지 않으려면 SQLException의 비표준 에러코드와 SQL 상태정보에 대한 해결책을 알아야 한다.
예시
<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>900,903,904,917,936,942,17006,6550</value>
</property>
<property name="invalidResultSetAccessCodes">
<value>17003</value>
</property>
<property name="duplicateKeyCodes">
<value>1</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>1400,1722,2291,2292</value>
</property>
<property name="dataAccessResourceFailureCodes">
<value>17002,17447</value>
</property>
<property name="cannotAcquireLockCodes">
<value>54,30006</value>
</property>
<property name="cannotSerializeTransactionCodes">
<value>8177</value>
</property>
<property name="deadlockLoserCodes">
<value>60</value>
</property>
</bean>
오라클 에러코드를 매핑한 파일이다. 위와같이 에러 코드들과 DataAccessException 계층에 있는 클래스 중 하나로 매핑해서 예외를 전환할 수 있다.
DataAcessException은 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다.
public interface UserDao {
public void add(User user)
}
인터페이스를 다음과 같이 작성하고 DAO 구현 케소드 안에서 예외를 런타임 예외로 포장해서 던져주게 되면 UserDao는 데이터베이스 접근에 전혀 상관없이 고정된 인터페이스를 가지게 된다.
특히나 최근에 등장한 JDO, Hibernate, JPA등의 기술은 런타임 예외를 사용해서 throws에 선언하지 않아도 된다.
DB 종류나 데이터 액세스 기술에 따라 키 값이 중복이 되는 상황에서 다른 예외가 발생한다.
데이터 액세스 기술을 Hibernate나 JPA를 사용했을 때도 동일한 예외가 발생할 것으로 기대하지만 실제로 다른 예외가 던져진다.
예를 들어 Hibernate에서 중복 키가 발생하는 경우에 하이버네이트의 ConstraintViolationException을 발생시킨다.
따라서, DataAccessException을 잡아서 처리하는 코드를 만들려고 한다면 미리 학습 테스트를 만들어서 실제로 전환되는 예외의 종류를 확인해 둘 필요가 있다.
만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면 DuplicatedUserIdException
처럼 직접 예외를 정의해두고, 각 DAO의 add() 메소드에서 좀 더 상세한 예외 전환을 해주면 된다.