4장. 예외

지하나·2021년 9월 15일
0

토비의 스프링 v1

목록 보기
4/6
post-thumbnail
  • 출처: 토비의 스프링 3.1 vol.1 스프링의 이해와 원리

4.1 사라진 SQLException

사실 JdbcContext에서 스프링의 JdbcTemplate으로 대체하면서 매우 중요하지만 언급되지 않은 것이 있는데 배로 예외 처리에 대한 부분이다.

// JdbcContext에는 있던 SQLException이..
public void deleteAll() throws SQLException {
	this.jdbcContext.executeQuery("delete from users");
}
// JdbcTemplate에서는 사라졌다..
public void deleteAll() {
	this.jdbcTemplate.update("delete from users");
}

SQLException 에러 처리.. 어떻게 없어도 되게 된것일까?

예외의 종류와 특징

자바에서 Exception 클래스는 UnCheckedExceptionCheckedException으로 나누어진다.

UnCheckedExceptionRuntimeException 클래스를 상속한 클래스이고, 런타임 예외 시 catch문으로 잡거나 throws로 선언하지 않아도 된다. 피할 수 있었지만 개발자의 부주의로 발생할 수 있는 경우에 던지도록 만들어진 것으로 예상하지 못했던 예외가 아니고, 굳이 잡아도 코드 레벨에서 복구를 할 수 있는 경우가 거의 없기 때문이다.

이와 반대로 CheckedException은 복구할 가능성이 조금이라도 있는, 말 그대로 "예외적인" 상황이기 때문에 예외를 처리하는 코드를 반드시 작성해주어야 컴파일 에러가 나지 않는다.

예외처리 방법

예외를 처리하는 첫 번째 방법은 예외 시, 정상적인 플로우로 바꿔놓는 예외 복구이다. 예를 들어 요청한 파일을 읽으려고 하였으나 파일이 없어 IOException이 났다면 사용자에게 이를 알려 다른 파일을 읽도록 하는 등의 해결을 말한다. 사용자 입장에서는 예외 복구라고 볼 수는 없지만 정상적으로 설계된 흐름으로 돌려보낸다는 관점에서의 복구라고 할 수 있다.

예외를 처리하는 두 번째 방법은 예외 처리를 자신이 담당하지 않고, 자신을 호출한 쪽으로 throws해서 책임을 회피하는 예외처리 회피이다. 단순한 예외 처리 회피라기 보다는 이 때는 자신을 호출한 측에서 예외를 다루는 것이 로직 상 분명할 때가 해당한다.

예외를 처리하는 세 번째 방법은 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던져주는 예외 전환이 있다. 예외 전환은 보통 2가지 목적을 가지는데, 첫 번째는 예외 상황에 대한 의미를 분명하게 해주기 위해 예외를 바꾸는 것이다. 보통 원래 발생한 예외에 담아서 중첩 예외로 만드는 것이 좋다. 두 번째는 예외를 처리하기 쉽고 단순하게 만들기 위해 담아주는 것으로 주로 예외 처리를 강제하는 체크 예외를 언체크 예외로 바꾸는 것이다.

SQLException은 어떻게 됐나?

그래서 SQLException 에러 처리 어떻게 없어지게 된 것인지를 다시 이야기하면.. SQLException이 복구가 가능한 예외인가를 생각해보면 된다.

SQLException이 난 경우는 대부분 코드 레벨에서 복구할 수가 없고, 통제할 수 없는 외부 상황에서 발생한다. 따라서 예외 처리를 기계적인 throws 선언으로 무책임한 예외 토스 릴레이를 계속 하기 보다 런타임 예외로 전환해주는 것이 맞다.

스프링의 JdbcTemplate은 이러한 방식을 따라서 그 안에서 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다. 따라서 이를 받아오는 UserDao 측에서는 예외 처리를 의도한다면 DataAccessException을 catch해서 처리해주면 되고, 그 외에 경우에는 무시해도 된다. 그래서 앞장에 JdbcTemplate을 적용한 UserDao 메소드에서는 SQLException이 사라진 것이었다.


4.2 예외 전환

정리하면 예외 전환에는 CheckedException을 런타임 에러로 포장하여 불필요한 예외 처리를 없애주는 것과 SQLException에 담겨있는, 다루기 힘든 상세한 예외 정보들을 좀 더 의미있고 일관성 있는 예외로 전환해서 추상화해주는 용도로 설명할 수 있다.

JDBC의 한계

JDBC는 표준 인터페이스를 통해 그 기능을 제공하기 때문에 DB의 종류에 상관없이 일관된 방법으로 개발할 수 있도록 하는 장점이 있다. 하지만 DB를 자유롭게 변경해서 사용할 수 있도록 완전히 유연한 코드를 제공하지 못하게 하는.. 몇 가지 한계가 있다.

첫 번째로 JDBC 코드에 사용되는 SQL 쿼리문를 생각해볼 수 있다. 개발 과정에서 비표준 SQL 문장이 사용될 수 있는데 이것이 코드에 들어가면 결국 특정 DB에 종속하게끔 만들게 하고, 다른 DB로 변경하려면 쿼리문들을 변경해야 한다.

