Spring/SpringDB

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

느리지만 꾸준하게 2022. 6. 28. 10:03

데이터를 DB에 저장할 때 같은 ID가 이미 데이터베이스에 저장되어 있다면, 데이터베이스는 오류 코드를 반환하고, 이 오류 코드를 받은 JDBC 드라이버는 SQLException 을 던진다. 

 

그리고 SQLException 에는데이터베이스가 제공하는 errorCode 라는 것이 들어있다.

 

 

데이터베이스 오류 코드 그림을 보자.

 

H2  데이터베이스의 키 중복 오류 코드

e.getErrorCode() == 23505
  • SQLException 내부에 들어있는 errorCode를 활용 => 데이터베이스에서 어떤 문제가 발생했는지
    확인할 수 있다.

H2 데이터베이스 예는 아래와 같다.

  • 23505: 키 중복 오류
  • 42000: SQL 문법 오류

 

키 중복 오류 코드(H2 데이터베이스 오류  코드는 여기를 참고

  • H2 DB: 23505
  • MySQL: 1062

 

예외를 변환해서 던지는 과정을 해보자.

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);
    }
}
  • 기존에 사용했던 MyDbException 을 상속받아서 의미있는 계층을 형성한다. 이렇게하면 데이터베이스
    관련 예외라는 계층을 만들 수 있다.
  • 그리고 이름도 MyDuplicateKeyException 이라는 이름을 지었다. 이 예외는 데이터 중복의 경우에만
    던져야 한다.
  • 이 예외는 우리가 직접 만든 것이기 때문에, JDBC나 JPA 같은 특정 기술에 종속적이지 않다. 따라서 이
    예외를 사용하더라도 서비스 계층의 순수성을 유지할 수 있다. (향후 JDBC에서 다른 기술로 바꾸어도 이
    예외는 그대로 유지할 수 있다.)

 

실제 예제 테스트 코드를 만들어 보면 아래와 같다.

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

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.*;

@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 저장 시도
    }

    @Slf4j
    @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={}", 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);
        }
    }








    @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 {
                JdbcUtils.closeStatement(pstmt);
                JdbcUtils.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 을 던진다.

 

리포지토리 부터 중요한 부분을 살펴보면

} 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 로 변환 할 수 있음.
  • 리포지토리 계층이 예외를 변환해준 덕분에 서비스 계층은 특정 기술에 의존하지 않는 yDuplicateKeyException 을 사용해서 문제를 복구하고, 서비스 계층의 순수성도 유지할 수 있음.

 

문제점이 있는데

  • SQL ErrorCode는 각각의 데이터베이스 마다 다르다. 결과적으로 데이터베이스가 변경될 때 마다
    ErrorCode도 모두 변경해야 함.
  • 예) 키 중복 오류 코드
  • H2: 23505
  • MySQL: 1062

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<출처 김영한:스프링 DB 1편 - 데이터 접근 핵심 원리>

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard

 

스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의

www.inflearn.com