예외와 에러의 차이점
- 에러(Error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류. StackOverflow에러나 OutOfMemory오류와 같이 발생하면 복구가 불가능한 심각한 오류이다
- 예외(Exception) : 발생하더라도 수습이 되는 덜 심각한 오류. 예외는 발생하더라도 적절한 코드를 작성해 비정상적 종료를 막을 수 있다.
자바 예외 클래스 계층 구조
Exception클래스도 기본적으로 Object클래스의 자손클래스이다. 또한 예외 클래스는 두 그룹으로 나눌 수 있다
- Exception클래스와 그 자손들(RuntimeException, 이의 자손 제외) : 사용자의 실수와 같은 외적인 요인에 의해 발생되는 예외를의미한다. 존재하지 않는 파일을 입력한다거나(FileNotFoundException), 클래스 이름을 잘못 적었다거나(ClassNotFoundException), 입력한 데이터 형식이 잘못된 경우(DataFormatException) 등등
- RuntimeException클래스와 자손들 : 프로그래머의 실수로 발생하는 예외. 배열의 범위를 벗어나던가(ArrayIndexOutOfBoundsException), 값이 null인 참조변수 멤버를 호출하려 했거나(NullPointerException), 클래스 형변환을 잘못했다(ClassCastException)던가 등등
try ~ catch문
예외는 프로그래머가 처리를 해주어야 한다. 예외 처리의 정의와 목적은 아래와 같다.
- 정의 : 프로그램 실행시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
- 목적 : 비정상적인 프로그램 종료를 막고, 정상적인 실행상태를 유지하는 것
try~catch문을 이용해서 예외를 처리할 수 있다. 하나의 try블럭 다음 여러 종류 예외를 처리할 수 있도록 또 다른 하나 이상의 catch블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 한개의 catch블록만 수행된다. 만약 일치하는 예외가 없는 경우에는 처리되지 않는다.
try ~ catch문은 아래 예제와 동일하게, 중복으로 작성이 가능하다, 단 주의해야할 것은, catch 블록 내에 선언된 변수는 catch블록 내에서만 유효하기 때문에, 예외클래스 참조변수가 중복되서는 안된다. 아래 예제에서 보면 catch(ClassCastException e1)문 안에 선언된 try~catch에서 catch문의 참조변수 e1이 겹치므로, 에러가 나는것을 볼 수있다. 이 예제 코드는 작동하지 않는코드이다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
try{
System.out.println("Code");
try{
}catch (Exception e){
try{
}catch (Exception a){
}
}
}catch (ClassCastException e1){
System.out.println("Exception1");
try{
}catch (Exception e1){ // 해당 try ~ catch문이 선언된, catch문 블록의 변수 e1이 중복됨.
}
}catch (IndexOutOfBoundsException e2){
System.out.println("Exception2");
}catch (OtherExceptions e3){
}
}
}
아래와 같은 코드를 보자. Math.random()은 0~1사이의 실수를 랜덤으로 반환하는 함수이다. 실행하고 보면, 일어나지 않을 수 도 있지만 'Exception in thread "main" java.lang.ArithmeticException: / by zero'가 나는것을 볼 수 있다.기본적으로 정수를 0으로 나누는것은 금지 되어있기 때문에 일어나는 예외이다. 이를 예외처리하면 아래와 같이 할 수 있다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
int number = 100;
int result = 0;
for(int i = 0 ; i < 10; i++){
try{
result = number / (int)(Math.random() * 10); // Math.random() : 0~1사이 실수 반환.
System.out.println(result);
}catch (ArithmeticException e){
System.out.println("You can't divide number with zero!");
}
}
}
}
try ~ catch흐름 파악하기
try ~ catch문의 흐름은 어떻게 흘러갈까? 아래와 같은 실행순서를 가진다.
- try블럭 예외가 일어난 경우
- 발생한 예외와 일치하는 catch블록이 있는지 확인
- 있는경우에는 해당catch블럭 내의 문장을 수행하고, try~catch문을 빠져나가 다음 문장 수행한다. 여기서 주의 할 점은 try블록에서 예외가 발생된 경우, try블록 내 예외가 난 해당 문 이후의 코드는 실행되지 않고 무시된다.
- 발견하지 못하는 경우에는, 예외처리를 하지 못하고 예외가 발생되, 프로그램이 중단됨
- 예외가 발생하지 않은 경우에는 try문 내에 있는 처리만 하고 catch블록을 무시한 후 계속 수행한다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(3);
}
catch (Exception e){
System.out.println(4); // 수행되지 않음
}
System.out.println(5);
}
}
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(3);
System.out.println(10 / 0); // ArithmeticException
System.out.println(4); // 실행되지 않음
}catch (ArithmeticException e){
System.out.println("Arithemetic Exception");
}
System.out.println(5);
}
}
Catch블록의 흐름
그렇다면 catch블록은 어떻게 예외를 처리하는 것일까? 우선 catch문은 괄호를 가지는데 이 괄호에는 처리하고자 하는 예외클래스 타입의 참조변수를 선언해 주어야한다. 아래와 같은 형태처럼 말이다.
catch (ArithmeticException e){}
만약 예외가 발생하면 해당 예외에 맞는 예외클래스 인스턴스가 반환하게 되는데, 만약 예외가 난 문이 try문 안에 있는 경우 catch()문을 찾아 첫번째 catch문 부터 하나씩 instanceof연산자를 통해 검사하여, true가 나오는 catch문을 만날때 까지 검사하게 되는것이다.
모든 예외클래스의 최고 조상 클래스는 Exception이다 그렇기 때문에 Exception클래스를 선언하면, 모든 예외에 대해 true값을 반환하게 되는것이다.(업캐스팅이 사용되는 것이다. 자식클래스 참조타입 -> 조상클래스 참조타입)
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(3);
System.out.println(10 / 0); // ArithmeticException
System.out.println(4); // 실행되지 않음
}catch (Exception e){ // Exception클래스는 모든 예외 클래스의 조상클래스
System.out.println("Exception Class Exception");
}
System.out.println(5);
}
}
다만, 주의할 점은 예를 들어 특정 예외를 모두 처리할 수 있는 예외클래스1, 예외 클래스 2가 있다고 한다면, 가장 먼저 만나는 catch문의 예외를 처리하게 된다. 아래 예제 같은 경우 0으로 나누는 예외는 Arithemetic 예외클래스와 Exception예외클래스 모두 처리할 수 있지만, 가장 첫번째로 나오는 것이 Arithemetic예외 클래스이므로 이 catch문의 값이 출력된다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(3);
System.out.println(10 / 0); // ArithmeticException
System.out.println(4); // 실행되지 않음
}catch (ArithmeticException e){
System.out.println("ArithmeticException Class Exception");
}
catch (Exception e){
System.out.println("Exception Class Exception");
}
System.out.println(5);
}
}
예외 정보 얻기
catch()문 괄호 안에 있는 변수는 예외 클래스 타입의 인스턴스를 저장하는 참조변수이다. 예외 클래스 안에는 예외의 정보가 담겨있다. 두가지 메소드를 이용해서 예외의 정보를 알 수 있다.
- printStackTree() : 예외 발생 시, 호출스택에 있던 메소드의 정보와 예외 메세지를 출력한다
- getMessage() : 발생한 예외 클래스의 인스턴스에 저장된 메세지를 얻을 수 있다.
아래와 같이 활용할 수 있다. printStackTree()는 getMessage()처럼 String을 반환하지 않고 메소드 내에서 출력하는 void반환자임을 알고가자.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(10 / 0);
System.out.println(4);
}catch(ArithmeticException e){
e.printStackTrace();
System.out.println("Exception Occurred");
System.out.println("Message : " + e.getMessage());
System.out.println("printStackTrace");
}
System.out.println(5);
}
}
멀티 catch
JDK1.7부터 유효한 문법이다 '|'기호를 이용해서 catch문 내에 예외 들을 묶어줄 수 있다. 논리연산자 '||'가 아니니 헷갈리지 말자.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(10 / 0);
System.out.println(4);
}catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
System.out.println(e.getMessage());
}
System.out.println(5);
}
}
단 아래와 같이 조상 - 자식 관계의 예외 클래스인 경우에는 에러가 난다. 에러가 나는 이유는, 불필요한 코드 제거의 의미를 가지고 있다고 한다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(10 / 0);
System.out.println(4);
}catch(ArithmeticException | Exception e){ //에러
System.out.println(e.getMessage());
}
System.out.println(5);
}
}
멀티 catch는 여러 예외 클래스중 하나를 처리하는 것이다. 그렇기 때문에, 여러 예외들 중 어떤 예외가 발생하는지 확실하지 않다. 그렇기 때문에 멀티 catch에서는 공통적인 조상 예외 클래스에 선언된 조상만 사용이 된다. 필요한 경우에는 instanceof를 사용하여, 개별적으로 사용할 수 있다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
System.out.println(10 / 0);
System.out.println(4);
}catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
if(e instanceof ArithmeticException){
e.(// some method in ArithemeticException)
}
else if(e instanceof ArrayIndexOutOfBoundsException){
e.(// some method in ArrayIndexOutOfBoundsException)
}
}
System.out.println(5);
}
}
예외 발생시키기
throw를 통해 예외를 발생시킬 수 있다.방법은 아래 순서를 따르면 된다.
- new 연산자를 통해 발생시키려는 예외 클래스 객체를 만든다
- 키워드 throw를 통해 예외를 발생시킨다
위 두 과정을 throw new (예외클래스)();형태로 축약해 줄 수 있다.
package exceptionex;
public class exceptionex {
public static void main(String[] args){
System.out.println(1);
System.out.println(2);
try{
//ArrayIndexOutOfBoundsException e = new ArrayIndexOutOfBoundsException("Test Error Message");
//throw e;
// 위 두줄을 아래와 같이 한줄로 쓸 수 있다
throw new ArrayIndexOutOfBoundsException("Test Error Message");
}catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
System.out.println(e.getMessage()); // "Test Error Message"
}
System.out.println(5);
}
}
아래 두 코드를 컴파일 해보자
package exceptionex;
import java.io.IOException;
public class exceptionex {
public static void main(String[] args){
throw new RuntimeException("Exception");
}
}
package exceptionex;
import java.io.IOException;
public class exceptionex {
public static void main(String[] args){
throw new IOException("Test");
}
}
첫번째 코드같은 경우에는 컴파일이 완료된다. 하지만, 두번째 같은 경우에는 컴파일 과정에서 오류가 난다. 동일하게 예외를 던진 구문인데 왜 차이가 날까? 첫번째 코드 같은 경우에는, RuntimeException이다. 이는, 프로그래머에 의해 발생되는 예외 들이기 떄문에, 예외 처리를 강제하지 않는다. 반면 Exception클래스 같은 경우에는 사용자의 실수 같은 외적인 요인에 의해서 예외가 일어난다고 했다. 이런 Exception클래스같은 경우에는 예외처리가 강제되는 성격을 가지고 있다.
이와 같이 컴파일러가 예외 처리를 확인하지 않는 RuntimeException클래스와 그 자손 클래스를 'unchecked예외'라고 하며, 예외 처리를 확인하는 Exception클래스같은 경우에는 'checked예외' 라고 부른다.
메소드 예외 선언하기
메소드에 예외를 선언하기 위해서는 메소드 선언부에 throws를 사용해 메소드 내에서 발생할 수 있는 예외를 적어주면 된다. 그리고 예외가 여러개인 경우에는 쉼표를 통해 구별해준다.
public static void main(String[] args) throws Exception, ArrayIndexOutOfBoundsException, IOException{
method1();
}
메소드 예외 선언을 통해서, 해당 메소드를 사용하기위해 어떤 예외들이 처리되야하는지를 알 수 있다. 이렇게 선언된 예외는 꼭 예외처리를 해주어야한다. 예를 들어 아래와 같은 코드를 작성했다고 보면 main()메소드의 method1()호출에 에러가 난 것을 볼 수 있다
package exceptionex;
import java.io.*;
public class exceptionex {
public static void main(String[] args){
test1();
}
static void test1() throws IOException{
System.out.println("Hello world");
throw new IOException();
}
}
https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html 한번 들어가 보면 메소드들에 대해 어떤 예외가 발생되는지 알 수 있고, 이 예외가 반드시 처리해 주어야 하는 예외임을 알 수 있다.
메소드에 throws를 명시하는 것은 예외를 처리하는것이 아닌, 자신을 호출한 메소드들에게 예외를 전달하여 떠맡기는 것이다. 예외를 전달 받은 메소드들은 또 다시 자신을 호출한 메소드에게 전달할 수 있으며, 최종적으로 main에서 까지 처리가 되지 않는 경우에는 main메소드 마저 종료되어 프로그램이 비정상적인 종료를 하게 된다.
package exceptionex;
import java.io.IOException;
public class exceptionex {
public static void main(String[] args) throws Exception{
method1();
}
static void method1(){
method2();
}
static void method2(){
throw new Exception("Test Exception");
}
}
위 코드를 보면 main에서 method1(),method1()에서 method2()를 호출했다. 결론적으로 method2(),method1()에서 모두 예외 처리를 해주었기 때문에 main으로 예외가 전달되었지만, 결론적으로 예외가 처리되지 않아, 비정상적인 종료로 가게된다. 결론적으로 예외가 일어나는 경우, 어딘가에서 예외를 처리해야 비정상적인종료를 막을 수 있다.
만약 예외가 일어날 가능성이 있는 메소드를 호출하는 줄에 대해 예외 처리를 하면 예외가 발생했다는 사실을 알 수 있지만
package exceptionex;
import java.io.IOException;
public class exceptionex {
public static void main(String[] args){
method1();
}
static void method1(){
try{
throw new Exception("Test Message");
}catch (Exception e){
System.out.println("Method1에서 예외가 처리됨");
e.printStackTrace();
}
}
}
반대로 예외가 일어나는 메소드 내에서 예외를 처리하는 경우, 예외가 일어났는지 모르게 된다.
package exceptionex;
import java.io.IOException;
public class exceptionex {
public static void main(String[] args){
try{
method1();
}
catch (Exception e){
e.printStackTrace();
e.getMessage();
}
}
static void method1() throws Exception{
throw new Exception("Test Message");
}
}
예외 처리를 이용해 간단히 파일 생성을 해보자
package exceptionex;
import java.io.*;
public class exceptionex {
public static void main(String[] args) {
try {
File f = createFile("test.txt");
System.out.println(f.getName() + "파일 생성 완료");
} catch (Exception e) {
System.out.println("유효하지 않은 파일이름입니다");
}
}
static File createFile(String fileName) throws Exception{
if(fileName == null || fileName == ""){
throw new Exception("Wrong types of file name");
}
File f = new File(fileName);
//https://docs.oracle.com/javase/7/docs/api/java/io/File.html#createNewFile() 기본적으로 IOException예외 처리를해주어야한다
//여기서도 createNewFile()메소드는 throws IOException이 명시되어있으므로 예외 처리를 해주는 것이다.
try{
if(f.createNewFile()){
System.out.println("파일 생성 완료");
}
else{
System.out.println("파일 생성 실패");
}
}
catch (IOException a) {
System.out.println("오류 발생");
}
return f;
}
}
finally문
finally블록은 예외 발생 여부 상관 없이 실행될 코드를 포함시킬 목적으로 사용된다. try~catch에 덫붙여 try~catch~finally형태로 사용된다.
package exceptionex;
import java.io.*;
public class exceptionex {
public static void main(String[] args) {
try{
}catch (Exception e){
}finally {
/*Code*/
}
}
}
finally는 예외가 일어나든, 예외가 일어나지 않든 실행된다는 점을 꼭 기억하자. finally문은 try블록이나 catch블록에 return이 있어도 실행되고 종료되는 성질을 가지고 있다.
package exceptionex;
import java.io.*;
public class exceptionex {
public static void main(String[] args) {
try{
test1();
}catch (Exception e){
System.out.println("Exception");
return;
}finally {
System.out.println("Finally Block");
}
}
static void test1() throws Exception{
throw new Exception("Test exception message");
}
}
package exceptionex;
import java.io.*;
public class exceptionex {
public static void main(String[] args) {
try{
System.out.println("Try block");
}catch (Exception e){
System.out.println("Exception");
return;
}finally {
System.out.println("Finally Block");
}
}
static void test1() throws Exception{
throw new Exception("Test exception message");
}
}
사용자 지정 예외처리하기
반대로 개발자 자신이 예외를 만들 수 도 있다. 예외를 만들기 위해서는 주로 Exception이나 RuntimeException클래스를 상속받는 클래스를 만들어야 한다.(사용자 지정은 예외이다, is~a이므로 상속이 알맞은 방법이다) 꼭 이 두개의 클래스 말고 상황에 따라 자신이 필요한 예외를 상속받아 예외 클래스를 만들 수 도 있다. 예전에는 'checked예외'인 Exception을 상속받아서 예외를 작성하는 경우가 많았지만, 최근에는 'unchecked예외'인 RuntimeException으로 작성하는 추세이다.
한번 예외 클래스를 만들어 보았다. 기본적으로 Exception클래스는 생성자에서 예외 발생시 메세지를 매개변수로 받기 때문에, 메세지를 super()로 Exception 클래스 생성자에 전달하여 예외클래스를 만들 수 있다.
class MyException extends Exception{
private int ERR_CODE;
MyException(String msg, int ERR_CODE){
super(msg);
this.ERR_CODE = ERR_CODE;
}
MyException(String msg){
this(msg,100);
}
public int getERR_CODE(){
return this.ERR_CODE;
}
}
'Language > Java' 카테고리의 다른 글
[Java] Collection FrameWork (0) | 2022.02.08 |
---|---|
[Java] 컬렉션 프레임워크(Collection Framework) (0) | 2022.01.25 |
[Java]클래스간의 관계 결정하기 (0) | 2022.01.07 |
[Java] 내부클래스 (0) | 2022.01.07 |
[Java]인터페이스 (0) | 2022.01.07 |