본문 바로가기
Java/Spring

스프링 예외 추상화

by oneny 2023. 10. 7.

예외 추상화

스프링이 제공하는 예외 추상화를 이해하기 위해서는 먼저 자바 기본 예외에 대한 이해가 필요하다.

 

 

체크 예외와 언체크 예외

  • Object: 예외도 객체이다. 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object이다.
  • Throwable: 최상위 예외로 하위에 Exception과 Error가 있다.
  • Error: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
    • 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외로 잡으면 Error 예외도 함꼐 잡을 수 있기 때문에 Exception부터 예외를 잡아야 한다.
    • 참고로 Error도 언체크 예외이다.
  • Exception: 체크 예외
    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단, RuntimeException은 예외로 한다.
  • RuntimeException: 언체크 예외, 런타임 예외
    • 컴파일러가 체크하지 않는 언체크 예외이다.
    • RuntimeException과 그 자식 예외는 모두 언체크 예외이다.

 

언체크 예외의 장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있다. 따라서 신경쓰고 싶지 않은 예외를 무시할 수 있어 의존관계를 참조하지 않아도 된다는 장점이 있지만 개발자가 실수로 예외를 누락할 수 있다는 단점이 있다. 따라서 언체크 예외는 문서화를 잘하는 것이 중요하다.

 

체크 예외의 장단점

체크 예외는 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아줄 수 있다는 장점도 있지만, 위처럼 데이터 접근 계층에 SQLException처럼 Java의 JDBC(Java Database Connectivity) API와 관련된 예외를 던지게 되면 향후 JPA나 다른 데이터 접근 기술을 사용하면 그에 따른 예외로 변경해야 한다. 즉, SQLException으로 인해 서비스 계층이 JDBC 전용 기술에 종속적이게 되었다고 할 수 있다.

 

또한, SQLException처럼 데이터베이스에서 발생하는 문제처럼 심각한 문제들은 대부분 애플리케이션 로직에서 처리할 방법이 없다. 따라서 이런 해결이 불가능한 공통 예외는 별도의 오류 로그를 남기고, 개발자가 오류를 빨리 인지할 수 있도록 메일, 알림(문자, 슬랙) 등을 통해서 전달받아야 한다. 서블릿 필터, 스프링 인터셉터, 스프링의 ControllerAdvice를 사용하면 이런 부분을 깔끔하게 공통으로 해결할 수 있다. 하지만 SQLException으로 인해 JDBC 전용 기술에 종속적이게 되었다는 문제는 해결할 수 없다. 이를 위해서는 데이터 접근 계층에서 체크 예외를 언체크 예외로 전환하여 해결할 수 있다. 그렇게 되다면 JPA와 같은 기술로 변경하는 경우에는 위 그림처럼 데이터 접근 계층만 변경하고, 서비스나 컨트롤러는 변경하지 않아도 된다.

 

체크 예외 활용

  • 계좌 이체 실패 예외
  • 결제 시 포인트 부족 예외
  • 로그인 ID, PW 불일치 예외

기본적으로 언체크(런타임) 예외를 사용하고, 체크 예외는 위 상황처럼 비즈니스 로직상 의도적으로 던지는 예외만 사용하자. 물론 이 경우에도 100% 체크 예외로 만들어야 하는 것은 아니다. 하지만 계좌 이체 실패처럼 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안된다고 판단할 수 있기 때문에 체크 예외로 만들어 두면 컴파일러를 통해 놓친 예외를 인지할 수 있다.

 

예외 추상화

스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공한다. 각각의 예외는 특정 기술에 종속적이지 않게 설계되어있기 때문에 스프링이 제공하는 예외를 사용하면 된다. 즉, JDBC 기술을 사용하든, JPA 기술을 사용하든 스프링이 제공하는 예외를 사용하면 된다.

