Checked, Unchecked Exception
Checked Exception 은 예외 처리가 필요한 예외를 뜻하고, Unchecked Exception 은 예외 처리를 하지 않아도 되는 예외를 뜻한다.
Error 는 시스템이 비정상적인 상황에서 발생한다. 개발자가 로직 구현 단계에서 미리 예측할 수 없기 때문에 따로 처리할 방안도 존재하지 않는다. 애플리케이션단
에서는 Error 에 대한 처리를 신경쓰지 않아도 된다. OOM(OutOfMemory) 나 ThreadDeath 같은 에러는 예외 처리로 잡아도 할 수 있는 것이 없기 때문이다. 따라서, Checked 와 Unchecked Exception 에 대한 예외처리가 중요해진다.
Checked, Unchecked Exception 의 특징을 살펴보자.
- Checked Exception
- 컴파일 타임에 검사되는 예외
- 반드시 예외 처리를 해야 함
- try-catch 구문으로 처리
- throws 로 예외 던지기
- (스프링 한정) 예외 발생 시 트랜잭션에 의해 roll-back 되지 않음
- Ex. IOException, SQLException, FileNotFoundException, ClassNotFoundException 등
- Unchecked Exception
- 런타임 시점에 검사되는 예외
- 예외 처리를 하지 않아도 상관 없음
- (스프링 한정) 예외 발생 시 트랜잭션에 의해 roll-back 된다.
- Ex. RuntimeException 을 포함한 하위에 존재하는 모든 예외
- NullPointerException, IllegalArgumentException, IndexOutOfBoundException 등
이 두가지 유형의 Exception 을 구분하는 기준은 RuntimeException
을 상속 받는지의 여부이다. 여기서 주의 깊게 볼 부분은 예외 발생 시 트랜잭션에 의해 rollback 이 되는지이다. 만약, rollback 이 일어나지 않는다면 데이터의 무결성
, 일관성
을 보장하지 못할 것이다.
rollback 이 일어나지 않을 경우 어떻게 동작할지 소스를 살펴보자.
public void transcationalTest() {
등록(INSERT)
수정(UPDATE)
삭제(DELETE)
throw new IOException();
}
등록, 수정, 삭제가 하나의 메서드 안에서 순차적으로 이루어지는데, CheckedException 이 발생하여, 삭제가 제대로 되지 않았다고 해보자. rollback 이 되지 않는다면, 등록과 수정은 수행되고 삭제만 수행되지 않은 상태로 데이터베이스의 상태가 변경된다.
Checked Exception 이 Rollback 되지 않는 이유 ?
자바 자체에서 Checked Exception, UnChecked Exception 은 사실 트랜잭션과 관련이 없다. 다만, 스프링에서 기본 전략을 UnChecked Exception 은 rollback, Checked Exception 은 rollback 하지 않는것으로 가져가고 있다.
기본 전략이 그렇다는거지, rollbackFor 옵션을 사용하여 Checked Exception 이 발생해도 rollback 되게 할 수 있고, UnChecked Exception 이 발생해도 rollback 이 되지 않게 할 수 있다.
기본적으로 Checked Exception 은 복구가 가능하다는 메커니즘
을 가지고 있다. 즉, 복구 전략
을 가질 수 있다는 것이다. 하지만 현실적으로 Checked Exception 이 발생했을 때, 복구 전략을 가지고 복구할 수 있는 경우가 많지 않다. 특히 SQLException
과 관련된 부분이 그렇다.
예를 들어, 한 테이블의 Unique 값이 중복 되어 SQLException 을 발생시키면 어떻게 복구할 것인가 ... 가장 현실적인 방법은 CheckedException 을 UncheckedExcpetion 으로 변경시켜서 다시 입력하게 하는 것이 가장 현실적인 방법이다.
Checked Exception을 만나면 더 구체적인 Unchecked Exception을 발생시켜 정확한 정보를 전달하고 로직의 흐름을 끊어야 한다. 예외 처리의 가장 중요한 원칙은 모든 예외는 적절하게 복구 되던지 아니면 작업을 중단시키고 개발자에게 분명하게 통보 되어야 한다.
예를 들어, 스프링에서 ObjectMapper 의 writeValueAsString 를 사용하여 JSON 을 String 타입으로 변환하는 코드가 있다고 해보자.
public void writeValueAsStringTest(Object obj) throws JsonProcessingException {
return objectMapper.writeValueAsString(obj);
}
해당 메서드는 Checked Exception(JsonProcessingException)을 발생 시킬 수 있기 때문에 예외 처리를 필수로 해야한다. (try-catch 를 쓰던, throws 로 예외를 던지던)
하지만 위 코드처럼 무턱대고 그냥 throws 를 남발하면, 해당 메서드를 사용하는 곳에서 다시 처리해야하기 때문에 목적이나 사용하고자하는 의미가 명확한것이 아니라면 좋은 방법은 아니다. 따라서, Checkd Exception 을 더 구체적인 Unchecked Exception 으로 변경시키고, 스프링을 사용하고 있다면 @ExceptionHandler
를 사용하여 예외 처리를 더 깔끔하게 할 수 있다.
public void writeValueAsStringTest(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch(JsonProcessingException e) {
throw new JsonSerializeFailed(e.getMessage());
}
}
try-catch 문을 사용한다면, 위 코드 처럼 더 구체적인 Unchecked Exception 을 발생시키는 것이 좋다. try-catch 를 썼는데 최악의 코드는 다음과 같다.
try {
// 생략
} catch (Exception e){
e.printStackTrace();
}
e.printStackTrace() 는 시스템 출력이기 때문에 서버에 로그를 남기기 위해서는 log
나 UncheckedException
으로 처리해야한다.
SQLException 이 사라진 이유 ?
SI 에서 근무한 경험이 있다면, 레거시 프로젝트들에서 DAO 에 SQLException 을 던지는 코드를 많이 봤을 것이다.
public void create(Member member) throws SQLException {
// JDBC API
}
여기서 SQLException 이 발생했다고 치자.. DAO 에 있는 create 메서드를 호출하는 곳은 서비스 레이어일 것이다. 그럼 서비스에서 try-catch 로 감싸서 예외 복구를 할껀가? 위에서 말했다싶이, SQLException 은 복구 처리가 불가능하다. SQLException 이 발생하는 이유 자체가 SQL 문법에 에러가 있거나 데이터 액세스 로직에 심각한 버그가 있거나, 서버가 죽거나 네트워크가 끊기 는 등의 심각한 상황이 벌어졌기 때문이다. 그러면 Controller 까지 throws 해버릴 것인가?.. 했다 치자.. 그러면 과연 rollback 이 제대로 될까? 설정을 잘 하면 되긴 한다.
저렇게 위와 같이 코드를 짠 사람이라면 제대로 rollback 이 되게끔 서비스 레이어에서 설정을 하지 않았을 가능성이 크다.
Checked Exception 은 Spring Transaction 에서 자동으로 rollback 을 하지 않는다.
따라서, 추가적인 설정이 필요한데 @Transacational
에 rollbackFor
설정을 주어야 한다.
@Transactional(rollbackFor = {SQLException.class})
public void create(Member member) throws SQLException {
// JDBC API
}
또, rollbackFor 옵션에 Excpetion 을 적는 행동은 하지 말아야 한다. 기대하지 않은 곳에서 롤백이 될 수 있다.
JdbcTemplate 을 사용하기 전
JdbcTemplate 을 사용하는 코드에서 SQLException 이 사라진 이유에 대해서 알기 위해서는 스프링의 예외 전략과 원칙을 이해해야 한다. 그 전에 JDBC 를 통해 데이터베이스에 접근하는 과정을 먼저 보자.
- 드라이버 로딩
- 데이터베이스 연결
- SQL 문 작성 및 전송
- 자원 해제
JDBC 를 이용하여 데이터베이스와 통신을 하기 위해선 위 과정을 동일하게 반복해야해서, 중복되는 코드가 많이 발생한다는 단점이 있다. JdbcTemplate 은 이전에 JDBC 를 사용하여 데이터베이스에 접근하기 위한 4개의 과정을 모두 Spring Framework
에게 맡기고, 개발자는 쿼리문만 생성하여 질의응답할 수 있게 되었다.
JPA 를 포함한 스프링과 Mybatis 를 사용하는 모든 곳에서는 이제 JdbcTemplate
을 이용할 수 있게 되었다. 스프링 JdbcTemplate 은 SQLException(Checked Exception) 을 DataAccessException(UncheckedException)
로 포장하여 예외를 던진다.
예외 전환의 목적
SQLException 을 DataAccessException 으로 예외 전환을 하는 이유에 대해서 살펴보자.
예외 전환의 목적은 두 가지 이다.
- 런타임 예외로 포장하여 불필요한 catch/throw 를 줄여주는 것
- 로우 레벨의 예외를 좀 더 의미 있고 추상화된 예외로 바꾸어 주는 것
스프링 JdbcTemplate 이 SQLException 을 DataAccessException 로 좀 더 의미있고 추상화된 예외로 바꾸어 주기 때문에 복구 불가능한 예외인 SQLException 에 대해 애플리케이션 레벨에서는 신경쓸 필요가 없어졌다.
JdbcTemplate 의 update(), query() 등 메서드 선언을 살펴보면 throws DataAccessException 이라고 되어있는 것을 볼 수 있다. 그 밖에도 스프링 API 메서드에 정의되어 있는 대부분의 예외는 런타임 예외이다.
SQLException 의 문제점
DB 를 사용하다보면 발생할 수 있는 예외의 원인들이 다양하다. 커넥션 문제, 문법 문제, 키가 중복되는 문제 등이 있을 것이다. 문제는 DB 벤더 마다 이러한 에러의 종류와 원인들이 제각각이다. 그래서 JDBC 는 데이터 처리 중에 발생하는 다양한 예외를 SQLException 하나에 담아버린다. 즉, JDBC API 는 SQLException 한 가지만 던지도록 설계되어있다. 따라서 DB 에 독립적인 유연한 코드를 작성하기 어렵다.
이러한 문제를 해결하고자 스프링 JdbcTemplate 이 SQLException 을 DataAccessException 으로 예외 전환을 하는 것이다. DataAccessException 서브 클래스에서는 데이터 처리 중에 발생할 수 있는 예외들에 대해서 세분화하여 정의하고 있다. 따라서, JdbcTemplate 이 DB 의 에러 코드를 DataAccessException 계층 구조의 클래스 중 하나로 매핑해준다.
이러한 이유들로 인해서 JdbcTemplate 에서는 SQLException 이 사라지게 되었다.
References
'Programming Languages > Java' 카테고리의 다른 글
자바 버전별 역사 및 특징 (0) | 2022.01.12 |
---|---|
다양한 자바 플랫폼(SE/EE/ME) (0) | 2022.01.11 |
JVM Archtiecture (0) | 2021.12.24 |
자바 프로그램 실행 과정 (0) | 2021.12.24 |
PatternMatchUtils 를 활용한 필터링 (0) | 2021.12.19 |