프로그램에서 오류가 발생하면 시스템 레벨에서 프로그램에 문제를 야기하여 원치 않는 버그를 일으키거나, 심각하면 실행 중인 프로그램을 강제로 종료시키도 한다.
프로그램 오류의 원인으로는 정말 다양한 상황이 있을 수 있다. 내부적인 요인으로는 프로그램 설계 로직에 구멍이 있어서 그럴수도 있고, 외부적인 요인으로는 프로그램 자체 문제가 아닌 하드웨어에서 문제가 생겨 프로그램에 오류가 발생할 수 도 있다.
실제로 프로그래밍 할때 겪어본 오류의 종류로는 단순 오타 서부터 시작해서 파일을 불러오는데 정작 파일이 없거나 등 잘못된 설계로 인한 메모리 펑크 까지 범위도 다양하다.
프로그래밍에서는 이러한 오류를 발생 시점에 따라 크게 3가지로 나눈다.
논리적 에러는 이른바 '버그' 라고 생각하면 된다.
프로그램이 실행하고 작동하는데는 아무런 문제가 없는 오류이지만, 결과가 예상과 달라 사용자가 의도한 작업을 수행하지 못하게 되어 서비스 이용에 지장이 생길 수 있다.
예를들어 재고량이 음수가 나오면 안되는데 음수가 나와버리는 경우, 게임 캐릭터가 피가 0이어도 죽지 않는 경우를 들 수 있다.
논리적 오류는 컴퓨터 입장에서는 프로그램이 멀쩡히 돌아가는 것이니 에러 메시지를 알려주지 않는다. 따라서 개발자는 프로그램의 전반적인 코드와 알고리즘을 체크 필요가 있다.
컴파일 에러는 컴파일 단계에서 오류 발견하면 컴파일러가 에러 메시지 출력해주는 것을 말한다.
컴파일 에러 발생의 대표적인 원인으로 문법 구문 오류(syntax error)를 들 수 있다.
예를들어 에디터에서 코딩을 할때 맞춤법, 문장부호(;), 선언되지 않은 변수 사용을 하면 아래와 같이 빨간줄로 잘못 되었다라고 컴파일 에러를 일으킨다.
하지만 사실 컴파일 에러는 그렇게 심각하게 볼 오류 종류는 아니다.
왜냐하면 컴파일 에러가 있다는 것은, 곧 컴파일이 안된다는 의미이며, 이는 즉 프로그램이 만들어지지 않아 프로그램 실행 자체가 불가하기 때문이다. 따라서 개발자는 차후에 일어날 에러를 컴파일러가 미리 멘토링 한다고 생각하며 코드를 수정하면 될 일이다.
사실 컴파일 에러는 소스 코드를 javac.exe로 컴파일 하는 과정에서 컴파일러가 전반적인 코드를 체크해서 에러 메세지를 보여주는 형태이지만, IDE에서는 일정 주기로 계속 자동으로 컴파일을 해주기 때문에 바로바로 문제를 알 수 있는 것이다.
컴파일 에러를 꼼꼼하게 잡아 컴파일에는 문제가 없더라도, 프로그램 실행 중에 에러가 발생해서 잘못된 결과를 얻거나, 혹은 외부적인 요인으로 기계적 결함으로 프로그램이 비정상적으로 종료될 수 있다.
이것이 우리가 집중적으로 파헤쳐 봐야 할 실행 오류(런타임 에러) 이다.
대체로 개발 시 설계 미숙(논리적)으로 발생하는 에러가 대부분이며, 런타임 에러 발생 시 프로그래머가 역추적해서 원인 확인해야 한다. 따라서 이러한 잠재적인 런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 경우의 수를 고려하여 이에 대한 대비를 철저히 해야 한다.

