[JAVA] 예외처리

Better late than never·2022년 9월 26일
0

프로그램 실행 흐름 상 오류가 발생하였을 때 그 오류를 대처하는 방법 - Exception, try/catch, throw, throws...

프로그램 오류

프로그램 실행 중 어떤 원인에 의해 프로그램이 해당 상황에 대처를 하지 못할 경우 비정상적으로 종료되거나 에러 팝업창이 뜨는 경우

발생시점에 따른 분류

에러

에러는 시스템 레벨에서 발생하는 아주 심각한 수준의 문제, 이러한 에러는 프로그래머가 미리 예측하지 못하며 로직으로 처리할 수 없다.

Compile Error

컴파일 시점에서 발생하는 에러로 소스코드를 컴파일러가 컴파일하는 시점에서 소스의 오타나 잘못된 구문, 자료형 체크 등 검사를 수행하는데 여기서 발생하는 에러를 컴파일 에러라 하며 이 시점에서 발생하는 문제들을 수정 후 컴파일을 성공적으로 마칠경우 클래스 파일(*.class)파일 이 생성

Runtime Error

프로그램 실행 시점에서 발생하는 에러로 컴파일러는 컴파일 시점에서 문법 오류나 오타같은 컴파일시점에서 예측가능한 오류는 잡아줄 수 있지만, 실행 중 발생할 수 있는 잠재적인 에러까지 잡을 순 없다. 컴파일으 문제없이 완료되어 프로그램이 실행도중 의도치 않은 동작에 대처하지 못해 에러가 발생할 수 있음

