Java

[Java] 예외 처리

nkdev 2024. 12. 18. 15:01

오류(Error)의 종류

오류는 세 가지 종류로 나뉘어진다.

  • 구문 오류(컴파일 에러)
    • 문법에 맞지 않거나, 오타가 발생했을 때 컴파일러가 알려주는 오류이다. 치명적인 오류는 아니다.
  • 논리 오류
    • 컴파일/실행은 되지만 프로그램이 잘못 동작하고 있는 오류이다.
    • 프로그램이 작동하는데는 아무 문제가 없지만, 실행 결과가 개발자가 의도한 바와 다른 경우이다.
    • 예를 들어 점수의 평균을 계산하는 메서드에서 총 과목의 수가 3인데 4로 나누도록 만들었다면 논리적 오류에 해당한다. 이 경우 로직이 틀린 것이기 때문에 개발자가 코드를 짰지만 의도한 대로 프로그램이 동작하지 않게 되며, 컴파일/실행했을 때는 정상 동작하므로 쉽게 찾기가 힘들다.
  • 실행 오류(런타임 에러)
    • 프로그램 실행(런타임) 도중에 어떤 특정 컴퓨터에만, 특정한 상황에만 발생하는 오류.
    • 이 오류는 개발자가 코드를 수정해서 해결되는 문제가 아니다.
      • 네트워크 에러, 하드디스크에 문제가 생겨 읽어올 수 없음
      • 사용자가 예상 밖의 입력값을 입력함

Java에서 런타임 오류

자바는 런타임 에러의 종류를 error, exception 두 가지로 나눈다.

  • error : 프로그램 코드로 해결할 수 없는 심각한 오류
  • exception : 프로그램 코드로 해결 가능한 경미한 오류
  Error Exception
Checked Exception Unchecked Exception
(RuntimeException)
설명 외부 환경의 문제 외부 리소스와의 상호작용에서 발생하는 문제 프로그램 내의 문법 오류, 로직 문제
예시 JVM 실행 환경에서의 오류
메모리 부족
네트워크 연결 문제
파일 입출력 문제
데이터베이스 접근 문제
null값을 가진 참조 변수를 호출
배열 인덱스 초과
0으로 나눔
OutOfMemoryError
StackOverFlowError
VirtualMachineError
IOException
ClassNotFoundException
FileNotFoundException
NullPointerException
ArithmeticException
IndexOutOfBoundsException
복구 가능성 애플리케이션 레벨의 문제가 아니므로 예외 처리를 통한 복구 가능성이 없음  복구를 목적으로 반드시 예외처리하게 되어있음 복구보다는 사전 예방이 중요
예외 처리 예외 처리 하지 않음 반드시 처리해야 함
try-catch 또는 throws로 처리하고
프로그램의 정상적인 실행을 이어나감
예외 처리를 강제하면 불필요한 try-catch블록과 throws가 굉장히 많아지므로 개발자의 책임으로 돌림

 

그리고 error, exception을 클래스로 제공한다.

(Object는 모든 클래스의 조상이기 때문에 error, exception도 Object를 상속받는다.)

 

error도 Throwable을 상속받고 있기 때문에 try-catch로 잡을 수는 있다. 그러나 애플리케이션 레벨에서 잡아봤자 이미 메모리가 꽉 차있어서 어떤 조치를 내려도 실행이 안 될 것이므로, 오류를 잡기보다는 안전하게 애플리케이션을 종료하고 사용자에게 오류가 발생했음을 알리는 식으로 해결한다.

 

error처럼 심각한 문제가 아니면 exception으로 분류되어 try-catch를 통해 해결할 수 있다. 그 중에 명시적으로 처리해줘야 하는 예외를 checked exception, 별도의 처리가 필요없는 예외를 unchecked exception이라고 한다.

 

checked exception은 주로 외부 영향으로 발생하는 예외이다. 사용자가 잘못된 값이나 데이터 형식을 입력했을 때 발생한다. 반면 unchecked exception은 프로그래머의 실수로 발생한다. 배열 범위를 벗어난 곳을 접근하거나, 형 변환을 잘못 했거나, null값을 가진 참조 변수를 사용했을 때이다.

 

예외 처리란?