자바 프로그래밍에서는 실행 시(runtime) 발생할 수 있는 오류를 에러(error)와 예외(exception) 두가지로 구분 하였다.
에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
Error는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고 예측이 불가능한 녀석이다. 즉, 에러는 JVM 실행에 문제가 생긴 것이므로 개발자가 대처할 방법이 없다.
반면 Exception은 발생하더라도 수습될 수 있는 비교적 덜 심각한 오류를 말한다. 즉 알고리즘 오류로 Exception 예외가 계속 발생한다고 해도 Error 처럼 프로그램이 죽거나 그럴경우는 적기 때문이다.
그렇다고 예외(Exception)가 단어의 어감 처럼 가볍게 볼 녀석은 아니다. 예외에 대한 오류 처리를 제대로 하지 않으면 전혀 예상하지 못한 오류 발생으로 프로그램에 작지 않은 문제를 야기하기 때문이다.
대부분의 예외(Exception)는 개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해 발생한다. 그래서 예외는 에러와 달리 문제가 발생하더라도 이에 대한 대응 코드를 미리 작성해 놓음으로써 어느정도 프로그램의 비정상적인 종료 혹은 동작을 막을 수 있다.
이 예외에 대한 대응 코드가 자바의 예외 처리 문법(try - catch)이 되겠다.
따라서 개발자는 예외 처리(exception handling) 를 통해 언제나 예외 상황을 처리하여 프로그램이 종료되는 일이 없록 코드의 흐름을 바꿀 필요가 있다.
정리
예외 처리(exception handling)
예외 처리란 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 행위를 말한다.
프로그램 실행도중에 발생하는 에러는 어쩔 수 없지만, 예외는 프로그래머의 실력에 따라 충분히 포괄적으로 방지할 수 있기 때문이다. 따라서 예외 처리의 목적은 예외의 발생으로 인한 실행중인 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것이다.

자바에서는 오류를 Error와 Exception으로 나누었고 이들을 클래스로 구현하여 처리하도록 하였다.
JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성하고서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해준다.
Error 클래스는 위에서 언급한 바와 같이 외부적인 요인으로 인해 발생하는 오류이기 때문에 개발자가 대처 할 수는 없다. 따라서 우리가 중점적으로 봐야할 클래스는 바로 Exception 클래스이다.
Throwable 클래스란? [예외 메세지 출력]
Error와 Exception 모두 자바의 최상위 클래스인 Object를 상속받는다.
그리고 그 사이에는 Throwable 클래스와 상속관계가 있는데, Throwable 클래스의 역할은 Error와 Exception에 대한 메시지를 담는 것이다. 대표적으로getMessage()(= 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.)와printStackTrace()(= 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력) 메서드가 바로 이 클래스에 속해 있다.
당연히 Throwable을 상속받은 Error와 Exception 클래스에서도 위 두 메서드를 사용할수 있게 된다.

RuntimeException 클래스(프로그래머의 실수로 발생하는 예외)
ArrayIndexOutOfBoundsException : 배열의 범위를 넘어선 인덱스를 참조할 때 발생하는 에러
ArithmeticException : 정수를 0으로 나눌 때 발생하는 에러
NullPointerException : 객체에 접근해서 method를 호출하는 경우 발생하는 에러 (객체가 없는 상태에서 객체를 사용하려 했으니), 자바 프로그램에서 가장 빈번하게 발생하는 에러
String s = null;
System.out.println(s.length());
NumberFormatException : 정수가 아닌 문자열을 정수로 변환할 때 예외 발생, 개발을 하다보면 문자열로 되어있는 데이터를 숫자타입으로 변경하는 경우가 자주발생하는데, 숫자타입으로 변경할 수 없는 문자를 치환시키려고 하면 발생하는 대표적인 에러
String stringNumber = "3.141592";
int num = Integer.parseInt(stringNumber); // "3.141592"를 정수로 변환할 때 NumberFormatException 예외 발생
// float num = Float.parseFloat(stringNumber); (parsefloat 으로 변경해 주어야 함)
ClassCastException : 타입 변환은 상위 클래스와 하위 클래스간의 상속 관계 이거나 혹은 구현 클래스와 인터페이스간 일 때만 가능하다.
상속, 구현 관계 아니면 클래스는 다른 클래스로 타입을 변환할 수 없는데, 이 규칙을 무시하고 억지로 타입을 변환시킬경우 발생하는 에러이다.
Object x = new Integer(0);
System.out.println( (String)x ); // 정수 객체를 스트링 객체로 캐스팅
InputMismatchException : 의도치 않는 입력 오류 시 발생하는 예외
import java.util.Scanner;
import java.util.InputMismatchException;
public class InputException {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("정수 3개를 입력하세요");
int n = scanner.nextInt(); // 정수 입력 -> 사용자가 문자를 입력하면 예외 발생
scanner.close();
}
}
Exception 및 하위 클래스(사용자의 실수와 같은 외적인 요인에 의해 발생하는 컴파일시 발생하는 예외)
IOException : 컴퓨터 프로그램이 실행될 때 언제 어떤 문제가 발생할지 모르는 일이기 때문에, 컴퓨터와 상호소통 하는 I/O(입력과 출력)에 관해서는 발생할 수 있는 예외에 대해서 까다롭게 규정하고 있다.
그래서 입력과 출력을 다루는 메서드에 예외처리(IOException)가 없다면 컴파일 에러가 발생하게 된다.
아래 코드는 write() 메소드에서 발생할 수 있는 IOException에 대한 예외를 처리하지 않았으므로 컴파일 시 오류가 발생한다.
참고로 print() , println() 메서드를 출력했는데 아무 문제가 없는 이유는 자체적으로 컴파일 예외처리를 미리 해놓았기 때문이다.
public class Exception01 {
public static void main(String[] args) {
byte[] list = {'a', 'b', 'c'};
System.out.write(list);
}
}
FileNotFoundException : 파일에 접근하려고 하는데 파일을 찾지 못했을 때 발생하는 에러
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new FileReader("test.txt")); // 만일 폴더에 test.txt 파일이 없는데 가져올 경우
br.readLine();
br.close();
}
}
컴파일러가 예외 처리 여부를 체크하느냐, 체크하지 않느냐의 차이
Checked Exception은 컴파일 예외클래스들을 가리키는 것이고, Unchecked Exception은 런타임 예외클래스들을 가리키는 것으로 보면 된다.

