-
[김영한 스프링] 23. 자바 예외 이해 - 데이터 접근 예외 직접 만들기Spring/스프링 DB 1편 - 데이터 접근 핵심 원리 2024. 1. 2. 22:14
데이터 접근 예외 직접 만들기
데이터베이스 오류에 따라서 특정 예외는 복구하고 싶을 수 있다.
예를 들어서 회원 가입 시 DB에 이미 같은 ID가 있으면 ID 뒤에 숫자를 붙여서 새로운 ID를 만들어야 한다고 가정해 보자.
ID를 hello라고 가입 시도 했는데, 이미 같은 아이디가 있으면 hello12345와 같이 뒤에 임의의 숫자를 붙여서 가입하는 것이다.
데이터를 DB에 저장할 때 같은 ID가 이미 데이터베이스에 저장되어 있다면, 데이터베이스는 오류 코드를 반환하고, 이 오류 코드를 받은 JDBC 드라이버는 SQLException을 던진다. 그리고 SQLException에는 데이터베이스가 제공하는 errorCode라는 것이 들어있다.
데이터베이스 오류 코드 그림
H2 데이터베이스의 키 중복 오류 코드
e.getErrorCode() == 23505
SQLException 내부에 들어있는 errorCode를 활용하면 데이터베이스에서 어떤 문제가 발생했는지 확인할 수 있다.
H2 데이터베이스 예
- 23505 : 키 중복 오류
- 42000 : SQL 문법 오류
참고로 같은 오류여도 각각의 데이터베이스마다 정의된 오류 코드가 다르다. 따라서 오류 코드를 사용할 때는 데이터베이스 메뉴얼을 확인해야 한다.
예) 키 중복 오류 코드
- H2 DB : 23505
- MySQL : 1062
H2 데이터베이스 오류 코드 참고
https://www.h2database.com/javadoc/org/h2/api/ErrorCode.html서비스 계층에서는 예외 복구를 위해 키 중복 오류를 확인할 수 있어야 한다. 그래야 새로운 ID를 만들어서 다시 저장을 시도할 수 있기 때문이다. 이러한 과정이 바로 예외를 확인해서 복구하는 과정이다. 리포지토리는 SQLException을 서비스 계층에 던지고 서비스 계층은 이 예외의 오류 코드를 확인해서 키 중복 오류(23505)인 경우 새로운 ID를 만들어서 다시 저장하면 된다.
그런데 SQLException에 들어있는 오류 코드를 활용하기 위해 SQLException을 서비스 계층으로 던지게 되면, 서비스 계층이 SQLException이라는 JDBC 기술에 의존하게 되면서, 지금까지 우리가 고민했던 서비스 계층의 순수성이 무너진다.
이 문제를 해결하려면 앞서 배운 것처럼 리포지토리에서 예외를 변환해서 던지면 된다.
SQLException -> MyDuplicateKeyException
MyDuplicateKeyException
package hello.jdbc.repository.ex; public class MyDuplicateKeyException extends MyDbException { public MyDuplicateKeyException() { } public MyDuplicateKeyException(String message) { super(message); } public MyDuplicateKeyException(String message, Throwable cause) { super(message, cause); } public MyDuplicateKeyException(Throwable cause) { super(cause); } }
main/java/hello/jdbc/repository/MyDuplicateKeyException 생성
- 기존에 사용했던 MyDbException을 상속받아서 의미 있는 계층을 형성한다. 이렇게 하면 데이터베이스 관련 예외라는 계층을 만들 수 있다.
- 그리고 이름도 MyDuplicateKeyException이라는 이름을 지었다. 이 예외는 데이터 중복의 경우에만 던져야 한다.
- 이 예외는 우리가 직접 만든 것이기 때문에, JDBC나 JPA 같은 특정 기술에 종속적이지 않다. 따라서 이 예외를 사용하더라도 서비스 계층의 순수성을 유지할 수 있다. (향후 JDBC에서 다른 기술로 바꾸어도 이 예외는 그대로 유지할 수 있다.)
ExTranslatorV1Test
test/java/hello/jdbc/exception/translator/ExTranslatorV1Test 생성
package hello.jdbc.exception.translator; import hello.jdbc.domain.Member; import hello.jdbc.repository.ex.MyDbException; import hello.jdbc.repository.ex.MyDuplicateKeyException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.jdbc.datasource.DriverManagerDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Random; import static hello.jdbc.connection.ConnectionConst.*; import static org.springframework.jdbc.support.JdbcUtils.closeConnection; import static org.springframework.jdbc.support.JdbcUtils.closeStatement; @Slf4j public class ExTranslatorV1Test { Repository repository; Service service; @BeforeEach void init() { DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD); repository = new Repository(dataSource); service = new Service(repository); } @Test void duplicateKeySave() { service.create("myId"); service.create("myId"); // 같은 ID 저장 시도 } @RequiredArgsConstructor 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) { log.info("키 중복, 복구 시도"); String retryId = generateNewId(memberId); log.info("retryId={}", memberId); 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); } } @RequiredArgsConstructor static class Repository { private final DataSource dataSource; public Member save(Member member) { String sql = "insert into member(member_id, money) values (?, ?)"; Connection con = null; PreparedStatement pstmt = null; try { con = dataSource.getConnection(); pstmt = con.prepareStatement(sql); pstmt.setString(1, member.getMemberId()); pstmt.setInt(2, member.getMoney()); pstmt.executeUpdate(); return member; } catch (SQLException e) { // h2 db if (e.getErrorCode() == 23505) { throw new MyDuplicateKeyException(e); } throw new MyDbException(e); } finally { closeStatement(pstmt); closeConnection(con); } } } }
로그
같은 ID를 저장했지만, 중간에 예외를 잡아서 복구한 것을 확인할 수 있다.
실행
리포지토리 부터 중요한 부분을 살펴보자.
catch (SQLException e) { //h2 db if (e.getErrorCode() == 23505) { throw new MyDuplicateKeyException(e); } throw new MyDbException(e); }
- e.getErrorCode() == 23505 : 오류 코드가 키 중복 오류(23505)인 경우 MyDuplicateKeyException을 새로 만들어서 서비스 계층에 던진다.
- 나머지 경우 기존에 만들었던 MyDbException을 던진다.
서비스의 중요한 부분을 살펴보자.
try { repository.save(new Member(memberId, 0)); log.info("saveId={}", memberId); } catch (MyDuplicateKeyException e) { log.info("키 중복, 복구 시도"); String retryId = generateNewId(memberId); log.info("retryId={}", retryId); repository.save(new Member(retryId, 0)); } catch (MyDbException e) { log.info("데이터 접근 계층 예외", e); throw e; }
- 처음에 저장을 시도한다. 만약 리포지토리에서 MyDuplicateKeyException 예외가 올라오면 이 예외를 잡는다.
- 예외를 잡아서 generateNewId(memberId)로 새로운 ID 생성을 시도한다. 그리고 다시 저장한다. 여기가 예외를 복구하는 부분이다.
- 만약 복구할 수 없는 예외(MyDbException)면 로그만 남기고 다시 예외를 던진다.
- 참고로 이 경우 여기서 예외 로그를 남기지 않아도 된다. 어차피 복구할 수 없는 예외는 예외를 공통으로 처리하는 부분까지 전달되기 때문이다. 따라서 이렇게 복구할 수 없는 예외는 공통으로 예외를 처리하는 곳에서 예외 로그를 남기는 것이 좋다. 여기서는 다양하게 예외를 잡아서 처리할 수 있는 점을 보여주기 위해 이곳에 코드를 만들어두었다.
정리
- SQL ErrorCode로 데이터베이스에 어떤 오류가 있는지 확인할 수 있었다.
- 예외 변환을 통해 SQLException을 특정 기술에 의존하지 않는 직접 만든 예외인 MyDuplicateKeyException로 변환할 수 있었다.
- 리포지토리 계층이 예외를 변환해준 덕분에 서비스 계층은 특정 기술에 의존하지 않는 MyDuplicateKeyException을 사용해서 문제를 복구하고, 서비스 계층의 순수성도 유지할 수 있었다.
남은 문제
- SQL ErrorCode는 각각의 데이터베이스마다 다르다. 결과적으로 데이터베이스가 변경될 때마다 ErrorCode도 모두 변경해야 한다.
- 예) 키 중복 오류 코드
- H2 : 23505
- MySQL : 1062
- 예) 키 중복 오류 코드
- 데이터베이스가 전달하는 오류는 키 중복뿐만 아니라 락이 걸린 경우, SQL 문법에 오류 있는 경우 등등 수십 수백가지 오류 코드가 있다. 이 모든 상황에 맞는 예외를 지금처럼 다 만들어야 할까? 추가로 앞서 이야기한 것처럼 데이터베이스마다 이 오류 코드는 모두 다르다.
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1
'Spring > 스프링 DB 1편 - 데이터 접근 핵심 원리' 카테고리의 다른 글
[김영한 스프링] 25. 자바 예외 이해 - JDBC 반복 문제 해결(JdbcTemplate) (1) 2024.01.03 [김영한 스프링] 24. 자바 예외 이해 - 스프링 예외 추상화 (1) 2024.01.03 [김영한 스프링] 22. 자바 예외 이해 - 체크 예외와 인터페이스 & 런타임 예외 적용 (2) 2024.01.02 [김영한 스프링] 21. 자바 예외 이해 - 예외 포함과 스택 트레이스 (0) 2024.01.01 [김영한 스프링] 20. 자바 예외 이해 - 체크, 언체크 예외 활용 (2) 2024.01.01