본문 바로가기

Web Sever 개발과 CS 기초/스프링

Spring을 사용한 JDBC 예외 누수 문제 해결하기

관련 내용

해당 프로젝트 깃허브

 

<JDBC, Spring - Transaction 처리에 대한 완벽한 이해>

  • 순수 JDBC만을 사용한 Transaction 사용 방법 알아보기

JDBC를 사용한 Trasaction 처리와 이해

  • 순수 JDBC-Transaction 문제 Spring TransactionManger로 해결하기 - 트랜잭션 템플릿, 트랜잭션 AOP 사용

JDBC Trasaction문제 Spring으로 해결하기

  • Spring Transaction AOP 동작 과정과 주의 사항 알아보기

스프링 Trasaction AOP 동작 과정과 주의 사항

  • Spring Transaction 전파 원리 이해하기

Spring Transaction 전파 이해하기

  • (현재 글)Spring을 사용한 JDBC 예외 누수 문제 해결하기

Spring을 사용한 JDBC 예외 누수 문제 해결하기

  • 자바 예외에 대한 이해

자바 예외에 대한 이해

개요 목적

Repository 인터페이스를 만들어, JDBC, JPA 어떤 기술을 사용하든 같은 코드를 사용할 수 있도록 하려 한다.

그런데, JDBC의 체크 예외 때문에, 인터페이스에도 해당 예외를 포함해야 한다. 그래서 JPA 구현체는 만들 수 없는 인터페이스가 되어 버렸다.

package hello.jdbc.repository;
import hello.jdbc.domain.Member;
//인터페이스에 JDBC 종속적인 체크 예외를 사용하게 되서, JPA 구현체를 만들 수 가 없다.
import java.sql.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;
}

그래서 스프링의 도움을 받아 체크 예외를 언 체크 예외를 만들고, 어떤 기술이든 구현체를 만들 수 있는 레포지토리 인터페이스 만드는 방법에 대해 알아보자.

체크 예외와 언 체크 예외 등 자바의 예외에 대한 이해는 아래 블로그 글에서 확인할 수 있다.

자바 예외에 대한 이해

예외 누수를 해결하고 레포지토리 인터페이스 만드는 과정

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\MemberRepository.java 이 인터페이스로 레포지토리를 추상화 했다. 그래서 JDBC 기술로 구현한 코드를 사용하다가 JPA로 변경되어도 인터페이스 틀은 같기 때문에, 서비스 계층에는 코드를 변경하지 않아도 된다.

public interface MemberRepository {

    Member save(Member member);

    Member findById(String memberId);

    void update(String memberId, int money);

    void delete(String memberId);
}

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\ex\MyDbException.java 이 런타임 예외를 만들어, 레포지토리에 발생하는 JDBC 종속적인 SQLException을 런타임 예외로 변경했다. 그래서 서비스에서 throws를 작성하지 않아도 예외를 밖으로 던질 수 있게 된다.

//예외 변환
catch (SQLException e) {
	 throw new MyDbException(e);
}

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\MemberRepositoryV4_1.java 체크 예외를 런타임 예외로 변경하고 인터페이스를 구현해서 기술에 종속적이지 않는 레포지토리 클래스를 구현했다.

/**
 * 예외 누수 문제 해결 체크 예외를 런타임 예외로 변경 MemberRepository 인터페이스 사용 
 * throws SQLException 제거
 */
public class MemberRepositoryV4_1 implements MemberRepository {

    private final DataSource dataSource;

    public MemberRepositoryV4_1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values(?, ?)";
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            throw new MyDbException(e);
        } finally {
            close(con, pstmt, null);
        }
    }
	...
}

스프링 도움으로 스프링 전용 예외 만들기

같은 SQLException이라도, 그 안에는 데이터 베이스가 제공하는 errorCode가 들어 있다. 에러 코드를 들여다 보면 데이터베이스에 정확히 어떤 문제가 발생 했는 지 확인할 수 있다.

예) 키 중복 오류 코드
H2 DB: 23505
MySQL: 1062

그런데 SQLException 에 들어있는 오류 코드를 활용하기 위해(좀 더 자세한 예외 코드를 사용하기 위해) SQLException 을 서비스 계층으로 던지게 되면, 서비스 계층이 SQLException 이라는 JDBC 기술에 의존하게 되면서, 지금까지 우리가 고민했던 서비스 계층의 순수성이 무너진다.

이 문제를 해결하기 위해서 리포지토리에서 errorCode 별 예외를(e.getErrorCode() == 23505 이런 방식으로) 변환해서 던지면 된다.

//이런 식으로 예외 코드를 잡아서 변환하면 된다. 
catch (SQLException e) {
	 //h2 db
	 if (e.getErrorCode() == 23505) {
	 throw new MyDuplicateKeyException(e);
	 }
	 throw new MyDbException(e);
}

그러나, SQL ErrorCode는 각각의 데이터베이스 마다 다르다. 데이터 베이스가 변경될 때마다 값을 수정해야 하는 문제가 발생한다.

이 문제를 해결해줄 스프링 예외 추상화에 대해서 알아보자.

스프링 예외 추상화

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

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

스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다. 그리고 데이터베이스마다 오류 코드가 다르다는 점도 해결해준다.

스프링이 제공하는 예외 변환기 SQLExceptionTranslator를 사용해서 스프링 정의 예외로 변환하는 법을 알아보자. KIMHWANG\jdbc\src\test\java\hello\jdbc\repository\SpringExceptionTranslatorTest.java

@Slf4j
public class SpringExceptionTranslatorTest {

    DataSource dataSource;
    @BeforeEach
    void init() {
        dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }

    @Test
    void exceptionTranslator() {
        String sql = "select bad grammar";
        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeQuery();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);

            //dataSource 파라미터를 넣어 트랜스 레이터 생성
            //org.springframework.jdbc.support.sql-error-codes.xml
            //도움을 받아서 DB마다 에러코드가 달라도 알맞은 스프링 예외를 반환
            SQLExceptionTranslator exTranslator = new
                SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException resultEx = exTranslator.translate("select", sql,
                e);

            //org.springframework.jdbc.BadSqlGrammarException를 생성
            log.info("resultEx", resultEx);
            assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
        }
    }
}

스프링 예외 추상화 적용

모든 예외를 스프링 전용 예외로 바꾸는 코드는 깃허브에서 확인할 수 있다. 여기서는 update 메소드만 확인해서 Repository에 exTranslate를 어떻게 사용했는 지 확인해보자.

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\MemberRepositoryV4_2.java

@Slf4j
public class MemberRepositoryV4_2 implements MemberRepository {

    private final DataSource dataSource;
    private final SQLExceptionTranslator exTranslator;

    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }
    @Override
    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) {
	    //이런식으로 스프링의 DataAccessException으로 변환한 후 throw해준다.
            throw exTranslator.translate("update", sql, e);
        
	} finally {
            close(con, pstmt, null);
        }
    }
}

결과적으로 @Repository 어노테이션을 사용하면 된다.

@Repository를 클래스 위에서 선언하게 되면 메소드에서 발생할 수 있는 unchecked exception들을 스프링의 DataAccessException으로 자동 변환해준다.

그래서 단순히 @Repository 어노테이션을 붙여서 예외에 대한 생각을 멈출 수 있다.