Checked Exception
Checked Exception은 체크 하는 시점이 컴파일 단계이기 때문에, 별도의 예외 처리를 하지 않는다면 컴파일 자체가 되지 않는다.
따라서 Checked Exception이 발생할 가능성이 있는 메소드라면 반드시 로직을 try - catch로 감싸거나 throws로 던져서 처리해야 한다.
// try - catch 로 예외처리
public static void fileOpen() {
// 파일을 열고 쓰고 닫는 아주 단순한 로직이어도 이에 대한 예외는 checked exception으로 분류 되기 때문에 반드시 try - catch로 감싸주어야 한다.
try {
FileWriter file = new FileWriter("data.txt");
file.write("Hello World");
file.close();
} catch(IOException e) {
e.printStackTrace();
}
}
// -------------------------------------------------------------------------
// throws 로 예외처리
public static void fileOpen() throws IOException {
// 파일을 열고 쓰고 닫는 아주 단순한 로직이어도 이에 대한 예외는 checked exception으로 분류 되기 때문에 반드시 try - catch로 감싸주어야 한다.
FileWriter file = new FileWriter("data.txt");
file.write("Hello World");
file.close();
}
Unchecked Exception
Unchecked Exception도 예외이긴 하지만, 개발자의 충분한 주의로 미리 회피할 수 있는 경우가 대부분이라 그나마 상대적으로 미약한 예외로 처리되어 자바 컴파일러는 별도의 예외 처리를 하지 않도록 설계 되어 있기 때문이다.
따라서 에러를 일부러 일으키는 코드가 있더라도 try - catch 처리하지 않더라도 컴파일도 되고 실행까지 가능하다.
public class Main {
public static void main(String[] args) {
// 일부로 예외를 무한적으로 발생시켜도 에러로그만 쌓이지 프로그램 자체는 왠만해선 죽지는 않는다. (미약한 오류이기 때문에)
while(true) {
String s = null;
s.length(); // NullPointerException - Unchecked Exception 이어서 예외를 발생시키는 옳지 못한 코드임에도 불구하고 빨간줄이 없다
}
}
}
try 블록에는 예외발생 가능 코드가 위치하고 만일 코드에 오류가 발생되면, 오류 종류(예외 클래스)에 맞는 catch 문으로 가서 catch 블록 안에 있는 코드를 실행 시킨다. 만일 오류가 발생하지 않으면 catch 문은 실행하지 않는다.

catch 문을 보면 예외클래스 타입과 변수 e 가 선언되어 있는데, 만일 try문에서 예외가 발생하면 그 예외에 맞는 예외클래스가 catch문에 아규먼트로 선언되어 있으면 실행되어 옳지 못한 동작에 대해서 대비를 할 수 있다.

