Search
Duplicate

Java - 에러(Error)와 예외(Exception)

글감
Java
작성자
작성 일자
2024/02/08 06:43
상태
완료
공개여부
공개
Date
생성자
작업자

프로그래밍 오류의 종류

프로그래밍의 오류
프로그램에서 오류가 발생하면 시스템 레벨에서 문제가 발생해 원치않는 버그를 일으키거나, 프로그램이 종료되기도 한다.
프로그램 오류의 원인에는 정말 많은 이유와 상황들이 있는데, 단순 오타부터 시작해 설계를 잘못했거나 하드웨어에 문제가 생겨 프로그램에 오류가 발생할 수도 있다.
이렇게 프로그래밍에는 여러 오류들을 발생 시점에 따라 크게 3가지로 나눈다
컴파일 에러(compile-time error) : 컴파일 시에 발생하는 에러
런타임 에러(runtime error) : 실행 중에 발생하는 에러
논리적 에러(logical error) : 실행은 되지만 의도와 다르게 동작하는 것
논리 에러(Logic Error)
논리 에러는 쉽게 말해 버그 상황으로 프로그램을 실행하도 작동하는데는 아무런 문제가 없지만, 의도한대로 동작하지 않아 사용자가 작업을 제대로 수행하지 못하게 되는 상황이다.
예를 들면, 물건의 재고가 음수가 되거나 게임 캐릭터의 피가 0이 되어도 죽지 않는 것이 있다.
논리 에러는 프로그램 상으로는 문제가 없기 때문에, 에러 메세지나 오류를 뱉지 않아 개발자가 자체적으로 해당 문제를 인지하고 수정해야한다.
컴파일 에러(Compillation Error)
컴파일 에러는 컴파일 단계에서 오류를 발견하여, 컴파일러가 에러 메세지를 출력해주는 것을 말한다.
대표적인 원인으로 문법 오류(syntax error)가 있고, 에디터에서 맞춤법이나 문장부호가 맞지 않거나 선언되지 않은 변수를 사용하는 등의 코드를 작성하면 빨간줄로 잘못 되었다고 컴파일 에러를 일으킨다.
사실 컴파일 에러는 컴파일을 해야 알 수 있지만, IDE에서 일정 주기로 계속해서 컴파일을 해주기 때문에 문제를 바로 알 수 있는 것이다.
컴파일 에러는 컴파일이 안된다는 의미로, 프로그램이 만들어지지 않기 때문에 프로그램 실행이 불가능하고 코드를 수정하면 되는 일이기 때문에 오류 자체는 그리 심각하지 않게 취급한다.
런타임 에러(Runtime Error)
컴파일 상에는 문제가 없더라도, 프로그램 실행 중에 에러가 발생해 잘못된 결과를 얻거나 외부적인 요인으로 인해 프로그램이 비정상적으로 종료되는 경우 실행 오류(런타임 에러)라 부른다.
대체로 논리적으로 설계가 잘못되어 발생하는 에러가 대부분이며, 런타임 에러 발생 시 프로그래머가 역추적해서 원인을 확인하고 문제를 해결해야한다.

에러(Error)와 예외(Exception)

자바에서는 실행 시(runtime) 발생할 수 있는 오류를 에러(Error)예외(Exception)으로 구분해두었다.
에러(Error)
프로그램 코드에 의해 수습될 수 없는 심각한 오류 발생
메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError) 등 예측 불가능하고 복구 불가능한 오류
예외(Exception)
프로그램 코드에 의해 수습될 수 있는 다소 미약한 오류 발생
대부분 알고리즘 오류나 설계 오류로 발생하는 복구 가능한 오류로, 오류를 예측하여 try-catch 문법을 통해 대응할 수 있다.

자바의 예외(Exception) 클래스

