프로그래밍 오류의 종류
프로그래밍의 오류
•
프로그램에서 오류가 발생하면 시스템 레벨에서 문제가 발생해 원치않는 버그를 일으키거나, 프로그램이 종료되기도 한다.
•
프로그램 오류의 원인에는 정말 많은 이유와 상황들이 있는데, 단순 오타부터 시작해 설계를 잘못했거나 하드웨어에 문제가 생겨 프로그램에 오류가 발생할 수도 있다.
•
이렇게 프로그래밍에는 여러 오류들을 발생 시점에 따라 크게 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
복사