런타임 시점에서 발생하는 오류는 에러(error)와 예외(exception으로 나뉜다

에러(error)

메모리 부족(OutOfMemoryError)이나 스택오버 플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류

예외(exception)

인자값 Null에러 NPE(NullPointException)같은 발생하더라도 수습이 가능한 덜 심각한 오류

Logical Error

소스코드 컴파일도 정상적으로 되고 런타임상 에러가 발생하는 것도 아닌 개발자의 의도와는 다르게 동작하는 에러, 버튼을 클릭하면 팝업이 뜨게 만들었으나 팝업이 아닌 새로운 페이지가 뜨거나 아무동작을 안하는 것. 시스템 상 프로그램이 멈추거나 하지는 않지만, 의도와는 다르게 동작하는 것을 의미

예외

예외는 프로그래머가 작성한 로직으로 인해 발생하는 문제이다. 미리 예측하여 처리할 수 있기 때문에 올바른 처리방법을 통해 핸들링하는 것이 중요

  • 자바 최상위 객체인 Object를 필두로 에러객체와 예외 객체가 있다. 그리고 에러 최상위 객체인 Throwble을 상속받는 Error와 Exception이 있다.
  • Exception 하위 예외 클래스 중 RuntimeException과 그 하위 예외를 선택적 예외로 개발자가 상황에 맞춰 대응해줘야하는 예외이고, 그외 나머지 예외 클래스와 그 하위 객체들을 필수(checked) 예외라하여 반드시 체크해줘야하는 예외라 한다.
  • RuntimeException 클래스들은 주로 프로그래머의 실수에 의해 발생될 수 있는 예외들이다.
    • ex : 배열의 범위를 벗어나거나, 값이 null인 참조 변수의 멤버를 호출하려 하는 경우
  • Exception클래스들은 주로 외부에 영향으로 발생할 수 있는 것들로, 대표적으로 I/O입출력에 의해 발생하는 경우가 많다.
    • ex : 클래스의 이름을 잘못 적거나, 데이터 형식이 잘못되었거나, 사용자가 존재하지 않는 파일명을 입력한 경우

컴파일 시점 예외

CheckedException, 소스코드 상 빨간 줄로 떠서 예외처리를 꼭 해줘야하는 것들, 컴파일 시점에서 체크해주는 예외

런타임 시점 예외

런타임 시점에 발생하는 예외들도 있는데 이는 UnCheckedException이라고도 하고 RuntimException이라고 한다. 이러한 예외는 컴파일 시점에 체크되지 않아 위 소스처럼 에러가 뜨지 않는다

예외를 처리하기 위한 문법

try-catch

프로그래머가 아닌 사용자가 원인으로 발생하는 예외는 개발자가 미리 대처를 해줄 수 있다. 예외 처리(exception handling)란, 프로그래머가 예기치 못한 예외의 발생에 미리 대처하는 코드를 작성하는 것으로, 실행중인 프로그램의 비정상적인 종료를 막고, 상태를 정상상태로 유지하는 것

만약 제대로 예외를 처리하지 못한다면 프로그램은 비정상적으로 종료되며, 종료시점에 처리되지 못한 예외에 대해서 JVM의 예외처리기(UncaughtExceptionHandler)가 받아서 예외의 원인을 화면에 출력해준다

  • try-catch문은 다른 분기문과 다르게 실행할 코드가 할줄이여도 블록({})을 생략할 수 없다.
  • try-catch문은 다른 문법처럼 중복으로 블록 내부에 또 try-catch문을 작성할 수 있다
  • catch문을 통해 예외를 잡아 처리해주지 못하다면, 예외는 처리되지 않는다.

finally

예외 발생 여부와 상관없이 무조건 수행되어야 할 로직이 있을 경우 사용하는 블럭

예외가 발생하지 않는다면 try블럭 이후 finally 수행, 예외가 발생한다면 try블럭 내에서 예외가 발생하기전까지 수행되다가 예외처리를 위한 catch 블럭이 수행된 후 finally 블럭이 수행

ex : 커넥션 풀 종료, 임시파일 삭제, 소켓 종료 등

→ try는 예외 발생 가능성이 있는 로직이 포함된 블럭, catch는 예외가 발생했을 때 처리되는 로직이 포함된 블럭, finally는 예외가 발생하던 안하던 최종적으로 처리되는 로직이 포함된 블럭

Q : catch의 매개변수에 Exception을 넣으면 결론적으로 try 문에서 발생한 모든 예외들은 저 하나의 catch문에서 처리되는게 아닌가? 그럼 다양한 예외에 대한 핸들링하지 못하잖아?

맞다. 저렇게 무작정 Exception으로만 넣게되면, 모든 예외를 한곳에서 핸드링하는 격이기 때문에 상황에 맞게 예외를 분기해서 처리해야 한다. 한곳에서 처리하는 것이 효율적인 예외도 있겠지만, 예를 들어 로직에 SQLException과 FileNotFoundException 예외가 발생할 수 있는 상황이라면 각각의 예외에 대한 세부 로그를 남기는 등의 예외처리를 하면 좋지 않을까 싶다.

printStackTrace(), getMessage()

예외 발생시 예외에 대한 정보를 printStackTrace()와 getMessage()를 통해 얻을 수 있다.

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

멀티 catch 블럭

여러 catch 블럭을 | 기호를 이용해 합칠 수 있다. 아키텍처 설계 시 특정 몇몇 예외에 대해서는 동일한 로직을 수행하게 싶을 떄 해당 기호를 사용

멀티 catch 블럭을 사용할 때 두 예외 클래스가 조상-관계라면 컴파일 시 에러가 발생한다. 그 이유는 그냥 조상 클래스만 써주는 것과 동일하기 때문에

멀티 catch 블럭에서 발생하는 예외는 여러 예외를 처리하기에 발생한 예외가 실제로 어떤 예외인지 알기가 쉽지 않다

try-with-resource

JDK 1.7부터 추가된 기능으로 입출력, 소켓, 커넥션풀 등 연결/종료가 무조건 한 쌍이 되어야 하는 로직의 경우 사용

EX) try-with-resource 이전 코드

try{
	fin = new FileInputStream(file);
}catch(IOException ie) {
	ie.printStackTrace();
}finally {
	try {
		if(Objects.nonNull(fin) {
			fin.close();
		}
	}catch(IOException e) {
		e.printStackTrace();
	}
}

EX) try-with-resource 이후 코드

try(fin = new FileInputStream(file)) {
	//...
}catch(IOException e) {
	e.printStackTrace();
}

try 키워드 괄호 내에 객체 생성 코드를 작성하면 해당 객체는 close()를 명시하지 않아도 try블럭을 벗어나는 순간 자동으로 close()가 호출된다. 개발자가 명시적으로 닫아주지 않아도 되기에 코드축소가 가능

  • 사용조건 : 해당객체가 AutoColseable 인터페이스를 구현해야 한다.
    public interface AutoColseable {
     void close() throws Exception;
    }

close() 로직에서 예외가 발생한다면?

try-with-reosurce에서는 try괄호내의 객체 생성코드를 try블럭 내부를 벗어나는 순간 자동으로 close()를 호출한다고 했는데 그럼 이 close()에서 예외가 발생한다면 두 예외가 동시에 발생할 수는 없고 실제로 발생하는 (try블럭내부 로직예외)예외는 정상적으로 출력되고 close()에서 발생하는 예외는 억제된(suppressed)예외로 실제 발생하는 예외 내부에 포함되어 노출된다.