예외 클래스의 계층 구조
자바에서는 오류를 Error와 Exception으로 나누었고, 이들을 클래스로 구현하여 처리하도록 했다.
JVM은 프로그램을 실행하는 중에 예외가 발생하면 해당 예외 클래스를 객체로 생성하고, 예외 처리 코드에서 예외 객체를 이용하여 getMessage()printStackTrace() 메서드를 활용해 예외 객체의 메서드를 가져와 오류를 출력해준다.
자바의 오류 클래스 계층 구조는 이와 같이 구성되어 있는데, Error 클래스는 언급한 것처럼 외부적인 요인으로 발생하는 오류로 개발자가 대처할 수 없다.
자바에서 다루는 모든 예외 종류는 Exception 클래스에서 처리하게 된다.
Exception 클래스도 이와 같이 컴파일 에러와 런타임 에러를 따로 클래스로 나뉘게 된다.
Throwable 클래스의 경우 오류나 예외의 메세지를 담는 역할을 맡는다. getMessage() 메서드와 printStackTrace() 메서드가 Throwable 메서드에 속해 있다. 모든 예외와 에러 클래스는 Throwable 클래스를 상속받는다.

컴파일 예외 클래스 종류

예외 타입
설명
IOException
입출력을 다루는 과정에서 문제가 발생하는 경우
FileNotFoundException
파일에 접근하는데 파일을 찾지 못했을 경우 발생

런타임 예외 클래스 종류

예외 타입
설명
ArithmeticException
어떤 수를 0으로 나누는 것과 같이 비정상 계산 중 발생
NullPointerException
NULL 객체 참조 시 발생
IllegalArgumentException
메서드의 전달 인자값이 잘못될 경우 발생
IllegalStateException
객체의 상태가 메서드 호출에 부적합할 경우 발생
IndexOutOfBoundsException
index 값이 범위를 넘어갈 경우 발생
UnsupportedOperationException
객체가 메서드를 지원하지 않는 경우 발생
SecurityException
보안 위반 발생 시 보안 관리 프로그램에서 발생
ProviderException
구성 공급자 오류 시 발생
NoSuchElementException
구성요소가 그 이상 없는 경우 발생
ArrayStoreException
객체 배열에 잘못된 객체 유형 저장 시 발생
ClassCastException
클래스 간 형 변환 오류 시 발생
EmptyStackException
스택이 비어있는데 요소를 제거하려고 할 경우 발생

검사 예외(Checked Exception)와 비검사 예외(Unchecked Exception)

컴파일 에러와 런타임 에러의 구분 이외에, 검사 예외와 비검사 예외로도 구분된다.
일반적으로 검사 예외는 컴파일 예외 클래스를 말하고, 비검사 예외는 런타임 예외 클래스를 가리키는 것으로 보면 된다.
검사 예외
비검사 예외
처리 여부
반드시 예외를 처리해야 함
명시적 처리를 하지 않아도 됨
확인 시점
컴파일 시
런타임 시
예외 종류
RuntimeException을 제외한 대부분 Exception 클래스
RuntimeException과 그 하위 클래스
검사 예외와 비검사 예외의 가장 핵심적인 차이는 반드시 예외를 처리해야 하는가이다.
검사 예외는 throws로 호출하는 메서드에서 발생할 예외가 지정되어 있어, 해당 예외를 try-catch 문법으로 처리하거나 throws로 바깥으로 전파하지 않으면 컴파일 자체가 되지 않는다.
반면 비검사 예외는 명시적인 예외 처리를 하지 않아도 컴파일은 된다. 비검사 예외의 경우 개발자가 주의를 기울인다면 회피할 수 있고, 예외가 발생하더라도 미약한 예외라서 자바 컴파일러에서 별도의 예외를 처리하지 않도록 설계되어 있다.
한 가지 짚고 넘어가야할 점은 검사 예외와 비검사 예외 모두 transaction처럼 roll-back하지 않는다는 것이다. 예외가 발생하면 그 자체로는 roll-back이 되지 않고, try-catch로 개발자가 직접 롤백을 할지 아니면 그대로 처리할지를 결정해야 한다.

예외(Exception) 처리하기

