[Java] Error And Exception
예외 계층
Error는 일반적으로 프로그램 자체에서 처리할 수 없는 심각한 문제다. Error는 일반적으로 하드웨어 또는 시스템 문제와 같은 외부 요인으로 인해 발생하며, 이로 인해 프로그램이 갑작스럽게 종료될 수 있다. Java에서 발생하는 Error의 예로는 OutOfMemoryError 와 StackOverflowError가 있다.
반면에 Exception은 프로그램 자체에서 처리할 수 있는 덜 심각한 문제다. Exception는 프로그램 로직의 오류 또는 잘못된 사용자 입력이나 네트워크 연결 실패와 같은 실행 중 예기치 않은 조건으로 인해 발생한다. Exception는 try-catch 블록을 사용하여 프로그램에서 포착하고 처리할 수 있으며, Java에서 Exception의 예로는 NullPointerException(RuntimeException의 자식 클래스)과 IllegalArgumentException이 있다.
- Exception 은 Checked Exception과 Unchecked Exception이 있다. Checked Exception은 try-catch블럭을 사용해 개발자가 예외처리를 직접해야 하지만 Unchecked Exception은 개발자가 예외를 처리해주지 않아도 컴파일 오류가 발생하지 않는다. (물론 try-catch block을 사용해서 예외를 처리해도되지만 강제되지 않는다는것이다).
- Unchecked Exception의 예로 RuntimeException이 있다. RuntimeException의 자식 클래스들 또한 Unchecked Exception이다.
예외는 발생한 곳에서 처리하지 못하면 밖으로 던져줘야 한다.
또한 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
즉 Exception 을 catch 로 잡으면 그 하위 예외들을 모두 잡을수 있으며, Exception을 throws로 던지면 그 하위 예외들도 모두 던질수 있다.
Checked Exception, Unchecked Exception
Exception을 상속받는 두 예외의 가장큰 차이는 예외를 처리할 수 없을때 해당 예외를 밖으로 던지는 부분에 있다.
Checked Exception : 해당 예외를 처리할수 없다면 반드시 예외를 던져줘야한다.
Unchecked Exception: 해당 예외를 처리할수 없다해도 예외를 던져주지 않아도 된다.
public void callThrow() throws MyUncheckedException {
repository.call();
}
이런식으로 RuntimeException을 상속받는 MyUncheckedException의 경우 unchecked Exception인 RuntimeException을 상속받으므로, 반드시 throws 를 해줄 필요는 없다.
public void callThrow(){
repository.call();
}
즉 이렇게만 써도 된다는 소리다.
Checked Exception, Unchecked Exception을 언제 사용해야 하는가?
Checked Exception 같은경우는 예외가 발생했을때 반드시 잡아서 처리해야 하는 문제일 때만 사용한다.
예를들어 계좌 이체 실패 예외 같이 매우 심각한 문제는 개발자가 Unchecked Exception으로 둘경우 예외를 놓치는 경우가 발생할수 있기 때문에 체크 예외로 만들어 두면 컴파일러를 통해 놓친 예외를 인지할수 있다.
❗️지금 까지만 보면 체크 예외가 런타임 예외(체크 예외)보다 더 안전하고 좋아 보인다. (컴파일러가 해당 예외를 잡지 않았을때 검증을 해주므로) 하지만 체크 예외를 기본적으로 사용하면 발생하는 문제들은 아래와 같다.
Checked Exception의 문제점
Repository 와 NetworkClient 는 각각 SQLException, ConnectException을 던진다. 이때 두 메서드를 호출하는 Service라는 곳에서 해당 예외를 처리할수 없다면, 두 예외가 계속해서 상위 클래스로 이동하게 된다.
결국 해당 예외는 ControllerAdvice에서 처리를 하게된다. 그후 웹 애플리케이션에서는 사용자에게 "서비스에 문제가 있습니다" 라는 일반적인 메시지를 보여준다.
즉 서비스 입장에서 알고싶지 않은 오류 내용(처리 불가능한)을 Checked Exception을 사용하므로써 강제적으로 알게된다는 문제점이 발생한다. 결론적으로 개발자가 예외를 던지는 시점에서 그것을 처리할 방법을 알 수 없는 경우 Unchecked Exception을 사용하는게 더 좋다는 뜻이다.
만약 Service, Controller 에서 처리할수 없는 (위에서 SQLException) 예외를 throws 하게 되면 어떠한 추가적인 문제점이 있을까?
가장 큰 문제점중 하나는 Service 또는 Controller에서 SQLException을 던지게 되면 해당 계층에서 java.sql.SQLException
에 의존하는 문제가 발생한다.
즉 우리가 추후 JPA를 사용하게 된다면 SQLException
에 의존하던 모든 서비스, 컨트롤러의 코드를 JPAException
에 의존하도록 고쳐야 한다.
따라서 이런 예외 하나때문에 OCP,DI를 통해 클라이언트 코드의 변경 없이 대상 구현체를 변경할수 있다는 장점이 사라지게 된다.
만약 Checked Exception을 Unchecked Exception으로 변경한다면?
만약 위처럼 SQLException을 RuntimeException을 상속하는 RuntimeSQLException
을 던지게 해보자.
이렇게 UncheckedException을 던지는 경우 해당 예외를 처리할수 없는 Service 나 Controller 에서 해당 예외에 대한 내용을 알지 못하며, ControllerAdvice에서 해당 예외를 공통적으로 처리할수 있다.
또한 Checked Exception을 사용하며 발생했던 문제, Service 나 Controller가 해당 예외에 의존하는 문제 를 모두 해결할수 있다. 이렇게 하게되면 중간에 기술이 변경 되더라도 해당 예외를 사용하지 않는 Controller나 Service의 코드는 변경하지 않아도 된다.
❗️위처럼 RuntimeException을 사용하는게 더 좋다. 다만 주의할점은 개발자가 해당 예외를 놓치지 않도록 런타임 예외에 대한 문서화를 잘 해놔야한다.
예외 포함과 스택 트레이스
예외를 전환할 때 기존 예외를 포함해야 한다. 그렇지 않으면 스택 트레이스를 확인할 때 심각한 문제가 발생한다.
스택트레이스 출력방법
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
log.info("ex", e);
}
}
이런식으로 log를 찍을때 마지막 파라미터에 예외를 전달하면 스택 트레이스를 출력할수 있다.
기존 예외를 포함하여 예외를 던지는 방법 (exception chaining)
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e); //기존 예외(e) 포함
}
}
SQLException 이 발생했을때 RuntimeSQLException을 던져주는데 이때 e 를 포함하면 RuntimeSQLException
을 catch할 때 원래의 SQLException
에 대한 정보도 함께 얻을 수 있다.
13:10:45.626 [Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest - ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException:
java.sql.SQLException: ex at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.ja
va:61)
즉 예외를 다시 던질 때 원래의 예외를 인자로 넘겨줘야 상위 레이어로 예외를 전달하면서 원래의 예외 정보를 유지할수 있기 때문에 예외를 다시 던질때 원래의 예외를 인자로 넘겨주는 것을 잊으면 안된다.
데이터 접근 예외 직접 만들기
만약 위 그림처럼 특정 데이터 베이스의 예외를 복구하고 싶다고 가정해보자.
데이터를 DB에 저장할때 Unique 가 설정되어 있는 Column에 같은 값을 넣으려 한다면 JDBC 그라이버는 SQLException
을 던진다. 그리고 이 SQLException
에는 ErrorCode라는게 들어있다.
예를들어
e.getErrorCode() == 23505
위처럼 ErrorCode가 23505 라면 키 중복 오류 라는 것을 알수있고
ErrorCode가 42000 이라면 SQL 문법 오류 라는 것을 알수 있다.
❗️해당 ErrorCode는 DB마다 다르기 때문에 DB메뉴얼을 참고하자
만약 Repository에서 SQLException 이 발생한다면 우리가 직접 예외를 따로 만들어 Checked Exception인 SQLException을 만드는게 아닌 RuntimeException을 상속 받는 예외를 만들어 던져주면 된다.
근데 앞서 말했다 싶이 DB마다 ErrorCode는 모두 다르다. 즉 23505를 통해 키 중복 오류를 catch 한다하면, 그후에 DB가 변경되었을때 해당 ErrorCode를 모두 변경해야 한다는 문제점이 생긴다. 또한 ErrorCode는 매우 많다 따라서 해당 예외가 JDBC 에서 발생할때 마다 하나하나 체크를 해서 예외를 잡아줘야하는 문제가 발생한다. 이런 문제는 어떻게 해결해야 할까?
DB 마다 다른 ErrorCode를 해결하는 방법
스프링이 제공해주는 데이터 접근 예외 계층을 사용하면 해당 문제를 해결할 수 있다.
위 사진에서 DataAccessException은 Spring이 제공하는 데이터 접근 예외 계층의 최상위 이며, DataAccessException는 RuntimeException을 상속받기 때문에 Unchecked Exception이다.
또한 스프링에서 제공하는 예외는 특정 기술에 종속적이지 않다. 즉 JDBC 기술을 사용하든 JPA 기술을 사용하든 스프링이 제공하는 예외를 사용하면 된다.
즉 예를 들어 우리가 잘못된 sql 쿼리문 작성시 Spring에서는 BadSqlGrammarException
이라는 예외를 던져주는 것이다.
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);
//org.springframework.jdbc.support.sql-error-codes.xml
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
//org.springframework.jdbc.BadSqlGrammarException
DataAccessException resultEx = exTranslator.translate("select", sql, e);
log.info("resultEx", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
extranslator.translate("select", sql, e)
- 첫번째 파라미터는 읽을수 있는 설명 (만약 save 기능을 하고있는 메서드에서 예외를 던지려면 "save"라 하면된다) 아무렇게나 우리가 알수 있게 설명을 적으면 된다.
- 두번째는 실행한 sql
- 세번째는 발생한 Exception
을 인자로 던져주면 resultEx을 통해 해당 예외가 어떠한 예외인지 알수 있다.
❗️결론
Service, Controller 같은 계층에서 예외 처리가 필요하다면 특정 기술에 종속적인 SQLException
과 같은 예외를 직접 사용하는 것이 아닌, 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.
그니까 Repository에서 스프링이 제공하는 추상화된 예외를 던지고, Service 계층에서는 해당 예외를 catch 하면 된다.
Reference
'Java > Java 개념' 카테고리의 다른 글
[Java] java.lang 패키지 (0) | 2023.03.12 |
---|---|
[Java] Test코드에서 Assertions를 통해 오류 잡기 (0) | 2023.03.12 |
[Java] ArrayList 출력 방법 3가지 (0) | 2023.03.12 |
[Java] Object를 String타입으로 변환 (0) | 2023.03.12 |
[Java] test코드작성시 console을 통해 입력받는 방법 (0) | 2023.03.12 |