Dev/Java

Exception - 에러와 예외, Unchecked 와 Checked

린네의 2024. 6. 15. 00:00

 

 

에러와 예외

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 표현한다.

 

  • 컴파일 에러(compile error) : 컴파일할 때 발생하는 에러
  • 런타임 에러(runtime error) : 프로그램의 실행도중에 발생하는 에러
  • 논리적 에러(logical error) : 컴파일도 잘되고 실행도 잘 되지만 의도한 것과 다르게 동작하는 것

소스 코드를 컴파일하면 컴파일러가 소스코드에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지를 알려 준다. 하지만 실행도중에 발생하는 런타임 에러는 언제나 발생할 수 있다. 

 

런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시 발생할 수 있는 프로그램 오류를 에러(error)예외(exception)로 구분한다.

 

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

 

 

예외 클래스의 계층 구조

자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의하였다. 모든 클래스의 조상은 Object클래스 이므로 Exception과 Error클래스 역시 Object클래스의 자손들이다.

 

예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.

 

  • Exception클래스와 그 자손들
  • RuntimeException클래스와 그 자손들

 

[출처] https://javatrainingschool.com/java-exception-types/

 

 

RuntimeException클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊다. 예를 들면 배열의 범위를 벗어난다거나, 값이 null인 참조변수의 멤버를 호출하려 했다던가, 클래스의 형변환을 잘못했다던가, 정수를 0으로 나누려고 했을 경우 등이  있겠다.

 

Exception클래스들은 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램의 사용자들의 동작에 의해서 발생하는 경우가 많다. 예를들면 존재하지 않는 파일의 이름을 입력했다던가, 실수로 클래스의 이름을 잘못 적었다거나, 입력한 데이터 형식이 잘못된 경우에 발생한다.

 

 

예외 처리하기 (Exception handling)

예외처리란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이며, 예외처리의 목적은 예외의 발생으로 인한 실행 중인 프로그램의 갑작스러운 비정상 종료를 막고, 정상적인 실행상태를 유지할 수 있도록 하는 데 있다.

 

발생한 예외를 처리하지 못하면 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외는 JVM의 예외처리기가 받아서 예외의 원인을 화면에 출력한다.

 

예외 처리를 위해서는 일반적으로 try - catch 구문을 사용한다. ( try - catch 사용법과 같은 java 문법은 포스팅에서 제외 했다. 본 글은 java에서 제공하는 예외와 에러, 예외의 종류에 대해 보다 집중하는 것을 목적으로 한다.) 모든 예외 클래스는 Exception클래스의 자손이므로, catch블럭의 괄호()에 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch블록에 의해서 처리된다. 또한 try-catch문의 마지막에 Exception클래스 타입의 참조변수를 선언한 catch블록을 사용하면, 어떤 종류의 예외가 발생하더라도 이 catch블록에 의해 처리되도록 할 수 있다.

 

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage()와 printStacktrace()를 통해 이 정보들을 얻을 수 있다.

 

  • printStackTrace() : 예외발생 당시의 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
  • getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

 

1.7부터 여러 catch블럭을 '|'기호를 이용해서 하나의 catch블록으로 합칠 수 있게 되었는데, 이것을 '멀티 catch'블록이라고 한다. 멀티 catch블록을 사용하면 중복된 코드를 줄일 수 있다. '|'기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다. 단, '|'기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생하므로 유의해야 한다. 

 

멀티 블럭으로 처리하게 되면 catch블록 내에서 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조변수 e로 멀티 catch블록에 '|'기호로 연결된 예외 클래스들의 공통분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.

 

 

 

예외 발생시키기, Unchecked와 Checked예외 

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.  new를 이용해서 발생시키려는 예외 클래스의 객체를 만들고, 키워드 throw를 이용해서 예외를 발생시킨다.

 

throw new Exception("고의로 발생시킴");

 

Exception인스턴스 생성시 생성자에 String을 넣어 주면, 이 String이 Exception인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.

 

Exception클래스들이 발생할 가능성이 있는 문장들에 대해 예외처리를 해주지 않으면 컴파일이 되지 않고, RuntimeException클래스와 그 자손에 해당하는 예외는 예외처리를 해주지 않아도 컴파일은 성공한다. RuntimeException은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는 것이다.

 

만약 RuntimeException, 즉 프로그래머의 실수에 의해 발생할 수 있는 곳에도 에러처리를 강제한다면 참조 변수와 배열이 사용되는 모든 곳에 예외처리를 해주어야 한다.

 

