java에서는 예와(exception)라고 부르는 오류가 있다. exception은 잘못된 문법, 코딩으로 인해 발생한 오류를 말한다. exception이 발생하면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하지만, exception의 처리를 통해 실행 상태를 유지할 수 있다. exception에는 다음 두 가지가 있다.
IOException
, SQLException
가 있다.NullPointerException
, ArrayIndexOutOfBoundsException
가 있다.java는 exception이 발생하면 exception 클래스로부터 객체를 생성한다. 이 객체는 exception을 처리하는 데 사용된다. java의 모든 error와 exception class들은 Throwable
을 상속받아 만들어지고, 추가적으로 exception class는 java.lang.Exception
클래스를 상속받는다.
Throwable
^
|--------------------------
| |
Exception Error
^
|
------------------------------------------------
| | |
classNotFoundException InterruptedException RuntimeException
^
|
-------------------------------------------------
| |
NullPointerException ArrayIndexOutOfBoundsException ...
RuntineException
을 상속받은 exception들은 'runtime exception'이다. 반면에 RuntimeException
을 상속하지 않고 같은 위치에 있어 Exception
을 상속하고 있는 exception들은 '일반 예외(exception)'이다.
exception을 처리하는 방법은 try-catch-finally
block으로 구성된다.
try{
// 에외 발생 가능성 코드
} catch(ExceptionClass e) {
//예외 처리
} finally {
//항상 실행
}
try
에 있는 '예외 발생 가능성 코드'의 Exception발생 여부에 따라 코드 분기를 다음과 같이 나눌 수 있다.
finally
는 모든 분기에서 실행되며, 생략이 가능하다.
다음의 예제를 보도록 하자.
public class Main {
public static void main(String[] args) {
printLength(null);
}
public static void printLength(String data) {
try {
int res = data.length();
System.out.println("문자 수 " + res);
} catch (NullPointerException e) {
System.out.println("getMessage: " + e.getMessage());
System.out.println("toString: " + e.toString());
e.printStackTrace();
} finally {
System.out.println("Finally!");
}
}
}
printLength
는 String을 받으면 그 길이를 계산해서 출력하는 함수이다. 문제는 null
이 올 수 있다는 것인데, null
이 오게되면 NullPointerException
이 발생한다. 이는 runtime exception이므로 컴파일 단계에서는 확인할 수 없다.
실행해보면 다음과 같은 결과가 나온다.
getMessage: Cannot invoke "String.length()" because "data" is null
toString: java.lang.NullPointerException: Cannot invoke "String.length()" because "data" is null
Finally!
java.lang.NullPointerException: Cannot invoke "String.length()" because "data" is null
at Main.printLength(Main.java:8)
at Main.main(Main.java:3)
catch
block을 보면 getMessage
로 예외가 발생한 이유를 확인할 수 있다. toString
도 getMessage
와 같이 exception이 발생한 이유도 설명해주지만, exception의 종류도 반환해준다는 차이가 있다.
printStackTrace
는 exception이 발생한 callstack을 반환해준다는 특징이 있다.
다음의 예제는 'runtime exception'이 아니라 일반 exception으로 compile단계에서 확인이 가능하다.
public class Main {
public static void main(String[] args) {
try {
Class.forName("java.lang.String");
System.out.println("java.lang.String class가 존재");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println();
try {
Class.forName("java.lang.String2");
System.out.println("java.lang.String2 class가 존재");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Class.forName
의 경우 특정 클래스 경로를 문자열로 입력하여 해당 클래스가 없다면 ClassNotFoundException
exception을 반환한다. 이때, ClassNotFoundException
은 'runtime exception'이 아니라 일반 exception이기 때문에 컴파일 단계에서 반드시 try-catch
로 감싸주어야 한다. 안그러면 compile error가 발생하기 때문이다.
결과는 다음과 같다.
java.lang.String class가 존재
java.lang.ClassNotFoundException: java.lang.String2
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:528)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:462)
at java.base/java.lang.Class.forName(Class.java:453)
at Main.main(Main.java:13)
catch
문을 잘보면 특정 exception class가 있는 것을 볼 수 있다. 이는 try
문에 실행되는 특정한 exception을 잡아서 처리하기 위함인데, 만약 try
에 여러 exception들이 발생할 수 있다면, 어떻게 이들을 구분하여 잡을 수 있을까?? 답은 간단한데, 여러 개의 catch
를 사용하면 된다.
다음의 예제는 배열의 인덱스가 초과되었을 경우 발생하는 ArrayIndexOutOfBoundsException
과 문자열을 정수로 변환하는데, 문자열이 숫자 타입이 아닐 때 발생하는 NumberFormatException
을 각각 다르게 exception 처리한다.
public class Main {
public static void main(String[] args) {
String[] array = {"100", "1oo"};
for(int i = 0; i <= array.length; i++) {
try {
int value = Integer.parseInt(array[i]);
System.out.println("array" +"[" + i + "]" + value);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스가 초과됨 " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("숫자로 변환할 수 없음 " + e.getMessage());
}
}
}
}
try
코드를 보면 배열의 인덱스를 넘어가면 발생하는 exception인 ArrayIndexOutOfBoundsException
이 발생할 수 있고, 문자열이 숫자가 아닌데 정수형 타입으로 변환하려는 시도로 인해 NumberFormatException
이 발생 할 수도 있다. 두 eception에 대해서 각각의 catch
로 잡아내는 것이다.
결과는 다음과 같다.
array[0]100
숫자로 변환할 수 없음 For input string: "1oo"
배열 인덱스가 초과됨 Index 2 out of bounds for length 2
만약, 두 개 이상의 exception을 하나의 catch
block으로 동일하게 exception 처리하고 싶다면, catch
block의 exception class에 |
로 연결하면 된다.
String[] array = {"100", "1oo"};
for(int i = 0; i <= array.length; i++) {
try {
int value = Integer.parseInt(array[i]);
System.out.println("array" +"[" + i + "]" + value);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스가 초과됨 " + e.getMessage());
} catch (NullPointerException | NumberFormatException e) {
System.out.println("숫자로 변환할 수 없음 " + e.getMessage());
}
}
다음과 같이 |
으로 서로 다른 exception에 대해서 같은 처리를 해줄 수 있다.
try문과 같이 특정 분기에 따라 code의 흐름이 달라지는 경우에 resource들을 관리하기 어려울 때가 있다. 가령 특정 'file'을 'open'했다면 'close'까지도 해주어야 한다. 그런데, try-catch의 코드 분기 흐름 때문에 실행해야했던 'close' 함수가 실행되지 않아 resource가 회수되지 않을 수 있다.
물론 finally
코드를 넣어서 안전하게 close할 수 있지만, 이는 직접 넣어주어야 하는 단점이 있다. 마치 python이 with
문을 통해서 자원을 회수하듯이 java도 try-with-resources
블록을 통해서 리소스르 자동으로 회수하도록 할 수 있다. try-with-resources
의 기본 문법은 다음과 같다.
try(FileInputStream fls = new FileInputStream("file.txt")){
...
}catch(IOException e) {
...
}
그동안 비워두었던 try
에 하나의 입력을 주는 것이다. 이 입력에 쓰이는 대상이 바로 reosource
이고, 해당 try-catch
block이 종료되면 회수해야하는 자원인 것이다.
try-with-resources
block을 사용하기 위해서는 조건이 하나 있는데, target이 되는 resource class에서 AutoCloseable
인터페이스를 구현해서 AutoCloseable
인터페이스의 close
메서드를 정의해야한다.
가령 FileInputStream
은 다음과 같이 AutoCloseable
인터페이스를 구현하고 있다.
public class FileInputStream implements AutoCloseable {
@Override
public void close() throws Exception {...}
}
만약, 여러 resource들을 try-with-resources
로 실행시키고 싶다면 다음과 같이 쓸 수 있다.
try(FileInputStream fls1 = new FileInputStream("file1.txt");
FileInputStream fls2 = new FileInputStream("file2.txt"))
{
}catch(IOException e) {
}
;
을 경계로 나란히 resource들을 써주면 된다.
java8 이전 버전은 try
괄호 안에서 resource변수를 반드시 선언해야 했지만, java9 이후부터는 외부 리소스 변수를 사용할 수 있다. 따라서, 위 코드는 다음과 같이 변경할 수 있다.
FileInputStream fls1 = new FileInputStream("file1.txt");
FileInputStream fls2 = new FileInputStream("file2.txt");
try(fls1; fls2)
{
}catch(IOException e) {
}
다음의 예제는 AutoCloasable
이라는 인터페이스를 구현한 MyResource
리소스를 try-with-resources
블록에서 사용한다. try
블록에서 exception 발생 여부와 상관없이 안전하게 close
메서드가 실행되는 것을 볼 수 있다.
public class MyResource implements AutoCloseable{
private String name;
public MyResource(String name) {
this.name = name;
System.out.println("MyResource 생성자");
}
public String read1() {
System.out.println("MyResource read1");
return "100";
}
public String read2() {
System.out.println("MyResource read1");
return "abc";
}
@Override
public void close() throws Exception {
System.out.println("MyResource" + name + " 닫기");
}
}
public class Main {
public static void main(String[] args) {
try (MyResource res = new MyResource("A")) {
String data = res.read1();
int value = Integer.parseInt(data);
} catch (Exception e) {
System.out.println("예외 처리 " + e.getMessage());
}
System.out.println();
try (MyResource res = new MyResource("A")) {
String data = res.read2();
int value = Integer.parseInt(data);
} catch (Exception e) {
System.out.println("예외 처리 " + e.getMessage());
}
System.out.println();
MyResource res1 = new MyResource("A");
MyResource res2 = new MyResource("B");
try (res1; res2) {
String data1 = res1.read1();
String data2 = res1.read1();
} catch (Exception e) {
System.out.println("예외 처리 " + e.getMessage());
}
}
}
결과는 다음과 같다.
MyResource 생성자
MyResource read1
MyResourceA 닫기
MyResource 생성자
MyResource read1
MyResourceA 닫기
예외 처리 For input string: "abc"
MyResource 생성자
MyResource 생성자
MyResource read1
MyResource read1
MyResourceB 닫기
MyResourceA 닫기
AutoCloseable
interface를 구현한 MyReource
가 try-with-resources
에 실행되어 자동으로 try문이 끝날 때마다 close
를 호출하는 것을 볼 수 있다.
메서드 내부에서 exception이 발생할 때 try-catch
블록으로 exception을 처리하는 것이 기본이지만, 메서드를 호출한 곳으로 exception을 떠넘길 수도 잇다. 이때 사용하는 키워드가 throws
이다. throws
는 메서드 선언부 끝에 작성하는데, 떠넘길 exception class를 쉼표로 구분해서 나열해주면 된다.
리턴타입 메서드명(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
}
exception을 throw하는 method를 사용하는 곳에서 해당 exception이 발생했을 때 어떻게 처리할 지에 대해서 try-catch
로 처리해야만 한다.
public class Main {
public static void main(String[] args) {
try {
findClass();
} catch (ClassNotFoundException e) {
System.out.println("예외 처리" + e.toString());
}
}
public static void findClass() throws ClassNotFoundException {
Class.forName("java.lang.String2");
}
}
findClass
메서드의 경우 ClassNotFoundException
exception을 던지는 것을 볼 수 있다. findClass
를 호출하는 함수에서, 해당 exception
이 발생할 수 있기 때문에 try-catch
로 처리해야만 한다. 만약 try-catch
와 같은 처리를 해주지 않으면 컴파일 단계에서 실행도 안된다.
throw할 exception이 다수라면 다음과 같이 나열하면 된다.
public static void findClass() throws ClassNotFoundException, IOException {
Class.forName("java.lang.String2");
}
그런데, 너무 많은 경우에는 Exception
클래스 하나만 throw하게 해주어도 된다. 왜냐면 모든 Exception들은 Exception
의 자식이기 때문이다.
public static void findClass() throws Exception {
Class.forName("java.lang.String2");
}
사용자의 비지니스 로직에 따른 exception들이 있기 때문에 사용자가 직접 exception을 만들어 사용할 수도 있다.
사용자 정의 exception은 컴파일러가 체크하는 '일반 예외(exception)'으로 선언할 수도 있고, 컴파일러가 체크하지않고 런타임에 발생하는 '실행 예외(runtime exception)'으로 선언할 수도 있다. 통상적으로 '일반 예외'는 Exception
의 자식 클래스로 선언하고, '실행 예외'는 RuntimeException
의 자식 클래스로 선언한다.
먼저 '일반 예외'로 CustomExeption
을 만들어보도록 하자.
public class CustomException extends Exception{
public CustomException() {
// 기본 생성자
}
public CustomException(String msg) {
super(msg); // unput exception msg
}
}
CustomException(String msg)
는 사용자의 입력을 받아서, getMessage
의 반환값으로 사용하기 위함이다.
다음은 '실행 예외'인 CustomRuntimeException
을 만들어보도록 하자.
public class CustomRuntimeException extends RuntimeException{
public CustomRuntimeException() {
}
public CustomRuntimeException(String msg) {
super(msg);
}
}
두 exception 대부분 같으나 상속하는 대상이 Exception
이냐 RuntimeException
이냐에 따라 다른 것이다.
사용자 정의 exception을 반환하기 위해서는 code분기에서 throw
로 던져주면 된다.
throw new CustomException();
throw new CustomException("custom exception");
throw new CustomRuntimeException();
throw new CustomRuntimeException("custom runtime exception");
exceptopn을 throw
하는 메서드에, throws
를 통해서 해당 exception을 나열하면 된다.
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try {
callCustomException();
} catch (CustomException e) {
System.out.println("Main exception: " + e.getMessage());
}
System.out.println();
try {
callCustomRuntimeException();
} catch (CustomRuntimeException e) {
System.out.println("Main exception: " + e.getMessage());
}
}
public static void callCustomException() throws CustomException {
throw new CustomException("CustomException call!!!");
}
public static void callCustomRuntimeException() throws CustomRuntimeException {
throw new CustomRuntimeException("CustomRuntmeException call!!!");
}
}
결과는 다음과 같다.
Main exception: CustomException call!!!
Main exception: CustomRuntmeException call!!!
추가적으로 직접 위의 코드를 써보면서 CustomException
은 compile 단계에서 반드시 처리하라고 나올 것이다. 반면에 CustomRuntimeException
은 compile 단계에서는 처리하지 않아도 된다고 나올 것이다. 이것이 '일반 예외'와 '실행 예외'의 차이이다.