스프링/스프링 기초DB

스프링 예외 추상화

chanhee01 2023. 3. 19. 17:00

예외가 발생했을 때 해당 예외를 그대로 반환하는 것이 아니라 런타임 예외를 따로 만들어서 해당 예외를 반환하는 것이 효율적이다.

 

@Test
void duplicateKeySave() {
    service.create("myId");
    service.create("myId"); // 같은 아이디 저장 시도
}

@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);
        }

    }
}

같은 아이디를 반복 저장하는 상황이다. service에서 반복되는 아이디를 저장한다는 요청이 들어오면 random으로 인해 10000까지의 숫자를 원래 아이디 뒤에 붙여준다. 이 때 에러코드가 23505일 때 MyDuplicateKeyException(e)로 반환하여서 중복 여부를 판단할 수 있다. 이처럼 특별한 예외가 발생했을 때에는 해당 예외처리를 할 수 있는 런타임 예외를 만들어서 반환해주면 된다. 하지만 에러 코드도 db마다 다르고 모든 예외를 저렇게 처리할 수 없으니 스프링에서 지원하는 예외 추상화를 사용하는 것이 좋다.

 

 

 

 

스프링 예외 추상화

@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);

        SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
        DataAccessException resultEx = exTranslator.translate("select", sql, e);
        // 스프링이 제공하는 SQL 예외 변환기
        
        log.info("resultEx", resultEx);
        assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
    }
}

SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);

DataAccessException resultEx = exTranslator.translate("select", sql, e);

 

위의 두 줄은 스프링이 제공하는 SQL 예외 변환기이다. 문법 오류가 발생하면 ErrorCode가 42122로 발생하는데 이러한 예외코드들을 db별로 저장해두어서 해당 예외가 발생했을 때 알맞은 예외를 반환해준다. db별로 의존하지도 않기 때문에 추후에 db를 바꿔준다해도 별도의 코드 수정을 최소화로 할 수 있다. 위의 코드는 실제 코드가 아니라 스프링 예외 추상화만을 보기 위한 테스트코드이다.

 

 

 

 

 

@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 Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?, ?)";
        // (?, ?)가 아니라 (memberId, money)라 하면 보안상의 위험

        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 exTranslator.translate("save", sql, e);
        } finally {
            close(con, pstmt, null);
            // 항상 close가 되도록 finally에서 호출해야 함
        }

    }

원래 코드인 save에 exTranslator.translate 메서드를 통해 스프링 예외 추상화를 진행했다.