try-catch 문
예외 처리를 위한 문법으로 try 블록에는 예외가 발생 가능하거나 발생이 예상되는 코드가 위치하여, 만약 try 블록 내의 코드에서 오류가 발생하면 예외 클래스에 맞는 catch 블록으로 가서 블록 안의 코드를 실행 시킨다.
오류가 발생하지 않았다면 catch 문은 실행하지 않고, 뒤에 finally 문이 있다면 오류가 발생했든 발생하지 않았든 finally 블록 안의 코드가 실행된다.
try { System.out.println("나눗셈 : " + (num1 / num2)); } catch (ArithmeticException e) { System.out.println("나눗셈 연산 오류 : " + e.getMessage()); } finally { System.out.println("나눗셈 끝"); }
Java
복사
위 코드에서는 숫자를 0으로 나누는 산술적인 오류가 발생하면, 해당 오류를 catch하여 메세지를 출력하는 예시 코드이다.
이렇게 catch 문을 통해 오류가 발생한 것을 복구하거나, 다른 예외로 처리할 수 있다.
예외 메세지 출력
catch 문의 Exception은 클래스 타입으로 e라는 변수를 통해서 해당 클래스 안의 여러 메서드들을 사용할 수 있다.
printStackTrace() : 예외 발생 당시의 호출 스택(Call Stack)에 있었던 메서드들의 정보와 예외 메세지를 출력한다.
getMessage() : 발생한 예외 클래스의 인스턴스에 저장된 예외 메세지를 가져올 수 있다.
try-with-resource
보통 resource란 외부의 데이터(DB, Network, File)을 말하는데, 이런 resource들은 자바 내부의 요소들이 아니기 때문에 외부에 있는 데이터에 접근하려할 때 예외가 발생할 여지가 존재한다.
특히 입출력 관련된 resource들은 자원의 관리를 위해 사용하고 나면 닫는(close) 것이 굉장히 중요하다.
public class Main { public static void main(String[] args) { FileWriter file = null; try { file = new FileWriter("data.txt"); file.write("Hello World"); } catch (IOException e) { throw new RuntimeException(e); } finally { file.close(); // 작업중에 예외가 발생하더라도 파일이 닫히도록 finally블럭에 넣음 // 근데 close()가 예외를 발생시키면 문제가 됨 } } }
Java
복사
위 코드는 파일에 출력하는 과정에서 발생할 수 있는 IOException을 처리했지만, 파일을 닫는 과정에도 IOException이 발생할 수 있기 때문에 컴파일 되지 않는다.
public class Main { public static void main(String[] args) { FileWriter file = null; try { file = new FileWriter("data.txt"); file.write("Hello World"); } catch (IOException e) { throw new RuntimeException(e); } finally { // close()에서 발생하는 예외를 처리하기 위해서 아래와같이 바꿀수도 있지만 코드가 복잡해져서 좋지않다. try { file.close(); } catch (IOException e) { throw new RuntimeException(e); } } } }
Java
복사
결과적으로 3줄의 동작을 위해 try-catch가 덕지덕지 붙은 이런 가독성 낮은 코드가 완성되는데, 자바에서는 이런 문제를 해결하기 위해 try-with-resource 문법을 도입하였다.
try-with-resource 문은 아래와 같이 try 블록에 괄호()를 추가하여 파일을 열거나 자원을 할당하는 명령문을 넣으면, try 블록이 끝나자마자 자동으로 파일을 닫거나 할당된 자원을 해제한다.
try (파일을 열거나 자원을 할당) { ... }
Java
복사
이 문법을 도입하면 위의 파일을 열고 닫는 예시도 아래처럼 간결하게 작성할 수 있다.
public class Main { public static void main(String[] args) { FileWriter file = null; try (file = new FileWriter("data.txt")) { file.write("Hello World"); } catch (IOException e) { throw new RuntimeException(e); } } }
Java
복사
이런 try-with-resource 문으로 사용되기 위해서는 AutoCloseable 인터페이스를 구현하고 있어야 한다.
위의 FileWriter 클래스 역시 AutoCloseable 인터페이스를 구현하였기 때문에, 위와 같이 try-with-resource 문에 사용할 수 있는 것이다.

Exception Handling

예외를 try-catch로 잡는다고 문제가 완전히 해결된 것이 아니며, 예외를 예상할 수 있다면 이를 해결하고 예상할 수 없는 예외라면 회피하거나 복구 동작을 수행하는 예외 핸들링의 과정이 필요하다.
예외를 처리하는 방법에는 예외 복구, 예외 처리 회피, 예외 전환이 있다.
예외 복구
예외 상황을 파악하고 문제를 해결하여 정상상태로 돌려놓는 방법이다.
반복문을 통해, 예외가 발생하더라도 일정 횟수만큼 재시도하여 예외 복구를 시도한다
최대 반복 횟수를 넘기게되면 다른 예외를 발생시키거나 다른 방법으로 예외를 처리한다.
final int MAX_RETRY = 100; public Object someMethod() { int maxRetry = MAX_RETRY; while(maxRetry > 0) { try { // ... return; // 성공시 바로 리턴 } catch(Exception e) { // 예외 발생시 로그를 출력 } finally { // 리소스 반납 및 정리 작업 } --maxRetry; // 실패하면 1000번 반복 } // 최대 재시도 횟수를 넘기면 직접 예외를 발생 throw new RetryFailedException(); }
Java
복사
예외 처리 회피
예외를 직접 처리하여 해결하는게 아닌 호출한 쪽으로 던져(throws) 회피하는 방법이다.
호출한 쪽에서 예외를 처리할 필요가 있는 경우에 사용되고, 그 외의 상황에서는 그리 추천되는 방법이 아니다.
public void add() throws SQLException { try { // ... 생략 } catch(SQLException e) { e.printStackTrace(); // 로그만 출력하고 throw e; // 다시 날린다 } }
Java
복사
예외 전환
예외 처리 회피와 비슷하게 메서드 밖으로 예외를 던지는 방법이지만, 적절한 예외로 필터링해서 넘기는 방법이다.
발생한 예외를 다른 예외로 변경 혹은 포장(wrap)하여 throws 하는 방법이다.
public void someMethod() throws EJBException { try { // ... } catch(NamingException | SQLExceptionne | RemoteException e) { // 상세한 예외가 들어와도 throw new EJBException(e); // 상위 예외클래스로 퉁쳐서 포장해서 던진다 } }
Java
복사
Exception Handling 주의사항
1.
catch 시에는 로깅이나 복구 등의 로직을 추가하기
예외를 아무 로직 없이 catch만 하거나 catch하여 단순 throw로 던지는 것은 아무것도 하지 않는 것과 같고 바람직하지 않다.
로그를 출력하거나 문제를 복구 시키는 로직을 추가하여 해당 예외에 대해 처리해주어야 한다.
2.
예외 Stack을 남겨 추적, 유지보수성 높이기
예외의 추적와 유지보수성을 높이기 위해, e.toString()이나 e.getMessage()로 예외 메세지만 남기기보다 전체 Exception Stack을 다 넘기는 편이 좋다.
로깅 라이브러리인 Slf4j 라이브러리의 log.error() 역시 e.printStackTrace()처럼 Exception의 stack을 남긴다.
3.
Logging Framework 사용하기
단순히 e.printStackTrace()를 사용하기보다 여러 로깅 라이브러리 프레임워크(slf4j, common loggins, log4j, logback 등)를 활용하자.
프레임워크에서 제공하는 여러 유용한 기능들을 사용하여 더 쉽고 편하게 로그를 남길 수 있다.

예외 던지기

예외 던지기(throw)
프로그램적으로 에러가 아니더라도, 로직상 개발자가 에러를 강제로 발생시켜 로그에 기록하고 싶은 상황이 있을 수 있다.
자바에서는 throw 키워드를 사용하여 강제로 예외를 발생시킬 수 있는데, 이렇게 발생시킨 에러나 검사 예외에 대해서는 try-catch 문을 적용해 발생시키거나 발생될 예외에 대한 처리를 할 수 있다.
public class Main { public static void main(String[] args) { try { if (Integer.parse(args[0]) < 0) throw new IndexOutOfBoundsException("음수는 허용되지 않습니다."); System.out.println("입력 값 : " + args[0]); ... } catch (Exception e) { System.out.println(e.getMessage()); } } }
Java
복사
예외 전파하기(throws)
예외를 던지거나 예외가 발생할 수 있는 코드를 작성하면 try-catch 문으로 처리할 수도 있지만, 메서드에 thorws 키워드를 추가하여 해당 메서드를 호출하는 곳에서 예외를 처리하도록 예외를 전파할 수 있다.
try-catch 문을 반복해서 사용하면 코드가 길어지고 가독성이 떨어지기 때문에, 발생하는 예외가 많다면 해당 예외들을 전파하여 처리하는 로직을 따로 분리하는 것이 좋다.
이렇게 전파된 예외는 해당 메서드를 호출하는 곳에서 처리하거나 전파하지 않으면 컴파일이 되지 않고, 이런 예외를 검사 예외라 부른다.
public class Main { public static void main(String[] args) { try { exceptionMethod(Integer.parse(args[0])); } catch (Exception e) { System.out.println(e.getMessage()); } } public static void exceptionMethod(Integer num) throws IndexOutOfBoundsException { if (num < 0) throw new IndexOutOfBoundsException("음수는 허용되지 않습니다."); System.out.println("입력 값 : " + num); ... } }
Java
복사

연쇄 예외(Chained Exception)

연쇄 예외
연쇄 예외는 하나의 예외가 발생할 것을 예상하여 try-catch 문으로 감싸 다른 예외를 던지도록 만드는 것이다.
예외 A가 발생했다면 이를 예외 B로 감싸서 던지는 것으로, A를 B의 원인 예외(cause exception)이라 한다.
모든 예외가 상속받는 Throwable 클래스에는 getMessage()와 printStackTrace() 메서드 이외에도 아래의 메서드들을 지원하여 연쇄 예외를 가능하게 한다.
Throwable initCause(Throwable cause) : 매개변수로 받은 예외를 원인 예외로 등록
Throwable getCause() : 원인 예외를 반환
class InstallException extends Exception { ... } class SpaceException extends Exception { ... } class MemoryException extends Exception { ... } public class Main { public static void main(String[] args) { try { install(); } catch (InstallException e) { System.out.println("원인 예외 : " + e.getCause()); // 원인 예외 출력 e.printStackTrace(); } } public static void install() throws InstallException { try { throw new SpaceException("설치할 공간이 부족합니다."); // SpaceException 발생 } catch (SpaceException e) { InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성 ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정 throw ie; // InstallException을 발생시켜 상위 메서드로 throws 된다. } catch (MemoryException e) { // ... } } }
Java
복사
검사 예외를 비검사 예외로 변환하기
검사 예외의 경우 try-catch 문법을 사용하거나 throws로 전파하지 않으면 컴파일이 되지 않는데, 이렇게 모든 검사 예외마다 예외 처리를 해주는 것은 귀찮기도 하고 가독성도 떨어지게 된다.
이런 식으로 처리하도록 설계된 이유는 견고한 프로그램을 작성하도록 유도하는게 목적이었지만, 컴퓨터 환경이 많이 달라져 런타임 예외로 처리해도 될 사항들이 아직 검사 예외로 등록되어 있어 예외 처리를 해주어야 한다.
이런 불편함을 해결하기 위해 chained exception을 이용해, 검사 예외를 비검사 예외로 변환할 수 있다.
public class Main { public static void main(String[] args) { FileWriter file = new FileWriter("data.txt"); file.write("Hello World"); } }
Java
복사
위의 file.write() 메서드의 경우 IOException을 검사 예외로 달고 있어 해당 예외를 처리하지 않으면 컴파일 오류가 발생하는데, 아래처럼 try-catch로 감싸 비검사 예외로 변환할 수 있다.
public class Main { public static void main(String[] args) { FileWriter file = new FileWriter("data.txt"); try { file.write("Hello World"); } catch (IOException e) { throw new RuntimeException(new IOException("설치할 공간이 부족합니다.")); // Checked 예외인 IOException을 Unchecked 예외인 RuntimeException으로 감싸 Unchecked 예외로 변신 시킨다 } } }
Java
복사