예외 클래스 갯수만 해도 수백개인데 이것을 일일히 코드에 다 작성하는 것은 무리이다.
따라서 클래스의 상속 관계(다형성)를 이용하여 예외클래스의 상위 클래스인 Exception 클래스 타입을 catch문 아규먼트에 선언하면, 코드 몇줄만으로 자바의 나머지 모든 예외 클래스를 catch 문으로 받아들일 수 있게 된다.
다만, 세세하게 어떠어떠한 예외인지는 부모 클래스인 Exception 클래스만으로는 알수 없게 된다는 단점이 있다.
public class Exception {
public static void main(String[] args) {
int a, b, c;
try {
// ... 길다란 코드
// ... 길다란 코드
// ... 길다란 코드
} catch (NumberFormatException e) {
System.out.println("숫자로 변환할 수 없습니다.");
} catch (ClassNotFoundException e) {
System.out.println("클래스가 존재하지 않습니다.");
} catch (Exception e) { // 부모 예외 클래스로 한꺼번에 처리했기 때문에 세세한 예외 클래스 종류는 지금은 알 수는 없다.
System.out.println("NumberFormatException와 ClassNotFoundException 이외에 모르는 어떠한 에러가 발생하였습니다");
}
}
}
printStackTrace()메서드를 catch 문 안에서 실행하면 부모 예외 클래스로 한꺼번에 예외 상황을 받아들여도 어떠한 예외 상황인지 세세하게 출력하여 추적할 수 있다.
어떤 예외가 발생하더라도 반드시 실행되어야 하는 부분이 있어야 한다면 finally 문으로 처리가 가능하다.
Sample sample = new Sample();
try {
sample.addSample(100);
sample.printSample(); // 만일 이 메서드를 실행하는데 에러가 나버리면 !
} catch (Exception e) {
// ... catch 문의 코드가 실행되고
} finally {
sample.shouldBeRun(); // 에러가 나든 안나든 무조건 finally 문은 실행된다.
}
심지어 메소드의 return문이 있어도 일단 finally의 코드를 실행하고 리턴한다.
예외가 발생한 경우에는 try → catch → finally 의 순서로 실행되고, 예외가 발생하지 않는 경우에는 try → finally 의 순으로 실행된다고 보면 된다.
catch 블럭을 | 기호를 통해서 하나의 catch 블럭으로 합칠 수 있게 되었다.
catch문이 연달아 나열되는 중복된 코드를 줄일 수 있으며 연결할 수 있는 예외 클래스의 개수에도 제한이 없다.
try {
// ...
} catch (NullPointException | ArrayIndexOutOfBoundsExcetion e) {
// ...
}
멀티 catch는 결국은 위의 부모 예외 클래스 Exceptoin과 같이 여러 개의 예외를 통짜로 처리하는 것이기 때문에 각 예외마다 세세하게 제어하고 싶다면 if문과 instanceOf 연산자로 하나하나 분기하며 처리해야 한다.
try {
// ...
} catch (NullPointException | ArrayIndexOutOfBoundsExcetion e) {
if(e instanceOf NullPointException) {
// ...
} else if(e instanceOf ArrayIndexOutOfBoundsExcetion) {
// ...
}
}
예외 발생시키기 (throw)
만일 프로그램적으로 에러가 아니라도 로직상 개발자가 일부러 에러를 내서 로그에 기록하고 싶은 상황이 올 수 있다.
자바에서는 throw 키워드를 사용하여 강제로 예외를 발생시킬 수 있다.
원래는 프로그램이 알아서 에러를 탐지하고 처리 하였지만, 이번에는 사용자가 일부러 에러를 throw하여 에러를 catch 한다는 개념으로 보면 된다.
이때 new 생성자로 예외 클래스를 초기화하여 던져는데, 이 클래스 생성자에 입력값을 주게되면, catch문의 getMessage() 메서드에서 출력할 메세지를 지정하게 된다.
package exception;
import java.util.Scanner;
public class ExceptionMain2 {
private int x,y;
public void input() {
Scanner sc = new Scanner(System.in);
System.out.print("x 입력 : ");
this.x = sc.nextInt();
System.out.print("y 입력 : ");
this.y = sc.nextInt();
}
public void output(){
if(y >= 0) {
int mul =1;
for(int i=1; i<=this.y; i++) {
mul *= this.x;
}
System.out.println(x+"의"+y+"승은 "+mul);
}else {
//System.out.println("y는 0보다 크거나 같아야 한다.");
//개발자가 강제로 Exception 발생
try {
throw new Exception("y는 0보다 크거나 같아야 한다.");
}catch(Exception e) {
System.out.println(e.getMessage());
//e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExceptionMain2 exceptionMain2 = new ExceptionMain2();
exceptionMain2.input();
exceptionMain2.output();
}
}
예외 떠넘기기 (throws)
예외가 발생할 수 있는 코드를 작성할 때 try - catch 블록으로 처리하는 것이 기본이지만, 경우에 따라서는 다른 곳에서 예외를 처리하도록 호출한 곳으로 예외를 떠넘길 수도 있다.
이때 사용하는 키워드가 throws이다. throws 는 메소드 선언부 끝에 작성되어 메소드에서 예외를 직접 처리(catch)하지 않은 예외를 호출한 곳으로 떠넘기는 역할을 한다.
public class Main {
public static void main(String[] args) {
try {
method1();
method2();
method3();
} catch (ClassNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println(e.getMessage());
}
}
public static void method1() throws ClassNotFoundException {
throw new ClassNotFoundException("에러이지롱");
}
public static void method2() throws ArithmeticException {
throw new ArithmeticException("에러이지롱");
}
public static void method3() throws NullPointerException {
throw new NullPointerException("에러이지롱");
}
}
트랜잭션(Transaction)은 하나의 작업 단위를 뜻한다.
즉, 자바 코드에서 메서드 블럭내의 코드들이 예외가 발생해도 모두 실행되느냐 아니면 예외가 발생하면 그상태로 중지하느냐의 작업 단위를 개발자가 어떤 형태의 예외 처리 방법을 사용하느냐에 따라 달라지게 된다.
예를들어 각 메서드에서 일일히 try - catch 하면, 메인 메소드에 있는 메서드 실행 코드 부분은 3개 모두 실행 자체는 된다.
왜냐하면 예외처리를 각 메서드에서 하기 때문에 상위의 메인 메서드의 코드들은 모두 실행되게 된다.

반면에 throws를 통해 예외처리를 상위 메서드에서 모아 처리를 한다면, 코드 어느 한곳에서 예외가 발생하면 그 뒤의 나머지 코드들은 당연히 실행되지 않게 된다. (바로 catch로 점프하니까)

이처럼 try - catch 문은 어디에 사용하냐 어디서 throws 하느냐에 따라 자바 코드의 작업 단위(트랜잭션)가 완전히 달라질 수 있게 되는 것이다. 따라서 자신의 프로젝트에 따라 적절한 예외 처리 로직을 짜 주어야 하는 방향으로 나아가야 한다.
연결된 예외(chained exception)는 한 예외가 다른 예외를 발생시킬 수 있는 기능이다.
우리가 클래스를 상속하여 다형성을 이용하여 부모 클래스 타입으로 다뤄온 것 처럼, 예외도 마치 부모 예외로 감싸서 보내 마치 예외의 다형성 처럼 다룰 수 있다.
예를 들어 예외 A가 발생했다면 이를 예외 B로 감싸서 throw하는 식으로, 마치 예외를 다른 예외로 감싸서 던진다고 보면된다. 그래서 예외 A(설치할 공간이 부족)가 예외 B(설치중 예외발생)를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.
Exception 클래스가 상속하고 있는 Throwable 클래스에는 getMessage() 와 printStackTrace() 이외에도 chained exception을 가능하게 해주는 다음 메서드를 지원한다.
Throwable initCause (Throwable cause) : 지정한 예외를 원인 예외로 등록
Throwable getCause() : 원인 예외를 반환
class InstallException extends Exception { ... }
public class Main {
public static void main(String[] args) {
try {
startinstall();
} catch (InstallException e) {
System.out.println("원인 예외 : " + e.getCause()); // 원인 예외 출력
e.printStackTrace();
}
}
public static void startinstall() throws InstallException {
try {
throw new SpaceException("설치할 공간이 부족합니다."); // SpaceException 발생
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
throw ie; // InstallException을 발생시켜 상위 메서드로 throws 된다.
}
}
}

연결된 예외(chained exception)를 사용하는 또 다른 이유는 checked예외를 unchecked예외로 바꿀 수 있도록 하기 위함이다.
예를 들어 checked exception의 종류의 예외를 포함한 코드를 작성하면 컴파일러가 예외 처리(try - catch)를 강제한다.
가장 대표적인 예로 FileWriter 클래스를 이용해 파일을 불러오는 코드를 작성하면 반드시 try - catch로 감싸주어야 컴파일이 된다.
이런식으로 설계한 이유는, 처음 자바 언어를 개발 했을때 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위해서인데, 실제로 별것 아닌 예외도 checked exception 으로 등록한 것이 꽤 많다.
자바 프로그래밍 언어가 처음 개발되던 1990년대와 지금의 컴퓨터 환경은 많이 달라졌기 때문에, 실제로 런타임 예외로 처리해도 될 것들이 아직도 checked exception으로 등록되어 강제적으로 try - catch 문을 사용해야 하는 불편함이 있고, 또한 로직상 Runtime Exception으로 할 수 밖에 없는 경우가 있기 때문에, 추가된 기법이라고 생각하면 된다.
따라서 연결된 예외(chained exception)을 이용해, checked 예외를 unchecked 예외로 바꾸면 예외처리가 선택적이 되므로 억지로 거추장 스러운 예외처리를 하지 않아도 된다.
class MyCheckedException extends Exception { ... } // checked excpetion
public class Main {
public static void main(String[] args) {
install();
}
public static void install() {
throw new RuntimeException(new IOException("설치할 공간이 부족합니다."));
// Checked 예외인 IOException을 Unchecked 예외인 RuntimeException으로 감싸 Unchecked 예외로 변신 시킨다
}
}