예외 구분(errorCode), DataAccessException, 예외 변환기

도토리·2023년 5월 7일
0

스프링 DB 접근

목록 보기
6/6

service layer를 순수하게 만들자.

repository 구현 기술이 변경되더라도 service 코드가 변경되지 않도록 하자.

  1. repository에 인터페이스 도입
    MemberSerivce는 MemberRepositroy 인터페이스에만 의존하면 된다.


이상적

  • 특정 기술에 종속되지 않은, 순수한(only 자바) 인터페이스
  • 이를 기반으로 한, 특정 기술에 종속된 구현체

문제

  • 구현 클래스 메소드에 선언할 수 있는 예외는 부모 타입에서 던진 예외와 같거나 하위 타입이어야 한다. EX) 구현 클래스 메소드에서 'throws SQLException'하기 위해서는, 인터페이스 메소드에 'throws SQLException', 'throws Exception' 등이 선언되어 있어야 함
  • 즉, 구현체가 'throws checked 예외'하기 위해서는 인터페이스에 먼저 'throws checked 예외'가 선언되어 있어야 하는데, 이렇게 되면, 인터페이스는 특정 기술에 종속된다(JDBC 기술에 종속적인 인터페이스).

  1. repository에서 발생하는 checked 예외를 unchecked 예외로 전환해서 servce에 던진다. service는 해당 예외를 무시(throws 선언 x) 할 수 있게 되고, 이에 따라 service layer는 특정 구현 기술에 의존하지 않고, 순수하게 유지(순수 자바 코드로 작성) 될 수 있다.

repo에서 던지는 특정 예외 잡기


  • 등장 배경: 현재 repo에서는 모든 SQLException -> MyDbException으로 전환해서 던지고 있다. service에서는 PK 중복과 같은 특정 SQLException에 대해서는 복구(catch로 예외 잡기) 하고 싶은데, repo에서 넘어오는 MyDbException을 구분할 수 없다. 예를 들어, DB 연결 오류가 원인인 SQLException인지, SQL 문법 오류가 원인인 SQLException인지, PK 중복이 원인인 SQLException인지를 구분하고 싶다.
  • 데이터를 DB에 저장할 때 DB에 동일한 PK가 존재한다면, DB는 JDBC 드라이버에게 오류 코드를 반환한다. 그리고 JDBC 드라이버는 오류 코드(errorCode)를 SQLException에 담아서 던진다.
  • SQLException에 들어있는 errorCode를 활용(e.getErrorCode())하면, DB에서 어떤 문제가 발생했는지 알 수 있다. 예를 들어, H2 DB의 경우, errorCode == 23505라면 PK 중복 오류, 42000이라면 SQL 문법 오류를 의미한다.
  • 참고로, 같은 오류라도 각 DB마다 정의된 오류 코드가 다르다. 예를 들어, PK 중복 오류의 경우, H2 DB는 errorCode가 23505, MySQL DB는 1062이다.

  • repo에서 SQLException의 errorCode 확인 결과,
    23505 -> MyDuplicateKeyException(RuntimeException)
    그 외 -> MyDbException(RuntimeException)
catch (SQLException e) {
	if (e.getErrorCode() == 23505) {
		throw new MyDuplicateKeyException(e);
	}
	throw new MyDbException(e);
}
  • service에서,
    MyDuplicateKeyException를 받으면 -> 잡기(catch)
    MyDbException를 받으면 -> 던지기. 이때, throws 선언 x

남은 문제

  • 동일한 DB 문제라도 DB마다 errorCode가 다르다. 예를 들어, PK 중복 오류의 경우, H2 DB는 errorCode가 23505, MySQL DB는 1062. 즉, DB가 변경되면 errorCode도 변경해야 한다.
  • SQLException의 원인은 정말 다양하다. PK 중복 오류, DB 연결 오류, SQL 문법 오류, lock이 걸린 경우 등 수 십, 수 백 가지의 원인이 있다. catch(SQLException e)에서, 모든 errorCode에 대한 코드를 작성해야 할까?

