[DB 접근 기술] 스프링과 문제 해결 - 예외 처리, 반복

2022. 7. 5. 09:12Database/DB 접근 기술

체크 예외와 인터페이스

서비스 계층은 가급적이면 순수한 비즈니스 코드만 남겨놓는 것이 바람직하다. 하지만 DB 접근 기술이(예를 들면 JDBC의 SQLException) 리포지토리, 서비스, 컨트롤러에서 처리할 수 없는 체크예외를 던지는 경우라면  서비스, 컨트롤러는 해당 DB 접근 기술에 종속된다. 
체크 예외는 메소드 선언에 throw가 필수로 선언되어야 하기 때문이다. 따라서 DB 접근 기술이 JPA로 변경되는 경우 throws SQLException을 다른 exception으로 변경해야 하는 수고가 있다.

우선 인터페이스를 도입해서 DB 접근 기술의 변경이 용이하도록 해보자.

MemberRepository를 인터페이스로 지정하면, 서비스 계층은 MemberRepository 인터페이스에만 의존하면 된다. 구현체가 무엇이 오든 신경 쓸 필요 없는 것이다.

인터페이스로 선언한 MemberRepository는 다음과 같다.

public interface MemberRepository {
 Member save(Member member);
 Member findById(String memberId);
 void update(String memberId, int money);
 void delete(String memberId);
}

기존에는 이런 인터페이스를 생성하는 것이 불가능했는데, 그 이유는 SQLException이 체크 예외이기 때문이다. 인터페이스의 메소드에도 throws SQLException이 달려있어야 한다. 아래는 그 예시이다.

public interface MemberRepositoryEx {
 Member save(Member member) throws SQLException;
 Member findById(String memberId) throws SQLException;
 void update(String memberId, int money) throws SQLException;
 void delete(String memberId) throws SQLException;
}

쉽게 말하면, MemberRepositoryImpl이 thows SQLException을 하려면 MemberRepositoryEx 인터페이스에도 throws SQLException 이 필요하다. 구현 클래스의 메서드에 선언할 수 있는 예외는 부모 타입에서 던진 예외와 같거나 하위 타입이어야 한다.

구현 기술을 쉽게 변경하기 위해서 인터페이스를 도입하더라도 SQLException 과 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야 한다. 하지만 이것은 오염된 인터페이스라고 할 수 있다. JDBC에 종속되기 때문이다. 

하지만!

런타임 예외는 이런 부분에서 자유롭다. 인터페이스에 런타임 예외를 따로 선언하지 않아도 된다. 따라서 인터페이스가 특정 기술에 종속적일 필요가 없다.

이제 우리는 MyDbException이라는 RuntimeException을 상속받는 예외를 만들 것이다. 그리고 SQLException을 try/catch로 잡아서 예외 되던지기를 해줄 예정이다.

그렇게 되면 체크 예외인 SQLException을 try에서 잡은 뒤 catch 블록에서 MyDbException으로 던지게 되는데, MyDbException은 런타임 예외이므로 메소드 선언에 throws를 생략해도 된다.

try {
     con = getConnection();
     pstmt = con.prepareStatement(sql);
     pstmt.setString(1, memberId);
     pstmt.executeUpdate();
 } catch (SQLException e) {
	 throw new MyDbException(e);   // 예외 되던지기!!!
 } finally { 
	 close(con, pstmt, null);
 }

참고로 예외 변환(되던지기)를 할 때, 생성자의 파라미터로 기존의 예외를 꼭 넣어주자. 
그래야만 예외를 출력했을 때 원인이 되는 기존 예외도 함께 확인할 수 있다. 파라미터를 생략하면  로그에서 진짜 원인이 남지 않는 심각한 문제가 발생한다.

이제 예외 누수 문제(불필요한 throws가 반복되는 것)는 해결했다. 하지만 지금 방식은 항상 MyDbException 이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다.
만약 특정 상황에는 예외를 잡아서 복구하고 싶으면 예외를 어떻게 구분해서 처리할 수 있을까?