예를 들어 내 프로그램이 어떤 메서드를 사용한다고 하자. 그런데 메서드 내부에서 여러 동작을 수행하다가 무언가 잘못되었다면 메서드 측에서 throw new xxxException()으로 발생한 오류에 맞는 예외 객체를 생성해서 던진다. 메서드가 던진 예외를 우리 프로그램이 받아서 어떤 오류인지 식별하고 적절한 조치를 취하면 된다.

try{
//예외 발생
} catch(XXX예외 e){
//적절한 조치. 사용자에게 오류가 발생했다는 화면을 보여주고, 심각 정도에 따라 프로그램 실행을 계속 이어나갈지 말지 결정함
}

예외 클래스 생성과 던지기(throw)

이제 예외를 throw하는 로직을 만들어보고, 직접 커스텀 예외를 만들어보자.

 

1. 발생한 예외를 다시 받아서 try-catch 블럭으로 처리

public class Calculator{
	public Calculator(){}

   	public static int add(int x, int y){//던져진 exception이 자기 자신에게 돌아옴int result = x + y;
        if(result > 1000)
        	throw new OverThousandException();
        return result;
    }
}

이 경우 throw된 OverThousandException은 add() 자신에게 돌아온다. 그래서 add() 내부에서 catch로 처리해줘야 한다.

 

2. 발생한 예외를 직접 처리하지 않고 위로 던지기

public class Calculator{
	public Calculator(){}

   	public static int add(int x, int y) throws OverThousandException, UnderZeroException {//던져진 exception을 add()를 사용하는 곳으로 다시 던짐int result = x + y;
        if(result > 1000)
        	throw new OverThousandException();
        if(result < 0)
        	throw new UnderZeroException();
        return result;
    }
}

throw된 OverThousandException을 add()를 사용하는 함수가 처리하게 하려면 add() 선언부에 throws OverThousandException을 명시해야 한다. 위 예제의 경우 두 add()함수가 두 개의 예외를 던질 수 있고, 두 예외 모두 본인이 처리하지 않고 위쪽으로 던지도록 명시해줬다.

public class Program{
	public static void main(String[] args) throws OverThousandException, UnderZeroException{
    	Calculator calc = new Calculator();
        int result = 0;
        result = calc.add(3, 1000);//OverThousandException 예외 발생. 이후 코드 모두 실행되지 않은 채 프로그램 종료
        System.out.print(result);
        result = calc.add(2, 4);
        System.out.print(result);
    }
}

만약 main()도 add()가 던진 예외를 처리하지 않고 던지면 자바 런타임 환경이 예외를 받게 된다. 자바 런타임 환경은 받은 예외가 어떤 예외인지 판단할 수 없기 때문에 아무 조치도 취하지 않고 그대로 프로그램을 종료시킨다.

try-catch문으로 던져진 예외를 받고 처리하기

예외를 처리하는 이유는 다음과 같다.

  1. 어떤 예외가 발생했는지 알 수 있음
  2. 예외가 발생된 후 프로그램이 계속 실행되게 하거나 정상 종료 처리할 수 있음
public class Program{
	public static void main(String[] args){
    	Calculator calc = new Calculator();
        int result = 0;

        try{
        	result = calc.add(3, 1000);//예외 처리를 해주었으므로 OverThousandException이 발생해도 이후 코드 모두 실행됨
        } catch(OverThousandException e){

        }
        System.out.print(result);
        result = calc.add(2, 4);
        System.out.print(result);
    }
}

개발자는 본인이 만든 코드가 어떤 예외를 발생시키는지 알고 있어야 catch로 잡아서 처리할 수 있다. 위 예제에서는 calc.add(3, 1000)에서 OverThousandException이 발생할 것을 알고있어야 한다. 어떤 예외가 발생할지는 자바 공식 문서를 통해 알 수 있다.

 

실행할 코드를 try 블럭에 넣고 catch 블럭 인자에 발생할 예외를 주면, 해당 예외가 발생할 경우 catch블럭이 잡아서 처리하기 때문에 프로그램이 비정상적으로 종료되지 않는다. 위와 같이 catch 블럭에 아무것도 쓰지 않아도 예외 처리된 것으로 인식되어 프로그램이 정상적으로 실행된다.

 