두 번째로 호환성 없는 SQLException이다. DB마다 SQL 쿼리문 뿐아니라 예외의 종류와 원인도 제각각인데 JDBC는 예외들을 모두 그냥 SQLException 하나에 모두 담아버린다. 예외 코드를 가져와도 이는 DB별로 모두 다르고, SQLException의 상태 코드도 정확하게 만들어주지 않기 때문에 호환성이 없다. 결국 SQLException만으로 DB에 독립적인 유연한 코드를 작성하는 것이 불가능하게 된다.

DB 에러 코드 매핑을 통한 전환

DB 종류에 상관없이 일관된 DAO를 만들기 위해서 우선 두 번째 문제에 집중해보자. SQLException에 담긴 SQL 상태 코드가 일관적이지 못하니 결국 해결책은 DB별로 에러 코드를 참조해서 예외의 원인을 파악해와서 의미가 분명히 드러나는 예외로 전환해주어야 한다.

앞에서 DB마다 에러 코드가 제각각이라고 하였는데 스프링에서는 DB별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러코드 매핑 정보 테이블을 만들어두고 이를 참고하여 적절한 예외 클래스를 반환해준다.

public class UserDao {
    private JdbcTemplate jdbcTemplate;
    public void add() throws DuplicateKeyException {
    }
}

JdbcTemplate를 사용하는 UserDao는 SQLException에 대해서는 이제 런타임 예외인 DataAccessException로 던져질 것이므로 예외 처리르 하지 않아도 된다. 만약 중복된 키에 대한 에러를 처리한다면 위와 같이 DuplicateKeyException으로 던지면 된다. 이 때는 DB 종류가 변경되더라도 JDBCTemplate 안에서 DB별로 미리 준비된 에러 코드와 비교해서 적절한 예외를 던져주기 때문에 일관된 예외 처리가 가능해진다.

DAO 인터페이스와 DataAccessException 계층 구조

자바에는 JDBC 외에도 데이터 액세스를 위한 표준 기술이 존재한다. 사실 DataAccessException은 JDBC의 SQLException 전환용으로 만들어진 예외는 아니고, JDBC 외에도 데이터 액세스 기술의 종류와 상관없이 의미가 같은 예외라면 일관된 예외가 발생하도록 만들어준다.

다시 한번 짚고 가면, DataAccessException 클래스들이 단지 JDBC의 SQLException을 전환하기 위한 용도가 아니라는 점이다. DataAccessException는 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화한 것이다.

스프링의 JdbcTemplate은 SQLException의 에러 코드를 DB별로 매핑해서 그에 해당하는 DataAccessException의 서브 클래스 중 하나로 전환해서 던져준다.

스프링은 왜 이렇게 DataAccessException 계층 구조를 이용하여 데이터 액세스 기술에 독립적인 예외를 정의하고 사용하게 하는 것일까?

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

우선 DAO를 왜 따로 분리해서 클래스로 정의했었는지부터 생각해보면.. 데이터 액세스 로직을 담은 코드로부터 분리하기 위함이었다.

DAO를 사용하는 측에서는 이 DAO가 내부적으로 어떻게 DB와 연결해서 데이터를 가져오는지는 신경쓰지 않아도 된다. 데이터 액세스 기술에 독립적으로 단순히 오브젝트를 주고받고 데이터 액세스 기술을 사용하기만 하면 된다.

그렇다면 DAO는 인터페이스로 정의해 추상화하고, DI를 통해 사용하는 것이 스프링스러운 방식일 것이다.

public interface UserDao {
    public void add(User user);
}

이렇게 인터페이스로 정의한 DAO에는 setDataSource() 메소드는 추가하지 않아야 한다. setDataSource() 메소드는 UserDao가 사용하는 기술에 따라 로직이 달라지고, 클라이언트 단에서 이를 알아야할 필요도 없기 때문이다.

그리고 위와 같이 인터페이스로 정의하면 예외 처리를 해주어한다는 사실 문제는 여기서 생긴다. 구현 기술마다 던지는 예외가 다르기 때문에 메소드 선언에서 문제가 생기는 것이다. 만약 JDBC API를 직접 사용하는 경우라면 SQLException을 던질 것이다. 그렇다고 메소드 선언문에 throws로 넣어주면 JDBC 외의 다른 액세스 기술은 사용할 수 없게 되는 것이다.

단순히 이를 모두 Exception으로 받아오면 되기는 하지만 무책임하다. 따라서 이러한 경우에는 DAO 내부 로직에서 런타임 예외로 포장해서 예외 전환으로 처리하도록 한다.

데이터 액세스 예외 추상화와 DataAccessException 계층구조

그러면 중복 키 에러와 같이 복구 가능하고 처리가 필요한 예외들은 어떻게 해야 할까? DAO를 사용하는 클라이언트 단에서 DAO가 사용하는 데이터 액세스 기술에 따라 예외 처리 방법을 달리해야 한다. 결국은 사용하는 기술에 의존적이 될 수 밖에 없게 된다.

그래서 스프링은 DataAccessException 계층 구조를 이용해서 데이터 액세스 기술에 독립적으로 예외를 처리할 수 있도록 한다.


"개인적으로 공부하면서 정리한 자료입니다. 오타와 잘못된 내용이 있을 수 있습니다."

profile
개발은 즐겁게🎶

0개의 댓글