이렇게 컴파일러가 예외처리를 확인하지 않는 RuntimeException클래스들은 'Unchecked예외'라고 표현하고, 예외처리를 확인하는 Exception클래스들은 'Checked'예외라고 부른다.

 

 

예외를 처리하는 방법중 메서드에 선언하는 방법이 있다. 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.

 

void method() throws Exception1, Exception2, ... ExeptionN {
	//메서드 내용
}

 

 

만약 모든 예외 클래스의 최고조상인 Exception클래스를 메서드에 선언하면 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻이다. 

 

메서드에 예외를 선언할 때는 일반적으로 RuntimeException클래스들은 적지 않는다. 보통 반드시 처리해주어야 하는 예외들만 적는다.

사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다. 예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면, main메서드마저 종료되어 프로그램 전체가 종료된다.

 

예외가 발생한 메서드 내에서 자체적으로 처리해도 되는 것은 메서드 내에서 try-catch문을 사용해서 처리하고, 메서드에 호출 시 넘겨받아야할 값을 다시 받아야 하는 경우에는 예외를 메서드에 선언해서, 호출한 메서드에서 처리해야 한다

 

 

자동 자원 반환 - try - with - resources문

jdk1.7부터 try-catch문의 변형으로 try-with-resources문이 추가되었다. 주로 입출력에 사용되는 클래스 중에서 자원 반환을 위해 사용한 후에 꼭 닫아줘야 하는 것들이 있는데,  이런 상황에 사용하면 유용하다.

 

 try(writer) {  // try-with-resources 를 통한 자동 자원 반환 수행
            //파일쓰기
            writeLogTemplate(request, writer);
} catch (IOException e) {
    throw new CustomException(WorkErrorCode.CANNOT_WRITE_FILE);
}

 

 

 

사용자정의 예외 만들기 

기존의 예외 클래스는 주로 Exception을 상속받아서 'checked예외'로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어가고 있다. 'checked예외'는 반드시 예외처리를 해주어야 하기 때문에 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해지기 때문이다.

 

예외처리를 강제하도록 한 이유는 프로그래밍경험이 적은 사람들도 보다 견고한 프로그램을 작성할 수 있게 유도하기 위해 설계된 것이다.  그러나 요즘은 자바가 탄생한 20년 전과 달리 프로그래밍 환경이 많이 달라졌다.  보통 자바는 모바일이나 웹 프로그래밍에서 주로 쓰이는데, 이처럼 바뀐 환경 탓에 필수로 처리해야만 할 것 같았던 예외들이 선택적으로 처리해도 되는 상황으로 바뀌었다. 그래서 필요에 따라 예외 처리의 여부를 선택할 수 있는 'unchecked예외'가 강제적인 'checked예외'보다 더 환영받고 있다.

 

@RequiredArgsConstructor
@Getter
public class CustomException extends RuntimeException {
    private final ErrorCode errorCode;
}

 

 

 

예외 던지기 (Exception re-throwing)

하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다. 이때 주의할 점은 예외가 발생한 메서드에서는 try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드의 선언부에 발생할 예외를 throws에 지정해줘야 한다는 것이다.

 

class Exception {


	public static void main(String[] args) {
    	try {
        	method1();
        } catch(Exception e) {
        	System.out.println("main메서드에서 예외처리");
        }
    
    }


	static void method1() throws Exception {
    
    	try {
        	throw new Exception();
        } catch(Exception e) {
        	System.out.println("method1메서드에서 예외가 처리되었습니다.");
            throw e;
        }
    }

}

 

 

 

 

연결된 예외(chained exception)

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'이라고 표현한다.  

 

initCause()를 사용해서, 원인 예외를 등록할 수 있다. 발생한 예외를 그냥 처리하지 않고 원인 예외로 등록하는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다. 만약 서로 엮여있는 두 예외가 상속관계가 아니더라도, 이를 사용해서 두 예외를 묶을 수 있다.

 

혹은 checked예외를 unchecked예외로 바꿀 수 있도록 하기 위해서이다. checked예외로 예외처리를 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것이었는데, 자바 개발 초기와 컴퓨터 환경이 바뀌게 되면서 checked예외가 발생해도 예외를 처리할 수 없는 상황이 발생하기 시작했다. 이럴 때는 그저 의미없는 try-catch문을 추가하는 것뿐인데, checked예외를  unchecked예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.

 

// checked예외를 unchecked예외로 바꾸는 예제
static void startInstall() throws SpaceException {

	if(!enoughSpace()) {
    	throw new SpaceException("1234"); // checekd 메서드
    }
    
    if(!enoughMemory()) {
    	throw new RuntimeException(new MemoryException("1234"));
    }

}