스프링 DB 1 정리 - 6. 스프링과 문제 해결 - 예외처리 (22.8.22)

피아노과 개발자도전?·2022년 8월 22일
0

Today I learned

목록 보기
41/75
post-custom-banner

김영한 개발자님의 스프링 DB 1편 강의를 수강하고 정리한 내용입니다.

6. 스프링과 문제 해결 - 예외처리


6.1. 체크 예외와 인터페이스

구현 기술을 쉽게 변경하기 위해서 인터페이스를 도입하더라도 SQLException 과 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야 한다. 하지만 이것은 우리가 원하던 순수한 인터페이스가 아니다.

public class MyDbException extends RuntimeException {

 	public MyDbException(String message) {
 	super(message);
 	}
}

체크 예외를 언체크 예외로 변환하면 해결된다.

public void update(String memberId, int money) {
 	String sql = "update member set money=? where member_id=?";
 	Connection con = null;
 	PreparedStatement pstmt = null;
    
 	try {
 		con = getConnection();
 		pstmt = con.prepareStatement(sql);
 		pstmt.setInt(1, money);
 		pstmt.setString(2, memberId);
 		pstmt.executeUpdate();
        
 	} catch (SQLException e) {
    //`SQLException` 이라는 체크 예외를 
    //`MyDbException` 이라는 런타임 예외로 변환해서 던진다.
 		throw new MyDbException(e);
 	} finally {
 		close(con, pstmt, null);
 	}
}

남은 문제

덕분에 JDBC에서 다른 구현 기술로 변경하더라도 서비스 계층의 코드를 변경하지 않고 유지할 수 있다.

리포지토리에서 넘어오는 특정한 예외의 경우 복구를 시도할 수도 있다. 그런데 지금 방식은 항상 MyDbException 이라는 예외만 넘어오기 때문에 예외를 구분할 수 없는 단점이 있다. 만약 특정 상황에는 예외를 잡아서 복구하고 싶으면 예외를 어떻게 구분해서 처리할 수 있을까?


6.2. 데이터 접근 예외 직접 만들기

데이터베이스 오류에 따라서 특정 예외는 복구하고 싶을 수 있다.
예를들어, ID를 hello 라고 가입 시도 했는데, 이미 같은 아이디가 있으면 hello12345 와 같이 뒤에 임의의 숫자를 붙여서 가입하는 것이다.

기존에 사용했던 MyDbException 을 상속받아서 MyDuplicateKeyException를 만든다. 이 예외는 우리가 직접 만든 것이기 때문에, JDBC나 JPA 같은 특정 기술에 종속적이지 않다.

public class MyDuplicateKeyException extends MyDbException {

 	public MyDuplicateKeyException(String message) {
 		super(message);
	}
}

중간에 예외를 잡아서 복구해준다.

//리포지토리 변경부분
catch (SQLException e) {
 //h2 db
//SQLException 내부에 들어있는 errorCode 를 활용하면 
// 데이터베이스에서 어떤 문제가 발생했는지 확인할 수 있다.
 	if (e.getErrorCode() == 23505) {
 		throw new MyDuplicateKeyException(e);
 	}
 		throw new MyDbException(e);
}
//서비스계층
static class Service {
 	private final Repository repository;
 	public void create(String memberId) {
    
 	try {
 		repository.save(new Member(memberId, 0));
 		log.info("saveId={}", memberId);
        
 	} catch (MyDuplicateKeyException e) {
//예외를 잡아서 generateNewId(memberId) 로 새로운 ID 생성을 시도한다. 그리고 다시 저장한다. 
//여기가 예외를 복구하는 부분이다.
 		log.info("키 중복, 복구 시도");
 		String retryId = generateNewId(memberId);
 		log.info("retryId={}", retryId);
 		repository.save(new Member(retryId, 0));
        
 	} catch (MyDbException e) {
 		log.info("데이터 접근 계층 예외", e);
 		throw e;
 	}

 	private String generateNewId(String memberId) {
 		return memberId + new Random().nextInt(10000);
 	}
}

남은 문제

리포지토리 계층이 예외를 변환해준 덕분에 서비스 계층은 특정 기술에 의존하지 않는 MyDuplicateKeyException 을 사용해서 문제를 복구하고, 서비스 계층의 순수성도 유지할 수 있었다.

하지만 SQL ErrorCode는 각각의 데이터베이스 마다 다르다. 결과적으로 데이터베이스가 변경될 때 마다ErrorCode도 모두 변경해야 한다.

6.3. 스프링 예외 추상화 이해

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

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

