Develop/Java

[Java] 사라진 SQLException

연로그 2022. 4. 7. 16:58
반응형

😏 서론

이번에 과제를 진행하면서 PreparedStatement를 이용해 DB 데이터를 가져왔다. 코드를 작성하다보니 자연스럽게 try - catch를 통해 SQLException를 처리하는 코드가 필수적으로 추가되었다. 여기서 한가지 의문점이 들었던 건, 예전에 만들었던 프로젝트들에서도 DB와 연동하는 프로젝트들은 정말 많았으나, SQLException을 따로 처리한 기억이 없었다. 어떤 차이가 있는가... SQLException은 어디서 처리됐는지, 왜 사라졌는지에 대한 이야기를 다뤄보려고 한다.

 

🤩 SQLException이란?

  • 데이터베이스 접근 또는 다른 에러에 대한 정보를 제공하는 예외
  • Checked Exception

 

🔻 Checked Exception vs Unchecked Exception

더보기

 

Java에서 예외는 크게 Error와 Exception으로 나뉜다.

  • Error: 시스템 레벨에서 발생하는 심각한 수준의 오류
  • Exception: 로직 상에서 발생하는 오류. 개발자가 예외를 예측해 별도로 처리 가능.

 

Exception은 크게 Unchecked Exception과 Checked Exception으로 나뉜다.

 

  Checked Exception Unchecked Exception
RuntimeException 상속 X O
예외 처리 여부 예외 처리 코드 필수 필수는 아님
확인 시점 컴파일 단계 런타임 단계
예제 IOException
SQLException
NullPointerException
IndexOutOfBoundException

 

 보통 둘의 차이점에 대해 공부할 때 트랜잭션에 대한 이야기가 많은데 알 수 없는 말이다.😅 트랜잭션이라는 단어만 보면 어떤 것이라고 정의내리기 애매하다. DB 트랜잭션도 있고 메시지 큐 트랜잭션도 있고 굉장히 다양하다. 만약 DB 트랜잭션을 의미하는거라면 Exception에 대한 예외 처리는 개발자가 정하지 Exception 종류에 따라 갈리지 않는다. 물론 별도로 처리를 하지 않는 경우 특정 프레임워크에서는 Checked Exception 발생 시 커밋 되는게 디폴트~ 같은 옵션이 존재할 수는 있겠다.

 

💥 SQLException, 사라지다?

 유니크한 값인 ID가 중복되어서 SQLException이 발생하는 경우 어떻게 복구해야 할까? 유저가 입력했던 ID에 난수를 더해 insert 시키는 방법이라던가 여러 방법을 사용할 수 있을 것이다. 하지만 가장 일반적인 방법은 RuntimeException을 발생시키고 입력을 다시 요청하는 것이다.

 

 중요한 포인트는 Exception을 발생시킬 때 어떤 예외가 발생했는지 정보를 전달해줘야 한다는 것이다. Checked Exception을 만나면 더 구체적인 Unchecked Exception을 발생시켜 정확한 정보를 전달하고 로직의 흐름을 끊는 것이 좋다. Spring이나 JPA 등에서 SQLException을 처리하지 않는 이유도 적절한 RuntimeException으로 던져주고 있기 때문이다.

 

 이건 주관적인 의견인데 Checked Exception의 경우 호출하는 메서드마다  throws Exception 을 명시해야하는게 번거로웠다. DAO에서 SQLException이 발생하면 DAO를 호출하는 Service와 Service를 호출하는 Controller와 Controller를 호출하는 main메서드에서도 선언을 해줘야했다. 😅

 

Spring의 JdbcTemplate 안에서는 모든 SQLException을 DataAccessException으로 던진다.

호출하는 클래스에서는 필요할 때 DataAccessException을 잡아서 처리하면 된다.

 

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    // ...
    
    @Override
    @Nullable
    public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
        // ...
        try {
            // ...
        }
        catch (SQLException ex) {
            // ...
            throw translateException("ConnectionCallback", sql, ex);
        }
        finally {
            // ...
        }
    }
    
    protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
        DataAccessException dae = getExceptionTranslator().translate(task, sql, ex);
        return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
    }
}

 

🔻 DataAccessException 코드

더보기
/**
 * Root of the hierarchy of data access exceptions discussed in
 * <a href="https://www.amazon.com/exec/obidos/tg/detail/-/0764543857/">Expert One-On-One J2EE Design and Development</a>.
 * Please see Chapter 9 of this book for detailed discussion of the
 * motivation for this package.
 *
 * <p>This exception hierarchy aims to let user code find and handle the
 * kind of error encountered without knowing the details of the particular
 * data access API in use (e.g. JDBC). Thus it is possible to react to an
 * optimistic locking failure without knowing that JDBC is being used.
 *
 * <p>As this class is a runtime exception, there is no need for user code
 * to catch it or subclasses if any error is to be considered fatal
 * (the usual case).
 *
 * @author Rod Johnson
 */