데이터 접근 예외 추상화(DataAccessException), 예외 변환기

  • 스프링은 SQLException의 errorCode에 맞는 exception을 제공한다.
catch (SQLException e) {
	if (e.getErrorCode() == 23505) { //PK 중복 오류
		throw new RuntimeExceptionA(e); //throw new DuplicateKeyException(e);
	} else if (e.getErrorCode() == 42000) { //SQL 문법 오류
		throw new ExceptionB(e); //throw new BadSqlGrammarException(e);
	}
	throw new MyDbException(e);
}

이전과 같이 errorCode에 따라 발생시킬 RuntimeException를 내가 만들 필요가 없고, 스프링이 제공하는 exception을 사용하면 된다.

  • service, controller에서 예외 잡기(catch)를 할 것이라면, 스프링이 제공하는 이 exception을 사용하면 된다.
  • 각 exception은 특정 기술(JDBC, JPA)에 종속적이지 않다. 따라서 service에서 스프링이 제공하는 exception을 사용해도 문제 없다(service에서 자바 exception을 사용는 것과 동일). 자바가 제공하는 exception, 스프링이 제공하는 exception은 특정 기술에 종속적이지 않음.
  • DataAccessException은 크게 2가지로 구분하는데, Nontransient, Transient이다.
    ⅰ. Nontransient와 하위 exception은 같은 SQL을 다시 실행해도 실패한다.
    예를 들면, SQL 문법 오류, DB 제약 조건 위배
    ⅱ. Transient와 하위 exception은 동일한 SQL을 다시 시도했을 때, 성공할 가능성이 있다. 예를 들면, 쿼리 타임아웃, lock 관련 exception

예외 변환기(SqlExceptionTranslator)

  • 등장 배경: 스프링이 제공하는 exception 덕분에 내가 직접 RuntimeException을 만들 필요가 없다. 그런데, if문으로 직접 errorCode를 확인하고, 하나하나 스프링이 제공하는 exception으로 전환해야 한다. 추가적으로, 같은 오류더라도 DB마다 errorCode가 다르다는 문제점도 있다.
  • 예외 변환기 사용법
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); //예외 변환기
DataAccessException resultEx = exTranslator.translate("", sql, e); //작업명, 실행한 SQL, SQLException

//DataAccessException resultEx = new SQLErrorCodeSQLExceptionTranslator(dataSource).translate("", sql, e);

* DataSource를 넣은 이유는, 어떤 DB를 사용하는지를 알기 위함이다.
* 작업명에는 설명을 넣으면 되는데, 어떤 정해진 문자열이 아니라 내가 원하는 문자열을 넣으면 된다. 
* 내가 유추한, 매개변수에 e를 넣는 이유: (1) e.getErrorCode()하려고 (2) 예외 전활할 때 e를 넣으려고
sql-error-codes.xml (제공되는 파일)

<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>

...

예외 변환기(SQLExceptionTranslator)는 DB 종류, errorCode를 sql-error-codes.xml 파일에 대입해서 어떤 스프링 데이터 접근 예외로 변환할지 찾아낸다. 예를 들어, errorCode가 42122인 경우, BadSqlGrammarException을 반환한다.
(참고. mssql + errorCode 1205는 DeadlockLoserDataAccessException, mysql + errorCode 1205는 CannotAcquireLockException를 반환)

  • 이전에는 다음과 같이 errorCode가 ○일 때, □로 예외 변환을 했었다.
if (e.getErrorCode() == 1) {
	throw new RuntimeExceptionA(e);
} else if (e.getErrorCode() == 2) {
	throw new RuntimeExceptionB(e);
}

예외 변환기를 도입하면서, 특정 errorCode를 넣으면 자동으로 DataAccessException을 반환받을 수 있게 되었다.


JdbcTemplate

  • 등장 배경: repo의 각 메서드를 살펴보면, 상당히 많은 부분이 반복된다.
  • JdbcTemplate을 사용하면 JDBC의 반복 문제를 해결할 수 있는데, JdbcTemplate은 템플릿 콜백 패턴으로 구현되어 있다.

0개의 댓글