최상위 예외 객체인 Throwable을 상속받는 Exception과 Error 중에, 잡아야 하는 예외는 Exception 이다. Error는 잡으려고 하면 안되기 때문이다.
Exception은 체크 예외 (컴파일 시점에서 체크하는), RuntimeException은 언체크 예외 (컴파일 시점에 체크하지 않는)
체크 예외
public class CheckedAppTest {
@Test
void checked() {
Controller controller = new Controller();
Assertions.assertThatThrownBy(controller::request)
.isInstanceOf(Exception.class);
}
static class Controller {
Service service = new Service();
public void request() throws SQLException, ConnectException {
service.login();
}
}
static class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void login() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
static class NetworkClient {
public void call() throws ConnectException {
throw new ConnectException("Connection refused");
}
}
static class Repository {
public void call() throws SQLException {
throw new SQLException("SQL Ex");
}
}
}
코드를 보면 알 수 있듯이, 체크 예외는 복구할 수 없기에 모든 계층에서 예외를 처리하지 못하고 던질 수 밖에 없다.
그렇기 때문에 각 계층에서는 관련 함수에 throws 구문을 선언해야 하며, 이 과정에서 SQLException 처럼 특정 기술에 의존하는 코드가 작성되는 문제가 발생한다.
즉, 처리할 수 없는 예외를 각 계층에서 던지기 위해 불필요한 의존관계 문제가 발생하는 것이다.
이러한 체크 예외들을 Exception 최상위 객체로 치환해서 던지는 것은, 특정 중요한 체크 예외를 처리하지 못하고 그냥 던져 버리는 안티 패턴이므로 지양하자.
이러한 체크 예외는 계좌 이체 실패 예외 등 비즈니스 로직상 의도적으로 무조건 잡아서 처리해야 하는 문제일 때만사용하는 방식으로 이용한다. 즉, 컴파일러를 통해 미리 놓친 예외를 알 수 있는 것이다.
언체크 예외
@Slf4j
public class UncheckedAppTest {
@Test
void unchecked() {
Controller controller = new Controller();
Assertions.assertThatThrownBy(controller::request)
.isInstanceOf(RuntimeException.class);
}
static class Controller {
Service service = new Service();
public void request() {
service.logic();
}
}
static class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() {
repository.call();
networkClient.call();
}
}
static class NetworkClient {
public void call() {
throw new RuntimeConnectException("연결 실패");
}
}
static class Repository {
public void call() {
try {
runSql();
}
catch (SQLException e) {
throw new RuntimeSqlException(e);
}
}
public void runSql() throws SQLException {
throw new SQLException("ex");
}
}
static class RuntimeConnectException extends RuntimeException {
public RuntimeConnectException(String message) {
super(message);
}
}
static class RuntimeSqlException extends RuntimeException {
public RuntimeSqlException(Throwable cause) {
super(cause);
}
}
}
체크 예외를 언체크 예외로 바꿔서 던지면, 이제 각 계층은 예외에 의존관계를 맺지 않아도 된다.
기술이 바뀌더라도, 전역 예외 처리기 등의 예외 공통 처리 모듈만 신경 쓰면 된다.
로그를 출력할 때, 마지막 파라미터에 예외를 추가하면 로그에 스택 트레이스를 출력할 수 있다.
언체크 예외가 어떤 체크 예외에 의해 발생했는지를 포함해서 로그로 출력하려면, 기존 예외를 포함해서 던져주어야 한다.
스프링 데이터 접근 계층 예외
스프링 데이터 접근 예외 계층
데이터 접근 예외는 데이터베이스마다 다르다. 따라서 스프링은 이를 추상화해서 제공한다.
@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(?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.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(conn, pstmt, null);
}
}
@Override
public Member findById(String memberId) {
String sql = "select * from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
throw exTranslator.translate("findById", sql, e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public void update(String memberId, int money) {
String sql = "update member set money = ? where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}" , resultSize);
} catch (SQLException e) {
throw exTranslator.translate("update", sql, e);
}
}
@Override
public void delete(String memberId) {
String sql = "delete from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}" , resultSize);
} catch (SQLException e) {
throw exTranslator.translate("delete", sql, e);
}
}
private void close(Connection conn, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 함.
DataSourceUtils.releaseConnection(conn, dataSource);
}
private Connection getConnection() {
// 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 함.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
따라서 스프링이 제공하는 데이터 접근 예외를 사용하면, 별도로 DB 예외를 따로 만들어서 변환할 필요가 없다.
JdbcTemplate
@Slf4j
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
@Override
public Member findById(String memberId) {
String sql = "select * from member where member_id = ?";
return template.queryForObject(sql, memberRowMapper(), memberId);
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
};
}
@Override
public void update(String memberId, int money) {
String sql = "update member set money = ? where member_id = ?";
template.update(sql, money, memberId);
}
@Override
public void delete(String memberId) {
String sql = "delete from member where member_id = ?";
template.update(sql, memberId);
}
}
JDBC의 커넥션 조회, 커넥션 동기화, PreparedStatement 생성 및 파라미터 바인딩, 쿼리 실행, 결과 바인딩, 예외 발생 시 예외 변환기 실행, 리소스 종료 등 계속 반복되는 코드 문제를 JdbcTemplate를 사용해서 해결할 수 있다.