  • 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.

  • Transient 는 일시적이라는 뜻이다. Transient 하위 예외는 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있다.
    예를 들어서 쿼리 타임아웃, 락과 관련된 오류들이다. 이런 오류들은 데이터베이스 상태가 좋아지거나, 락이 풀렸을 때 다시 시도하면 성공할 수 도 있다.

  • NonTransient 는 일시적이지 않다는 뜻이다. 같은 SQL을 그대로 반복해서 실행하면 실패한다. SQL 문법 오류, 데이터베이스 제약조건 위배 등이 있다.


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

스프링이 제공하는 SQL 예외 변환기는 다음과 같이 사용하면 된다.

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

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

SQL 문법이 잘못되었으면 BadSqlGrammarException 을 반환한다. 눈에 보이는 반환 타입은 최상위 타입인 DataAccessException 이지만 실제로는 BadSqlGrammarException 예외가 반환된다.


각각의 DB마다 SQL ErrorCode는 다르다. 그런데 스프링은 어떻게 각각의 DB가 제공하는 SQL ErrorCode까지 고려해서 예외를 변환할 수 있을까?

org.springframework.jdbc.support.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>

스프링 SQL 예외 변환기는 SQL ErrorCode를 이 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다.

결론적으로 서비스, 컨트롤러 계층에서 예외 처리가 필요하면 특정 기술에 종속적인 SQL Exception 같은 예외를 직접 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.


6.4. JDBC 반복 문제 해결 - JdbcTemplate

JDBC 반복 문제

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

이런 반복을 효과적으로 처리하는 방법이 템플릿 콜백 패턴이다.
스프링은 JDBC의 반복 문제를 해결하기 위해 JdbcTemplate 이라는 템플릿을 제공한다.

public void update(String memberId, int money) {
	String sql = "update member set money=? where member_id=?";
 	Connection con = null;
 	PreparedStatement pstmt = null;
    
 	try {
 		con = getConnection();
 		pstmt = con.prepareStatement(sql);
 		pstmt.setInt(1, money);
 		pstmt.setString(2, memberId);
 		pstmt.executeUpdate();
 	} catch (SQLException e) {
 		throw exTranslator.translate("update", sql, e);
 	} finally {
 		close(con, pstmt, null);
 	}
}

이랬던 코드를

public class MemberRepositoryV5 implements MemberRepository {

 	private final JdbcTemplate template;
    
 	public MemberRepositoryV5(DataSource dataSource) {
 		template = new JdbcTemplate(dataSource);
 	}
    
    @Override
 	public void update(String memberId, int money) {
 		String sql = "update member set money=? where member_id=?";
 		template.update(sql, money, memberId);
 	}
}

이렇게 바꿀 수 있다.

JdbcTemplate 은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다. 그 뿐만 아니라 지금까지 학습했던, 트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다.


6.5. 정리

  • 트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 최대한 유지하면서 서비스 계층에서 트랜잭션을 사용할 수 있다.

  • 스프링이 제공하는 예외 추상화와 예외 변환기 덕분에, 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지하면서 예외도 사용할 수 있다.

  • 서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도 서비스 계층을 순수하게 유지할 수 있다.

  • 리포지토리에서 JDBC를 사용하는 반복 코드가 JdbcTemplate 으로 대부분 제거되었다

profile
공부한 내용 정리
post-custom-banner

0개의 댓글