들어가기 전에
코드를 작성하다 보면, 비즈니스 로직뿐 아니라 오류 또는 문제가 생길 상황을 방지하기 위해 예외 처리를 하는 경우가 많습니다. 예외 상황까지 예측하여 처리해야 하는 상황에서 예외처리 관련 코드는 무성의하게 작성될 수 있다고 생각합니다. 오히려 잘못된 예외처리 코드 때문에 버그를 낳을 수 있고, 난처해질 수 있습니다.
혹시 코딩을 하시면서 한 번이라도 다음과 같이 예외처리를 한 적이 있으신가요?
// 1번 예외를 잡고는 아무것도 하지 않습니다.
try {
... // 비즈니스 로직
}
catch (SQLException e) {
// 빈 공간
}
// 2번
catch (SQLException e) {
System.out.println(e);
}
// 3번
catch (SQLException e) {
e.printStackTrace();
}
아니! 예외가 발생하면 화면에 출력해 주는데 무엇이 문제일까요? 콘솔이나 실행 창에서 메시지를 한눈에 발견할 수 있을지 모르겠지만, 실무를 하면서 다른 로그나 메시지에 묻혀버리거나 해당 오류를 찾으려고 수 만 줄의 로그를 확인하는 건 쉽지 않다고 생각합니다. 테스트 중에 해당 예외처리가 발생하지 않고 운영 서버에 올라가게 된다면 더욱 심각합니다. 아무리 서버를 담당하시는 분이라도 계속 모니터링하여 해당 오류를 발견하는 것은 당연하지 않다고 생각합니다. 오히려 개발 단계에서 예외를 확실히 확인하고 관리하는 게 더 나은 개발자로서의 자세가 아닐까요?
예외 처리를 하면서 핵심은, 모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 하는 것입니다.
예외의 종류와 특징
오류란?
시스템에 비정상적인 상황이 생겼을 때 발생합니다. 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안 됩니다. 예를 들어 OutofMemoryError 나 ThreadDeath 같은 에러는 catch 블록으로 잡아봤자 아무런 대응 방법이 없기 때문입니다.
따라서 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서는 이런 에러에 대한 대한 처리는 신경 쓰지 않아도 됩니다.
Exception과 체크 예외
java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용됩니다.
아래 그림은 예외 클래스의 구조입니다.
Exception 클래스는 다시 체크 예외(checked exception)와 언체크 예외(unchecked exception)로 구분된다.
체크 예외(checked exception)는 Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않는 것들입니다. 반면, 언체크 예외(unchecked exception)는 RuntimeException을 상속한 클래스들을 말합니다. 물론 RuntimeException은 Exception의 서브 클래스이므로 Exception의 일종이긴 하지만 자바는 RuntimeException와 그 서브 클래스는 특별하게 다룹니다.
간략하게, RuntimeException을 상속하지 않는 것만을 말하는 체크 예외라고 생각해도 됩니다. 체크 예외가 발생할 수 있는 메서드를 사용하게 된다면 반드시 예외를 처리하는 코드를 작성해야 합니다. 사용할 메서드가 체크 예외를 사용한다면 catch 문 또는 throws를 정의해서 메서드 밖으로 던져야 합니다. 그렇게 하지 않는다면, 컴파일 에러가 발생합니다.
RuntimeException과 언체크/런타임 예외
java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불립니다. 런타임 예외라고 불리기도 합니다. 물론 명시적으로 잡거나 throws를 선언해 줘도 상관없습니다.
런타임 예외는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들입니다.
대표적으로 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullPointerException 그리고 허용되지 않은 값을 사용해서 메서드를 호출할 때 발생하는 IllegalArgumentException 등이 있습니다.
코드 작성 시 주의를 한다면 피할 수 있지만, 실수 또는 부주의로 인해 발생하도록 만든 것이 런타임 예외입니다.
예외 처리 방법
예외 복구
예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것입니다. 예외 복구의 핵심은 예외가 발생하여도 애플리케이션은 정상적인 흐름으로 진행된다는 것입니다.
사용자가 파일을 읽으려고 했으나, 파일이 없다거나 다른 문제가 있어서 읽히지 않아서 IOException이 발생했다고 가정해 보자. 사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내하서 예외상황을 해결할 수 있습니다. IOException 에러 메시지가 사용자에게 그냥 던져지는 것은 예외 복구라고 볼 수 없습니다. 예외가 처리되면, 애플리케이션에서는 정상적으로 설계된 흐름을 따라 진행돼야 한다.
int maxretry = MAX_RETRY;
while(maxretry -- > 0) {
try {
... // 예외가 발생할 가능성이 있는 시도
reutrn; // 작업 성공
}
catch(SomeException e) {
// 로그 출력 및 정해진 시간만큼 대기
}
finally {
// 리소스 반납, 정리 작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
예외처리 회피
예외처리 회피는 말 그대로 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것입니다. throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 것(rethrow) 하는 것입니다.
예시 1 ) 예외처리 회피 1
public void add() throws SQLException {
// JDBC API
}
예시 2 ) 예외처리 회피 2
public void add() throws SQLException {
try {
// JDBC API
}
catch(SQLException e) {
// 로그 출력
throw e;
}
}
물론 자신의 코드에서 발생하는 예외를 그냥 던져버리는 건 무책임한 책임회피일 수 있습니다.
만약 DAO가 SQLException을 생각 없이 던져버리면 어떻게 될까요?
=> 해당 DAO를 호출하는 서비스 계층이나 웹 컨트롤러에서 과연 SQLException을 처리할 수 있을까요?
=> 예외는 서비 계층 메서드가 다시 던지고, 컨트롤러도 또다시 던져서 서버에 그대로 전달될 것입니다.
예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 합니다. 콜백/템플릿처럼 긴밀한 관계가 있는 다른 오브젝트에게 예외처리 책임을 분명히 지게 하거나, 자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 확신이 있어야 합니다.
예외 전환
예외 전환은 발생한 예외를 그대로 던지는 것이 아니라 적절한 예외로 전환해서 던진다는 특징이 있습니다. 예외 전환은 보통 두 가지 목적으로 사용됩니다.
첫 번째는 내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하여 적절한 의미를 가진 예외로 넘겨주는 것입니다. 아래 예제 코드를 한 번 같이 보시죠.
public void add(USER user) throws DuplicateUserIdException, SQLException {
try {
// JDBC를 이용해 user 정보를 DB에 추가하는 코드 또는
// 그런 기능을 가진 다른 SQLException 을 던지는 메소드를 호출하는 코드
}
catch(SQLException e) {
// ErrorCode 가 MySQL 의 "Duplicate Entry(1062)"" 이면 예외 전환
if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
throw DuplicateUserIdException();
else
throw e;
}
}
DAO에서 SQLException을 그대로 서비스 계층으로 던진다고 가정해 보자. 사용자를 추가하기 위해 DAO를 호출한 서비스 계층 입장에서는 왜 오류가 났는지 확인하기가 어렵습니다. 물론 서비스 계층에서도 원인을 해석해서 대응할 수 있지만, 특정 예외에 대한 내용을 해석하여 비즈니스 로직에 녹여내는 것은 굉장히 어색합니다. 즉, DAO에서 만약 중복된 ID가 있다는 내용을 가진 DuplicateUserIdException으로 바꿔서 던져주는 게 좋습니다.
두 번째 전환 방법은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장(wrap)하는 것입니다. 주로 예외처리를 강제하는 체크 예외를 언체크 예외로 바꾸는 경우에 사용합니다. 대표적으로 EJBException을 들을 수 있습니다.
간략하게 EJBException이란, 예기치 않은 오류(예: 인스턴스가 데이터베이스 연결을 열지 못함)로 인해 호출된 비즈니스 메서드 또는 콜백 메서드를 완료할 수 없음을 보고하기 위해 EJBException이 발생합니다.
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
}
catch (NamingException ne) {
throw new EJBException(ne);
}
catch (SQLException se) {
throw new EJBException(se);
}
catch (RemoteException re) {
throw new EJBException(re);
}
EJBException은 RuntimeException 클래스를 상속한 런타임 예외입니다. 런타임 예외이기 때문에 EJB 컴포넌트를 사용하는 다른 EJB나 클라이언트에서 일일이 예외를 잡거나 다시 던지는 수고를 할 필요가 없습니다. 이런 예외는 잡아도 복구할 만한 방법이 없기 때문입니다.
어차피 복구하지 못할 예외라면 애플리케이션 코드에서는 런타임 예외로 포장하여 던지고, 예외처리 서비스 구현하여 자세한 히스토리나 로그를 남기는 것이 좋습니다. 물론 담당자나 운영자에게는 해당 내용을 공유하고, 서비스를 이용하는 사용자에게는 메시지를 통해 현재 상황을 알려주는 것이 좋습니다.
참고 사이트
넥스트리소프트-JAVA 예외(Exception) 처리에 대한 작은 생각
'개발 > JAVA' 카테고리의 다른 글
[JAVA] JAVA8 과 JAVA11 의 차이점을 알아보자. (0) | 2023.09.23 |
---|---|
[JAVA] 함수형 프로그래밍 (0) | 2023.09.22 |
[JAVA] 접근 제어자 (Access Modifier) 에 대하여 (0) | 2023.09.16 |
[JAVA] JAVA7 과 JAVA8 의 차이점을 알아보자. (0) | 2023.09.08 |
[JAVA] Synchronized 란? (2) | 2023.08.24 |