예외의 최상위는 org.springframework.dao.DataAccessException이다. 그림에서 보는 것처럼 런타임 예외를 상속받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다. DataAccessException은 크게 2가지로 구분하는데 NonTransient과 Transient 예외이다.

  • Transient 예외: Transient는 일시적이라는 뜻이다. 즉, 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있는 예외이다. 예를 들어 쿼리 타임아웃, 락과 관련된 오류들이 있는데 데이터베이스 상태가 좋아지거나 락이 풀려 다시 시도하면 성공할 가능성이 있다.
  • Nontransient 예외: NonTransient는 일시적이지 않다는 뜻이다. 같은 SQL을 그대로 반복해서 실행하면 실패한다. 예를 들어 SQL 문법 오류, 데이터베이스 제약조건 위배 등이 있다.

 

@Slf4j
class SpringExceptionTranslatorTest {

    DataSource dataSource;

    @BeforeEach
    void setUp() {
       dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }

    @Test
    void exceptionTranslator() {

       String sql = "select bad grammer";

       try {
          Connection connection = dataSource.getConnection();
          PreparedStatement pstmt = connection.prepareStatement(sql);
          pstmt.executeQuery();
       } catch (SQLException e) {
          SQLErrorCodeSQLExceptionTranslator translator = new SQLErrorCodeSQLExceptionTranslator(
             dataSource);
          DataAccessException resultException = translator.translate("select", sql, e);
          log.info("resultException", resultException);

          assertAll(
             () -> assertThat(e.getErrorCode()).isEqualTo(1054),
             () -> assertThat(resultException.getClass()).isEqualTo(BadSqlGrammarException.class)
          );
       }
    }
}

스프링이 제공하는 SQL 예외 변환기는 위처럼 사용하면 된다. translate() 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두 번째는 실행한 sql, 마지막은 발생된 SQLException을 전달하면 된다. 이렇게 하면 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환해준다. 따라서 SQL 문법이 잘못되었으므로 BadSqlGrammarException을 반환하는 것을 테스트하여 확인할 수 있다.

하지만 각각의 DB마다 SQL ErrorCode는 다른데 어떻게 각각의 DB가 제공하는 SQL ErrorCode까지 고려하여 예외를 변환할 수 있을까?

 

org.springframework.jdbc.support.sql-error-codes.xml

스프링 SQL 예외 변환기는 SQL ErrorCode를 이 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다. SQL 문법 예외가 발생하면 MySQL 데이터베이스에서 1054가 발생하기 때문에 badSqlGrammaerCodes를 통해 BadqlGrammerException을 반환하는 것이다.

 

스프링 예외 추상화 적용 - JdbcTemplate

데이터 접근 계층에 그냥 SQLErrorCodesSQLExceptionTranslator를 적용하면 다음과 같은 JDBC 반복 문제가 발생한다.

  • 커넥션 조회, 커넥션 동기화
  • PreparedStatement 생성 및 파라미터 바인딩
  • 쿼리 실행
  • 결과 바인딩
  • 예외 발생시 스프링 예와 변환기 실행
  • 리소스 종료

 

이런 반복을 효과적으로 처리할 수 있는 방법으로 트랜잭션에서 TransactionTemplate이 있듯이 예외를 추상화하기 위해 JdbcTemlate이 있다. JdbcTemplate 또한 JDBC의 반복 문제를 해결하기 위해 템플릿 콜백 패턴을 사용한다.

 

위 JdbcTemplate의 execute 메서드를 살펴보자. 사용자가 작성한 콜백을 넘겨받아 action.doInConnection() 메서드를 통해서 실행시키는 것을 확인할 수 있다. 그리고 만약 예외가 발생해서 SQLException에 잡히게 되면 throw translateException("ConnectionCallback, sql, ex)를 실행시키는 것을 확인할 수 있다. translateException 메서드를 살펴보면 SQLErrorCodesSQLExceptionTranslator 객체를 통해 translate 메서드를 실행해 SQL 예외를 변환시키는 것을 확인할 수 있다.

이렇게 JdbcTemplate은 템플릿 콜백 패턴을 제공하여 데이터 접근 계층의 반복 문제를 해결할 수 있다.

 

출처

스프링 DB 1편 - 데이터 접근 핵심 원리