Exception
Exception과 RuntimeException
일반 예외와 런타임 예외를 구분하는 데서 시작해야겠다.
-
Exception
반드시 처리해야 하는 예외. 그래서 체크 예외라고 불린다. 이 예외는 처리하지 않으면 컴파일할 수 없어서 컴파일 예외라고도 부른다. 이를 테면
ClassNotFoundException
이나IOException
같은 것들이 체크 예외에 속한다. -
RuntimeException
RuntimeException
은Exception
을 상속받는 예외 클래스다. 프로그램 실행 중에 발생하는 에러를 런타임 예외라고 부르며 반드시 처리할 필요는 없다. 따라서 언체크 예외라고 부른다. 런타임 예외로는 흔히 볼 수 있는NullPointerException
이 있다. 컴파일하는 데에는 아무 문제 없지만 런타임 중에 널포인터 예외가 발생하면 프로그램이 예상한 대로 작동하지 않을 수 있다.
예외 처리 try - catch
언체크 예외는 예외가 발생할 수 있는 상황을 미리 예측하여 try - catch보다 문법이 비교적 간단한 조건문을 사용하여 처리해도 된다. 하지만 이미 말했듯이 체크 예외는 반드시 처리해야 한다. 그때 사용하는 문법이 try - catch
문이다.
배열이나 컬렉션 프레임워크를 사용할 때 가장 자주 접하는 체크 예외 중 하나가 ArrayIndexOutOfBoundsException
이다. 배열의 인덱스 범위를 벗어났을 때 발생하는 예외다.
public static void main(String[] args) {
int[] arr = new int[5];
for(int i=0; i<10; i++) {
arr[i] = i; //ArrayIndexOutOfBoundsException
}
//입력에 성공한 값만이라도 반드시 출력해야 한다.
System.out.println(Arrays.toString(arr));
}
이 코드를 실행하면 ArrayIndexOutOfBoundsException
가 발생하면서 컴파일에 실패한다. 반복문을 돌면서 배열의 크기 즉, 배열이 가진 인덱스 범위를 벗어나기 때문이다.
그런데 이 코드가 매우 중요한 코드라서 어쨌든 입력에 성공한 값만이라도 반드시 출력해야 한다고 가정해보자. try - catch는 이 문제를 해결한다. 예외를 잡아서, 처리한다. 그것이 try - catch의 목적이다.
public static void main(String[] args) {
int[] arr = new int[5];
try {
for(int i=0; i<10; i++) {
arr[i] = i; //ArrayIndexOutOfBoundsException
}
//입력에 성공한 값만이라도 반드시 출력해야 한다.
System.out.println(Arrays.toString(arr));
} catch (ArrayIndexOutOfBoundsException e) {
//예외 발생할 시 처리해야 할 작업
e.printStackTrace(); //예외 로그
System.out.println(Arrays.toString(arr));
}
}
예외가 발생할 수도 있는 코드를 try
블럭 안에 두고 예외가 발생하면 catch
문이 붙잡아서 처리한다. 이때 발생한 예외가 catch
문에 선언된 예외와 같아야 예외를 처리할 수 있다. 여러 개의 catch블록을 사용하는 것이 가능하기 때문에 여러 예외를 처리할 수 있다. 또한 try블럭이나 catch블럭 안에 다중으로 try - catch문을 집어넣는 것도 가능하다.
멀티 catch
여러 개의 catch블럭을 사용할 수 있다고 말했는데 JDK1.7부터는 하나의 캐치블럭에서 여러 예외를 처리할 수 있게 되었다.
try {
//IOException을 발생시킬 수 있는 코드
//IllegalArgumentException을 발생시킬 수 있는 코드
} catch (IOException e) {
//IOException 예외처리
} catch (IllegalArgumentException e) {
//IllegalArgumentException 예외 처리
}
이런 다중 catch블록을 아래처럼 하나의 catch블록에서 처리할 수 있다.
try {
//IOException을 발생시킬 수 있는 코드
//IllegalArgumentException을 발생시킬 수 있는 코드
} catch (IOException | IllegalArgumentException e) {
if(e instanceof IOException) {
//IOException 예외처리
} else if (e instanceof IllegalArgumentException) {
//IllegalArgumentException 예외 처리
}
}
멀티 catch에 상속 관계에 있는 예외 클래스를 함께 둘 수 없다는 제약이 있다. 그러니까 catch(IOException | Exception e )
이런 식으로 선언할 수는 없다는 말이다. 이건 Exception이 이미 IOException을 포함한 예외이니 여러 번 선언하지 말라는 뜻이다. 개인적으로는 멀티 catch문보다 다중 catch블럭을 사용하는 것이 좀 더 명시적이고 읽기 쉬운 코드인 것 같다.
throw
직접 예외를 발생시켜야 하는 상황이 있다. 흔히 예외를 던진다, 고 말한다. 예외의 상황이 발생할 것 같을 때, 직접 예외를 던질 수 있다. 이를 테면 ARS상담 메서드를 정의한다고 가정해보자.
private void arsService (int number) {
switch(number) {
case 0 : System.out.println("상담사 연결"); break;
case 1 : System.out.println("요금 조회"); break;
case 2 : System.out.println("요금 납부"); break;
case 3 : System.out.println("다시 듣기"); break;
default :
throw new IllegalArgumentException("잘못된 입력입니다. 다시 입력하세요.");
}
}
0, 1, 2, 3, 4를 제외한 다른 숫자를 입력했을 때 적절한 예외와 함께 메시지를 던져야 한다. 그렇지 않으면 그냥 아무 일도 일어나지 않을 것이다.
throws
메서드/생성자 선언부에 throws
를 사용할 수 있다. 여러 개의 예외가 발생할 수 있다면 콤마(,)를 사용하여 여러 예외를 선언할 수 있다.
public Scanner(Path source) throws IOException //IOException, FileNotFoundException, ....여러 예외 선언 가능
이 메서드가 IOException
을 발생시킬 수도 있다는 것을 명시하는 동시에 try - catch로 예외처리를 하지 않고 메서드 밖으로 예외를 던질 수 있다.
그러니까 Scanner()
가 자신이 발생시킨 IOException
을 try - catch로 복구하지 않고 Scanner()
를 호출한 클라이언트 쪽에서 예외를 처리하도록 책임을 위임할 수 있다는 것이다.
클라이언트 쪽에서도 throws
를 사용하여 다시 책임을 위임할 수 있다. 이렇게 위임 -> 위임 -> 위임 ...반복되다가 마지막까지 예외 처리가 되지 않으면 그곳에서 예외가 발생한다. 어디선가는 반드시 예외를 처리해야 한다. 이렇게 예외를 던지기만 하고 밖으로 던지기만 하는 것은 좋은 방법이 아니다. 처리할 수 있는 예외는 처리하고, 자신이 처리할 수 없는 예외만 밖으로 던져서 책임을 위임하는 것이 좋다.
finally
try - catch문 마지막에 finally
블럭을 사용할 수 있다. 실행 순서는 try --> catch(예외 발생 시) --> finally 순이다. 예외가 발생하지 않으면 try -> finally순으로 실행된다. 즉, finally
는 예외 발생 여부와 상관없이 반드시 한 번 실행된다.
맨 처음에 예시로 사용한 코드를 보자.
int[] arr = new int[5];
try {
for(int i=0; i<10; i++) {
arr[i] = i; //ArrayIndexOutOfBoundsException
}
//입력에 성공한 값만이라도 반드시 출력해야 한다.
System.out.println(Arrays.toString(arr));
} catch (ArrayIndexOutOfBoundsException e) {
//예외 발생할 시 처리해야 할 작업
e.printStackTrace(); //예외 로그
System.out.println(Arrays.toString(arr));
}
try
블럭과 catch
블럭에서 System.out.println(Arrays.toString(arr))
arr을 출력하는 코드가 반복된다. 예외가 발생할 지 정확하게 알 수 없기 때문이다. 이럴 때 finally
를 사용할 수 있다.
try {
for(int i=0; i<10; i++) {
arr[i] = i; //ArrayIndexOutOfBoundsException
}
//입력에 성공한 값만이라도 반드시 출력해야 한다.
} catch (ArrayIndexOutOfBoundsException e) {
//예외 발생할 시 처리해야 할 작업
e.printStackTrace(); //예외 로그
} finally {
System.out.println(Arrays.toString(arr));
}
이렇듯, 예외 여부와 관계없이 한 번은 실행해야 하는 코드가 있다면 finally
를 사용하면 된다. finally
는 try - catch블럭에 return
문이 있더라도 반드시 거치게 된다.
try - with - resources
사용이 끝나면 닫아야 하는 객체들이 있다.(close()
) 이를 테면 JDBC가 제공하는 ResultSet
, PreparedStatement
, Connection
이 사용 후 반드시 close()
를 호출하여 자원을 반납해야 하는 객체들이다.
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(dburl, dbUser, dbpasswd);
String sql = "SELECT description,role_id FROM role WHERE role_id = ?";
ps = conn.prepareStatement(sql);
ps.setInt(1, roleId);
rs = ps.executeQuery();
if (rs.next()) {
String description = rs.getString(1);
int id = rs.getInt("role_id");
role = new Role(id, description);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
출처: https://androphil.tistory.com/763 [소림사의 홍반장!]
catch
블럭 안에서 다시 try-catch를 사용하여 객체를 닫는 이유는 close()
가 예외를 발생시킬 수도 있기 때문이다. 어쨌거나 매우 복잡해보이는 코드다. 이 문제를 해결하기 위해 JDK1.7에 try - with - resources
가 추가되었다.
try(
Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();) {
rs.next();
return rs.getInt("total");
} catch (Exception e) {
log.info(e.getMessage());
}
try - with - resources
문의 괄호()안에서 생성한 객체는 따로 close()
를 호출하여 닫지 않아도 try
블럭이 끝나면 close()
가 자동 호출된다.
이 try - with - resources
를 사용하여 자동으로 close()
를 호출하려면 AutoCloseable
인터페이스를 상속 받는 객체여야 한다. 실제로 Connection
이나 PreparedStatement
, ResultSet
모두 AutoCloseable
인터페이스를 상속받고 있다.
try - with - resources
에서 괄호() 안에서 생성하는 객체가 하나 뿐이라면 세미콜론(;)을 붙이지 않는다.
Custom Exception
Exception
이나 RuntimeException
같은 Exception
의 하위 클래스를 상속 받아서 사용자 정의 예외를 만들 수도 있다. 아래는 RuntimeException
을 상속 받아서 만든 사용자 정의 예외다.
public class NotEnoughStockException extends RuntimeException {
public NotEnoughStockException() {
}
public NotEnoughStockException(String message) {
super(message);
}
public NotEnoughStockException(String message, Throwable cause) {
super(message, cause);
}
public NotEnoughStockException(Throwable cause) {
super(cause);
}
}
반드시 처리해야만 하는 예외가 아니라면 RuntimeException
을 상속 받는 게 좋다. Exception
을 상속 받아서 체크 예외로 만들면 예외 처리를 강제하므로 코드가 지저분해질 수 있다.
tip :
Error
와Exception
은 다르다. 말 그대로Error
는 발생하면 손 쓸 수 없고,Exception
은 발생하더라도 프로그래머가 예측하여 수습할 수 있다.
'Java' 카테고리의 다른 글
call by value (0) | 2022.09.14 |
---|---|
왜 main()는 public static void인가? (0) | 2021.02.09 |
8주차 - 인터페이스 (0) | 2021.01.08 |
7주차 - package, import, classpath, 접근 제어자(access modifier) (0) | 2021.01.02 |
다이나믹 디스패치, 더블 디스패치 (0) | 2020.12.26 |