@SuppressWarnings("serial")
public abstract class DataAccessException extends NestedRuntimeException {

	/**
	 * Constructor for DataAccessException.
	 * @param msg the detail message
	 */
	public DataAccessException(String msg) {
		super(msg);
	}

	/**
	 * Constructor for DataAccessException.
	 * @param msg the detail message
	 * @param cause the root cause (usually from using a underlying
	 * data access API such as JDBC)
	 */
	public DataAccessException(@Nullable String msg, @Nullable Throwable cause) {
		super(msg, cause);
	}
}
  • JDBC의 내용을 모르는 상태로 발생하는 오류를 찾고 이를 처리할 수 있도록 하기 위해 사용
  • JDBC가 사용되고 있는지를 몰라도 optimistic lock 실패에 반응
    (Optimistic Lock: 버전 정보를 이용해 업데이트를 처리하는 방법)
  • 런타임 예외. 오류가 치명적이라고 간주되면 사용자 측에서 해당 클래스를 처리할 필요 없음

 

😎 Checked Exception 처리하기

현재는 Spring을 사용하지 않을거라 JdbcTemplate을 사용하진 못한다.

그렇다면 Checked Exception을 어떻게 처리할지에 대해 고민해보았다.

 

예외를 처리하는 방법에 대해서는 크게 3가지로 나뉜다.

  1. 예외 복구: 다른 작업 흐름으로 유도
  2. 예외처리 회피: throw를 통해 호출한 쪽에서 예외 처리하도록
  3. 예외 전환: 명확한 의미의 예외로 전환하여 throw

 

나는 JdbcTemplate처럼 예외 전환을 해보려고 한다.

PieceDao에는 아래와 같은 메서드가 존재한다.

public void delete(String position) {
    String sql = "delete from piece where coordinate = ?";

    try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
        preparedStatement.setString(1, position);
        preparedStatement.executeUpdate();

    } catch (SQLException e) {
        e.printStackTrace();
    }
}

 

SQLException이 발생하면 콘솔에 에러 상황을 프린트만 해두고 별도로 처리하지 않는다.

해당 메서드를 호출하는 곳에서 쿼리문이 잘 실행됐는지 안됐는지 잘 모른다.

 

public class DataAccessException extends RuntimeException {

    private final static String MESSAGE = "쿼리가 정상적으로 실행되지 않았습니다.";

    public DataAccessException() {
        super(MESSAGE);
    }

    public DataAccessException(String message) {
        super(message);
    }
}
public void delete(String position) {
    String sql = "delete from piece where coordinate = ?";

    try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
        preparedStatement.setString(1, position);
        preparedStatement.executeUpdate();

    } catch (SQLException e) {
        e.printStackTrace();
        throw new DataAccessException();
    }
}

 

RuntimeException을 상속받은 Custom Exception을 생성했다.

SQLException 발생하는 경우 해당 Exception으로 throw를 해주면 이제 delete()를 호출하는 곳에서도 SQL이 잘 실행됐는지에 대한 여부를 확인할 수 있다.

잘 처리가 되지 않는 경우 특정 메시지를 출력한다던가 별도의 처리 로직을 추가할 수도 있을 것이다.

 

그냥 try-catch로 안감싸면 되는거 아니냐? 하는 의견이 있을 수 있다.

 

public void delete(String position) throws SQLException {
    String sql = "delete from piece where coordinate = ?";

    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    preparedStatement.setString(1, position);
    preparedStatement.executeUpdate();
}

 

delete를 호출한 적 있는 메서드와 그 메서드를 호출하는 메서드들 전부 throws SQLException을 처리해야 한다.🙃

throws를 해주지 않는다면 실행도 전에 컴파일 단계에서 빨간줄이 그어질 것이다.

 

🤔 SQLException이 Checked인 이유

그냥 처음부터 SQLException을 Unchecked로 만들어졌다면...? 라는 의문이 들었다.

오늘도 Stack Overflow를 뒤적거리며 구글 번역기가 열일한다...

 

참고의 2, 3링크를 보면 여러가지 이유가 나오는데... 내 나름의 생각을 정리해보았다.

 

 Checked Exception은 컴파일러가 check를 하기 때문에 붙여진 이름이다. 예측 가능하지만, 예방할 수 없는 경우에 사용한다. SQLException같은 경우에는 DB 서버가 꺼져있어서 등의 사유로 오류 발생 가능하다. 하지만 이걸 Java 코드 쪽에서 예방하기는 어렵다. 이에 대한 처리가 반드시 필요하다!를 컴파일 단계에서 알려주기 위함이라고 생각한다.


참고

  1. Oracle - SQLException
  2. Stack Overflow - Why is SQLException a checked exception 
  3. Stack Overflow - When to choose checked and unchecked exception
  4. 백기선님 유튜브 - 언체크드 예외 발생 시 트랜잭션 롤백?
  5. chees10yun님 블로그 - checked exception
반응형