...
프로그래밍의 오류 종류
프로그램에서 오류가 발생하면 시스템 레벨에서 프로그램에 문제를 야기하여 원치 않는 버그를 일으키거나, 심각하면 실행 중인 프로그램을 강제로 종료시키도 한다.
프로그램 오류의 원인으로는 정말 다양한 상황이 있을 수 있다. 내부적인 요인으로는 프로그램 설계 로직에 구멍이 있어서 그럴수도 있고, 외부적인 요인으로는 프로그램 자체 문제가 아닌 하드웨어에서 문제가 생겨 프로그램에 오류가 발생할 수 도 있다.
실제로 여러분이 프로그래밍 할때 겪어본 오류의 종류로는 단순 오타 서부터 시작해서 파일을 불러오는데 정작 파일이 없거나 등 잘못된 설계로 인한 메모리 펑크 까지 범위도 다양하다.
프로그래밍에서는 이러한 오류를 발생 시점에 따라 크게 3가지로 나눈다.
- 컴파일 에러(compile-time error) : 컴파일시에 발생하는 에러
- 런타임 에러(runtime error) : 실행시에 발생하는 에러
- 논리적 에러(logical error) : 실행은 되지만 의도와 다르게 동작하는것
논리 에러 (Logic Error)
논리적 에러는 이른바 '버그' 라고 생각하면 된다.
프로그램이 실행하고 작동하는데는 아무런 문제가 없는 오류이지만, 결과가 예상과 달라 사용자가 의도한 작업을 수행하지 못하게 되어 서비스 이용에 지장이 생길 수 있다.
예를들어 재고량이 음수가 나오면 안되는데 음수가 나와버리는 경우, 게임 캐릭터가 피가 0이어도 죽지 않는 경우를 들 수 있다.
논리적 오류는 컴퓨터 입장에서는 프로그램이 멀쩡히 돌아가는 것이니 에러 메시지를 알려주지 않는다. 따라서 개발자는 프로그램의 전반적인 코드와 알고리즘을 체크 필요가 있다.
컴파일 에러 (Compillation Error)
컴파일 에러는 컴파일 단계에서 오류 발견하면 컴파일러가 에러 메시지 출력해주는 것을 말한다.
컴파일 에러 발생의 대표적인 원인으로 문법 구문 오류(syntax error)를 들 수 있다.
예를들어 에디터에서 코딩을 할때 맞춤법, 문장부호(;), 선언되지 않은 변수 사용을 하면 아래와 같이 빨간줄로 잘못 되었다라고 컴파일 에러를 일으킨다.
사실 컴파일 에러는 소스 코드를 javac.exe로 컴파일 하는 과정에서 컴파일러가 전반적인 코드를 체크해서 에러 메세지를 보여주는 형태이지만, IDE에서는 일정 주기로 계속 자동으로 컴파일을 해주기 때문에 바로바로 문제를 알 수 있는 것이다.
하지만 사실 컴파일 에러는 그렇게 심각하게 볼 오류 종류는 아니다.
왜냐하면 컴파일 에러가 있다는 것은, 곧 컴파일이 안된다는 의미이며, 이는 즉 프로그램이 만들어지지 않아 프로그램 실행 자체가 불가하기 때문이다. 따라서 개발자는 차후에 일어날 에러를 컴파일러가 미리 멘토링 한다고 생각하며 코드를 수정하면 될 일이다.
런타임 에러 (Runtime Error)
컴파일 에러를 꼼꼼하게 잡아 컴파일에는 문제가 없더라도, 프로그램 실행 중에 에러가 발생해서 잘못된 결과를 얻거나, 혹은 외부적인 요인으로 기계적 결함으로 프로그램이 비정상적으로 종료될 수 있다.
이것이 우리가 집중적으로 파헤쳐 봐야 할 실행 오류(런타임 에러) 이다.
대체로 개발 시 설계 미숙(논리적)으로 발생하는 에러가 대부분이며, 런타임 에러 발생 시 프로그래머가 역추적해서 원인 확인해야 한다. 따라서 이러한 잠재적인 런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 경우의 수를 고려하여 이에 대한 대비를 철저히 해야 한다.
오류(error) 와 예외(exception)
자바 프로그래밍에서는 실행 시(runtime) 발생할 수 있는 오류를 '에러(error)'와 '예외(exception)' 두가지로 구분 하였다.
- 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
- 예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
Error은 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고 예측이 불가능한 녀석이다. 즉, 에러는 JVM 실행에 문제가 생긴 것이므로 개발자가 대처할 방법이 없다.
반면 Exception은 발생하더라도 수습될 수 있는 비교적 덜 심각한 오류를 말한다. 즉 알고리즘 오류로 Exception 예외가 계속 발생한다고 해도 Error 처럼 프로그램이 죽거나 그럴경우는 적기 때문이다.
그렇다고 예외(Exception)가 단어의 어감 처럼 가볍게 볼 녀석은 아니다. 예외에 대한 오류 처리를 제대로 하지 않으면 전혀 예상하지 못한 오류 발생으로 프로그램에 작지 않은 문제를 야기하기 때문이다.
대부분의 예외(Exception)는 개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해 발생한다. 그래서 예외는 에러와 달리 문제가 발생하더라도 이에 대한 대응 코드를 미리 작성해 놓음으로써 어느정도 프로그램의 비정상적인 종료 혹은 동작을 막을 수 있다.
이 예외에 대한 대응 코드가 바로 우리가 배울 자바의 예외 처리 문법(try - catch)이 되겠다.
따라서 개발자는 예외 처리(exception handling)를 통해 언제나예외 상황을 처리하여 프로그램이 종료되는 일이 없록 코드의 흐름을 바꿀 필요가 있다.
[ 예외 처리(exception handling) ]
예외 처리란 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 행위를 말한다.
프로그램 실행도중에 발생하는 에러는 어쩔 수 없지만, 예외는 프로그래머의 실력에 따라 충분히 포괄적으로 방지할 수 있기 때문이다. 따라서 예외 처리의 목적은 예외의 발생으로 인한 실행중인 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것이다.
자바의 예외(Exception) 클래스
예외 클래스의 계층 구조
자바에서는 오류를 Error와 Exception으로 나누었고 이들을 클래스로 구현하여 처리하도록 하였다.
우리에게 익숙한 IllegalArgumentException을 비롯해 NullPointerException과 IOException도 모두 클래스이다.
JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성하고서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해준다. 나중에 배울 getMessage() 나 printStackTrace() 메서드 역시 예외 객체의 메서드를 가져와 오류를 출력하는 것이다.
자바의 오류 클래스 계층 구조를 살펴보면 다음과 같이 구성 되어 있다.
Error 클래스는 위에서 언급한 바와 같이 외부적인 요인으로 인해 발생하는 오류이기 때문에 개발자가 대처 할 수는 없다. 따라서 우리가 중점적으로 봐야할 클래스는 바로 Exception 클래스이다.
[ Throwable 클래스란? ]
오류와 예외 모두 자바의 최상위 클래스인 Object를 상속받는다.
그리고 그 사이에는 Throwable 클래스와 상속관계가 있는데, Throwable 클래스의 역할은 오류나 예외에 대한 메시지를 담는 것이다. 대표적으로getMessage()와printStackTrace()메서드가 바로 이 클래스에 속해 있다.
당연히 Throwable을 상속받은 Error와 Exception 클래스에서도 위 두 메서드를 사용할수 있게 된다.
위의 예외 클래스 계층 구조는 조금 보기 복잡하니 심플하게 표현하자면 아래와 같다.
자바에서 다루는 모든 예외 오류는 Exception 클래스에서 처리한다. 그리고 아래의 Exception 클래스 트리 구조를 보면 파랑색과 붉은색으로 색깔별로 구분됨을 볼 수 있는데, 이것이 컴파일 에러와 런타임 에러를 따로 클래스로 구분했기 때문이다.
즉, Exception 클래스는 다시 RuntimeException(런타임 에러를 다룸)과 그 외의 자식 클래스 그룹(컴파일 에러를 다룸)으로 나뉘게 된다.
- Exception 및 하위 클래스 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 컴파일시 발생하는 예외
- 존재하지 않는 파일의 이름을 입력 (FileNotFoundException)
- 실수로 클래스의 이름을 잘못 기재 (ClassNotFoundException)
- 입력한 데이터 형식이 잘못된 경우 (DataFormatException)
- RuntimeException 클래스 : 프로그래머의 실수로 발생하는 예외
- 배열의 범위를 벗어남 (IndexOutOfBoundsException)
- 값이 null인 참조 변수의 멤버를 호출 (NullPointerException)
- 클래스 간의 형 변환을 잘못함 (ClassCastException)
- 정수를 0으로 나누는 산술 오류 (ArithmeticException)
런타임 예외 클래스 종류
코딩하다 보면 자주 보게될 몇가지 중요한 실행(runtime) 예외 클래스에 대해 간단히 정리해보는 시간을 가져본다.
다음 실행 예외 종류를 보고 언제 발생되고, 어떤 오류 메시지가 출력되는지 잘 알아보도록 하자. 숙련된 개발자라면 오류메시지를 보고 바로 오류를 해결할 수 있어야 한다.
ArrayIndexOutOfBoundsException
- 배열의 범위를 넘어선 인덱스를 참조할 때 발생하는 에러
public class RuntimeErrorsExample {
public static void main(String[] args) {
int ary[] = {4, 6, 2}; // ary[0] ~ ary[2]
system.out.println("Result: " + ary[3]);
}
}
ArithmeticException
- 정수를 0으로 나눌 때 발생하는 에러
System.out.println(12/0);
NullPointException
- null 객체에 접근해서 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();
}
}
컴파일 예외 클래스 종류
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
자바의 예외(Exception)는 컴파일 에러와 런타임 에러로 구분된다는 것 쯤은 알고 있을 것이다.
그런데 또다시 예외 종류로서 Checked Exception과 Unchecked Exception 로 나뉜다.
오류면 오류지, 자꾸 오류를 나누고 또 나눠서 머리가 깨질 지경이겠지만 어렵게 생각할 필요 없다.
그냥 Checked Exception은 컴파일 예외클래스들을 가리키는 것이고, Unchecked Exception은 런타임 예외클래스들을 가리키는 것으로 보면 된다.
그냥 컴파일 / 런타임 예외로 분류하면 되지, 또다시 Checked / Unchecked Exception으로 재분류 한 이유는 코드적 관점에서 예외 처리 동작을 필수 지정 유무에 따라 나뉘기 때문이다.
Checked Exception | Unchecked Exception | |
처리 여부 | 반드시 예외를 처리해야 함 | 명시적인 처리를 안해도 됨 |
확인 시점 | 컴파일 단계 | 런타임 단계 |
예외 종류 | RuntimeException을 제외한, Exception 클래스와 그를 상속받는 하위 예외 - IOException - FileNotFoundException - SQLException |
RuntimeException 과 그 하위 예외 - NullPointerException - IllegalArgumentException - IndexOutOfBoundException - SystemException |
코드에서 명시적 예외 처리 유무
두 Checked / Unchecked 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(); // NullPointException - Unchecked Exception 이어서 예외를 발생시키는 옳지 못한 코드임에도 불구하고 빨간줄이 없다
}
}
}
Checked를 Unchecked 예외로 변환하기
앞서 checked exception은 반드시 try - catch 문으로 감싸야된다고 했었다.
하지만 코드마다 일일히 예외처리하는 것도 귀찮고 오히려 가독성을 해친다고 생각할 경우, 그냥 unchecked exception으로 변환시켜 컴파일에게 예외처리를 강제하지 않게 할수있다.
예를 들어 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 예외로 변신 시킨다
}
}
# 참고자료
https://www.youtube.com/watch?v=I4XrVgCzKM4
https://www.youtube.com/watch?v=BUzMuRT8xa0
https://rollbar.com/blog/java-exceptions-hierarchy-explained/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.