[인프런 김영한] 자바 예외

Jong Hwan
|2026. 2. 8. 13:10

예외 계층

예외 계층 그림

 

  • 최상위 예외 객체인 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를 사용해서 해결할 수 있다.
  • 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동으로 실행해준다.