실행 흐름 :

try블럭에서 예외 인스턴스 생성됨 -> catch블럭을 차례로 방문하며 일치하는 예외가 있는지 instance of 연산자로 확인 -> 일치하는 예외 1개만 실행/일치하는 예외가 없으면 아무것도 실행하지 않음-> try-catch문 탈출 -> 계속해서 다음 코드 실행

다중 예외 처리

//1. 예외별로 다르게 처리하고 싶을 때 - catch 블럭 여러 개 사용
try {

} catch(OverThousandException e){

} catch(UnderZeroException e){

}

//2. 두 예외를 같은 방법으로 처리하고 싶을 때 - catch 블럭에 or 연산자 사용
try {

} catch(OverThousandException | UnderZeroException e){

}

//3. 특정 예외 외에 다른 나머지 예외를 처리하고싶을 때 - 마지막 catch 블럭에 Exception 예외 사용
try {

} catch(OverThousandException e){

} catch(UnderZeroException e){

} catch(Exception e){

}

//4. 모든 예외에 공통적인 방법을 적용하고싶을 때 - finally 블럭 사용
try {

} catch(OverThousandException e){

} catch(UnderZeroException e){

} catch(Exception e){

} finally {
//공통 로직
}

try블럭 이후에 여러 종류의 예외를 다른 방법으로 처리하고 싶으면 여러 개의 catch문을 사용하면 된다. 그 중 발생한 예외와 일치하는 1개의 catch 블럭만 수행된다. 일치하는 예외가 없으면 catch 블럭은 실행되지 않는다. 때문에 Exception 예외를 처리하는 catch 블럭은 특정 예외의 catch 블럭보다 위쪽에 위치하면 안 된다. Exception 예외가 다른 모든 예외들이 상속받는 부모 클래스이기 때문에, 자식 예외의 catch 블럭에 도달하기 전에 항상 Exception 예외로 먼저 처리되어버린다.

 

JDK 1.7부터 ‘|’ 기호로 여러 catch블럭을 하나로 합칠 수 있다. 이를 ‘멀티 catch 블럭’이라고 하며, 개수 제한 없이 예외 클래스를 연결할 수 있다.

 

finally블럭은 예외가 발생했든 안 했든 무조건 실행되는 블럭이다. 모든 catch블럭에 공통적으로 수행하고싶은 로직이 있으면 finally블럭에 넣는다.

 

Unchecked 예외

checked 예외는 반드시 던지고 잡아야 한다. 그래서 throw를 했을 때 catch처리를 반드시 해주어야 한다. 그래서 catch 처리를 안 해주면 ide에 빨간 줄로 표시가 된다.

그러나 unchecked 예외는 던져진 예외를 반드시 처리하지 않아도 된다. 그냥 런타임에 어떤 예외가 발생했는지 확인해볼 수 있도록 할 뿐, 반드시 처리해주지는 않아도 된다.

우리가 커스텀으로 만든 예외가 발생했을 때 반드시 어떤 조치를 취하도록 만들고싶으면 Exception을 상속받게 하면 되고, 반드시 처리하지 않아도 되게 만들고 싶으면 RuntimeException을 상속받게 하면 된다.

public class UnderZeroException extends RuntimeException{//발생 시 반드시 명시적으로 처리하지 않아도 되는 예외

}
public class Calculator{
	public Calculator(){}

   	public static int add(int x, int y) throws OverThousandException, UnderZeroException {//던져진 exception을 add()를 사용하는 곳으로 다시 던짐int result = x + y;if(result > 1000)
        	throw new OverThousandException();
        if(result < 0)
        	throw new UnderZeroException();
        return result;
    }
}

https://docs.oracle.com/javase/7/docs/api/index.html

 

Java Platform SE 7

 

docs.oracle.com

https://opentutorials.org/module/516/6222

 

API와 API 문서 보는 법 - Java

기본 패키지와 사용자 정의 로직 아래 코드를 보자. System.out.println(1); 지금까지 무수히 많은 예제에서 사용했던 코드다. 이것이 화면에 어떤 내용을 출력하는 것이라는 건 이미 알고 있다. 하지

opentutorials.org