이때는 DB가 제공하는 에러코드를 사용하면 된다.
각각의 DB는 에러코드를 제공한다.

수업에서는 H2 DB를 사용했는데, H2는 키 중복 오류의 경우 23505라는 에러코드를 SQLException에 포함해서 보내준다. 우리는 이제 SQLException 내부에 있는 에러코드의 번호를 활용한다면, 에러의 구체적인 원인을 파악할 수 있을 것이다.

} catch (SQLException e) {
 //h2 db
 if (e.getErrorCode() == 23505) {
 throw new MyDuplicateKeyException(e);
 }
 throw new MyDbException(e);
}

만약 에러 코드가 23505라면, MyDuplicateKeyException이 발생하도록 만들어준다. MyDuplicateKeyException는 런타임 예외를 상속받아 만든 예외 클래스다.

에러 코드를 통해 예외의 구체적인 원인도 알 수 있고, 내가 세밀하게 다룰 수 있다는 점은 좋다. 그런데 에러 코드는 같은 예외일지라도 DB마다 전부 다 다르다. H2는 키 중복 오류가 23505인 반면 MySQL은 1062다.
또, 키 중복 뿐만 아니라 락이 걸린 경우, SQL 문법에 오류 있는 경우 등등 수십 수백가지 오류 코드가 있다.

그렇다면 우리는 이 모든 상황에 맞는 예외를 전부 다 MyDuplicateKeyException처럼 만들어야 할까? 
==> 아니다. 이미 스프링이 다 해놓았다.

스프링 예외 추상화 이해

스프링은 앞서 설명한 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공한다.

각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 따라서 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다. 예를 들어서 JDBC 기술을 사용하든, JPA 기술을 사용하든 스프링이 제공하는 예외를 사용하면 된다.

DataAccessException 은 크게 2가지로 구분하는데 NonTransient 예외와 Transient 예외이다.

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

스프링이 제공하는 예외 변환기

스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다.

SQLExceptionTranslator exTranslator = new
SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);

translate() 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException 을 전달하면 된다. 이렇게 하면 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환해준다. 어떻게 스프링은 예외를 자동으로 변환해줄 수 있는 걸까?

아까 위에서, 각 DB마다 같은 예외에 대한 에러코드가 다 다르다는 문제점이 있다고 했다. 스프링은 각 DB별로 에러코드와 예외 매핑 테이블을 이미 만들어 두었다. 즉, 에러코드를 받으면 그 에러코드에 해당하는 예외로 스프링은 자동 변환해준다.

<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
    	<value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
    </property>
    <property name="duplicateKeyCodes">
    	<value>23001,23505</value>
    </property>
</bean>
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
    	<value>1054,1064,1146</value>
    </property>
    <property name="duplicateKeyCodes">
    	<value>1062</value>
    </property>
</bean>

예를 들어 H2 데이터베이스에서 42000 이 발생하면 badSqlGrammarCodes 이기 때문에 BadSqlGrammarException 을 반환한다.

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

해당 파일을 확인해보면 10개 이상의 우리가 사용하는 대부분의 관계형 데이터베이스를 지원하는 것을 확인할 수 있다.

이제 서비스 계층에서 예외를 잡아서 복구해야 하는 경우, 예외가 스프링이 제공하는 데이터 접근 예외로 변경되어서 서비스 계층에 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다!

'Database > DB 접근 기술' 카테고리의 다른 글

트랜잭션과 격리수준, MVCC  (0) 2022.11.12
[DB 접근 기술] 자바 예외 이해  (0) 2022.05.29
[DB 접근 기술] 트랜잭션 (2)  (0) 2022.05.28
[DB 접근 기술] 트랜잭션 (1)  (0) 2022.05.28
[DB 접근 기술] DB 락  (0) 2022.05.27