throw, throws

프로그래머가 예외를 발생시킴. 추가적으로 예외에 대한 책임을 전가할수도 있다. 이게 바로 throw와 throws이다

throw는 예외를 강제로 발생시킨 후, 상위 블럭이나 catch문으로 예외를 던진다.

비즈니스 로직에서 컴파일의 문법 오류는 없지만 로직 자체가 개발자가 의도한대로 진행되는지에 대 대한 검증(Validation)로직을 통과하지 못했을 경우 고의로 예의를 발생시켜야 할 떄 사용

throws는 예외가 발생하면 상위메서드로 예외를 던진다.

일반적으로 throws를 사용하면 try, catch 구문이 생성되지 않는 것을 확인할 수 있는데, 이 이유는 throws구문에 의해 예외에 대한 처리를 호출부로 위임하기 때문

→ throw를 통해 예외를 발생시키고 throws는 이 예외를 밖으로 던져버리고 있다.

두 가지 모두 적용하는 방식도 있다

사용자 정의 예외

class 사용자정의예외 extends 해당Exception{
}

예외 되던지기(exception re-throwing)

  • 예외를 의도적으로 자신을 호출한 로직에서 처리하게끔 되던지는 기능
  • 특정 로직에서 특정 예외를 공통으로 처리하고자 할 때 사용 가능
    • ex : Scanner예외 IOException 공통처리, 숫자 연산 예외 기본값 기본처리

EX

catch(NumberFormatException err) {
	err.printStackTrace();
}

//...
catch(NumberFormatException ne) {
	throw ne;
}

연결된 예외(chained exception)

  • 특정 예외에서 다른 예외를 발생시키는 것
  • A예외에서 B예외를 생성해 예외 던지기를 통해 B예외 처리 블록으로 유도

연결된 예외를 사용하는 이유

checked 예외를 uncheched예외로 바꿀 수 있게 된다, 즉 Exception 예외 객체를 상속하는 하위 예외 객체들을 RuntimeException으로 감싸서 unchecked 예외로 만들 수 있는데 이 기능 사용해 더 이상 억지로 try-catch로 처리해줄 필요가 없어 진다

  • 사용
    • 연결할 새로운 예외 객체 생성
    • 새로운 예외객체에 initCause 메서드에 인자값으로 기존 연결 될 예외객체를 넣어준다
      • initCause는 Throwable 객체에 정의되어 있어 어떤 예외든 사용 가능
    • throw 키워드로 연결 할 예외 객체를 던진다

EX

try {
	startInstall();
	copyFiles();
}catch(SpaceException ex) {
	InstallException ie = new InstallException("설치 중 예외 발생");
	ie.initCause(ex);
	throw new RuntimeException(ie);
}
/* catch(MemoryException me) {
	...
*/

MemoryException은 checked 예외이기에 무조건 처리해줘야 한다

하지만, 용량 부족 예외는 사실상 코드에서 어떻게 처리할 수 없기 떄문에 의밍벗는 catch블럭이 되는데, 이를 RuntimeException으로 wrapping함으로써 unchecked 예외가 되어 불필요한 catch블럭을 삭제할 수 있다.

Exception 분류

Exception은 checked와 unchecked 두 가지로 나뉠 수 있다.

체크 예외(checked Exception)

빨간색은 체크 예외, RuntimeException을 상속하지 않는 예외들을 말하는데, 체크 예외가 발생할 수 있는 메소드를 사용할 경우, 복구가 가능한 예외들이기 때문에 반드시 예외를 처리하는 코드를 작성해야 한다. catch문으로 예외를 잡거나, throws로 예외를 자신을 호출한 클래스로 던지는 방법으로 해결해야 한다. 이때 해결하지 않으면 컴파일 시 체크 예외가 발생한다. 체크 예외는 Java컴파일러와 JVM이 규칙을 준수하는지 확인하기 때문에 Exception이 호출된다.

대표적인 Exception - IOException, SQLException

언체크 예외(unchecked Exception)

초록색은 언체크 예외이다. RuntimeException을 상속한 예외들을 말하는데, 언체크 예외라고 불리는 이유는 명시적으로 예외처리를 강제하지 않기 때문이다. 언체크 예외는 따로 catch문으로 예외를 잡거나, throws로 선언하지 않아도 된다. 프로그램에 오류가 있을 때 발생하도록 의도된 것

대표적인 Exception - NullPointerException, IllegalArgumentException

0개의 댓글