728x90

이 강의는 자바의 프로젝트를 어떻게 만드는지, 또한 빌드를 어떻게 하는지에 대한 강의를 진행하지 않는다.

그러나 자바의 기본적인, 그리고 자바에 필수적인 기능들도 분명 존재하고 이 블로그엔 그 작업에 필요한 포스팅도 함께 제공하고 있다.

그 정보를 확인하고 싶다면 여기를 참조하라. 양이 조금 되고 현재진행형으로 늘어나고 있으므로 페이지내의 찾기(ctrl+f 나 cmd+f)로 찾아보도록하자.


예외처리를 배우기 앞서서 반드시 알아야할것은 에러와 예외이다.

사실 두녀석은 어찌보면 비슷하지만 결정적 차이가 있다.

전통적인 프로그래밍에서 에러와 예외는 사실상 거의 같다고 볼 수있다.

두 녀석의 공통점은 일단 발생하면 프로그램의 정상진행이 불가피하므로 프로그램을 종료해야한다는데 있다.

그리고 과거 예외처리가 존재하지 않던 C나 Basic같은 언어의 경우 이 둘은 그냥 뭉뚱그려서 에러라고 불렀다.

하지만 에러와 예외의 결정적인 차이점은 회피할수 있냐 아니냐로 분류된다.


이렇게 이야기하면 너무 추상적이니까 예를 들도록 하자.

대표적인 에러로 스택 오버플로우가 있다. 기본적으로 모든 메모리는 한정자원이다.

이건 굳이 어렵게 설명하지 않아도 모두가 알거라 생각한다.

그러면 변수를 만들고 만들고 또만들다보면 그 변수를 저장하는 스택이라는 그릇이 다차게 된다.

이 다차있는상태에서 또다시 변수를 생성하려고하면 스택 오버플로우가 일어난다.

문제는 이 스택 오버플로우의 경우에 해결하려한다면 미리 예상해서 스택이 다찼다는것을 확인한 후

이미 할당되어있는 변수를 해제시켜버리면된다. 문제는 아무리 비효율적이라도 미리 예상하는것이 가능은 하지만

도대체 어느 변수를 해제시키느냐가 문제이다. 스택이라는 자료구조특성상 해제시켜야한다면

마지막 변수밖에 없을건데 공간직접도(최근에 쓰인것이 자주 쓰인다는 이론)에 의해서 손해볼 확률이 굉장히 높다.

즉 이 에러는 회피가 불가능 하므로 에러를 뱉어내는 것이다.


그러나 예외는 조금 다르다. 대표적인 예로 0을 나누는 DIV/0의 경우에

일단 만약 0을 정말로 나눠버린다면 무한루프를 돌게되므로 프로그램의 정상적인 진행을 할 수 없다.

그러나 나누기 직전 미리 예상해서 0을 나눠버리는 행위를 할것을 판단한 후

실제로는 0을 나누는 일을 실행하기 전에 예외를 뱉어내는 것이다.

즉 프로그래머한테 "야 니 0으로 나누려 한다."라고 예외를 뱉어내면

프로그래머는 그것을 보고 "아 맞다 0으로 나누면 안되지!"라고 생각하고 예외를처리할 수 있는 것이다.


이와 같이 회피가능한 오류를 예외라하고 회피불가능한 오류를 에러라고 부른다.

시간이 더 흘러 컴퓨터가 발달하거나 컴퓨터의 매커니즘이 바뀐다면 기존의 에러들이 예외로 바뀌는 일또한 가능한 것이다.



이 그림은 Error의 클래스 다이어그램이다.

NoClassDefFoundError는 굉장히 자주볼수 잇는 에러 예제중 하나이다.



이 것은 Exception의 클래스 다이어그램이다.

여기나오는 모든 Exception은 언제든지 볼 수 있다.


그럼 예제를 보고 확인해 보자.


package com.jiharu.main;

public class Main {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
System.out.println(num1 / num2);
}
}

이러한 예제가 있다고 가정해보자.

이 코드는 당연히 작동해서는 안된다. 왜냐하면 숫자 연산상 나누기 0을 하면 무한루프를 돌기 때문이다.

그래서 앵간한 컴퓨터들은 이 연산을 막아둔다.

이걸 실행시키면 아래와 같은 메시지를 볼 수 있다.



아마 처음 봤던 사람은 이걸 에러라고 생각할 것같다.

그러나 글을 자세히 읽어보면 java.lang.ArithmeticException이라고 되어있다.

이는 예외가 발생한 것이다. 즉 0으로 나누려고 했기에 예외가 발생한 것이다.

에러와 예외의 차이점은 에러는 무조건 프로그램을 종료해야한다.

그러나 예외는 처리해서 회피할 수 있는 것이다.

만약 여러분이라면 이 상황을 어떻게 회피해나가겠는가?

아마 이렇게 해결할 생각을 할 것이다.

아래의 예제를 보자.


package com.jiharu.main;

public class Main {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
if(num2 != 0){
System.out.println(num1 / num2);
}
else{
System.out.println("0으로 나눌 수 없다.");
}
}
}

이는 전통적인 예외처리 방법이다. 즉 예방이다.

이 방식은 전통적이면서도 아직도 자주 쓰이는 방법이다.

이 방식이 뭐 큰 문제점을 가지진 않았지만 기존의 이 방법은 약간 껄끄러운 점이 있다.

일단 if문이 뭔지를 알아야한다. if문은 분기문이다.

즉 원래의 if문은 특정 상황에서 상황에 나눠서 코딩할 수 있게 만드는걸 역활을 한다.

그러나 위의 코드는 사실 분기로서의 역활로 if문을 썼다기 보단 예외를 처리하기 위해서 사용하고 있다.

그러면 if문 대신 예외를 처리할 방법이 없을까?

당연히 존재한다. 그것이 바로 try문이다.


package com.jiharu.main;

public class Main {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
try{
System.out.println(num1 / num2);
}
catch (ArithmeticException e){
System.out.println("0으로 나눌 수 없다.");
}
}
}

try문을 사용하는 방식은 아래와 같다.


try{

syntax1;

}

catch(Exception e){

syntax2;

}


예외가 발생할 상황을 try문에 넣는다.

그 다음 예외가 발생했을 때 처리할 내용을 catch문에 넣는다.

이제 위의 예제코드를 실행해보자.



try문에서 예외가 발생하면서 catch문으로 넘어간 것을 알 수 있다.

이제 try문에 대해서 조금 더 알아보도록 하자.


package com.jiharu.main;

public class Main {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
try{
System.out.println("나누기 전");
System.out.println(num1 / num2);
System.out.println("나누기 후");
}
catch (ArithmeticException e){
System.out.println("0으로 나눌 수 없다.");
}
}
}

이 코드를 보자. 흐름이 어떨지 감이 잡히는 가?

num1/num2는 실행될 수 없다. 그러므로 반드시 예외는 일어난다.

그러면 나누기 후라는 글자가 출력이 될까?

한번 시행해보자.



num1/num2에서 예외가 발생한다. 이 경우 예외가 발생한 줄 이하의 코드는 실행되지 않는다.

그리고 바로 catch문으로 넘어가게 된다.

그리고 과연 정말로 프로그램이 종료되지 않았을까? 그것도 확인해 보자.


package com.jiharu.main;

public class Main {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
try{
System.out.println("나누기 전");
System.out.println(num1 / num2);
System.out.println("나누기 후");
}
catch (ArithmeticException e){
System.out.println("0으로 나눌 수 없다.");
}
System.out.println("프로그램의 마지막");
}
}

이 구문에서 프로그램의 마지막이 출력이 될까?

이 역시 한번 테스트해보도록 하자.



보면 알겠지만 프로그램이 종료되지 않고 프로그램의 마지막이 출력된걸 확인할 수 있다.


이제 예외에 대해서 어느정도 알았다.

그러나 여기서는 0으로 나누는 예외만 처리하였다.

그럼 그 외의 다른 예외는 발생할 수 없을까?

예제를 조금 바꿔보도록하자.


package com.jiharu.main;

import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없다.");
}
System.out.println("프로그램의 마지막");
}
}

이 예제는 사용자에게 입력을 받아서 두 숫자를 나눈 것이다.

이 코드는 잠재적으로 여러가지 예외를 내포한다.

감이 안오는가? 아래와 같이 입력해보자.



우리는 int형으로 받기로했는데 float형으로 입력을 받았다.

이 경우 약속한것과는 다른짓을 했으므로 exception이 발생한다.

이름을 자세히보면 java.util.InputMismatchException이라고 되어있다.

이건 우리가 처리한다고(catch문) 약속하지 않은 Exception이다.

따라서 처리되지 않았으며 프로그램이 바로 종료된걸 알 수있다.

만약 이 Exception을 처리하려면 어떻게 해야할까?

당연히 위의 ArithmeticException처럼 처리해주면된다.

근데 난 둘다 처리하고싶다! 이럴 경우에는 총 3가지의 방법이 존재한다.


1.catch블록 추가

ackage com.jiharu.main;

import java.util.InputMismatchException;
import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없다.");
} catch (InputMismatchException e){
System.out.println("int형 넣어라");
}
System.out.println("프로그램의 마지막");
}
}

ifelse문을 여러개 겹치듯이 catch문을 겹치면된다.

한번 실행시켜보자.



보다시피 이제 정상적으로 예외가 처리되는걸 확인할 수 있다.


2.multi catch 블록

package com.jiharu.main;

import java.util.InputMismatchException;
import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (ArithmeticException | InputMismatchException e){
System.out.println("AritmeticException이나 InputMismatchException이 발생");
}
System.out.println("프로그램의 마지막");
}
}

catch문에 |(파이프라인 문자)를 사용해서 예외를 잡는 방식이다.

JDK1.7부터 추가된 비교적 따끈따끈한 기능인데... 사실 잘 안쓴다.

예외를 뭉뚱그려서 처리할 수 있다는 장점이 있지만 경우를 나눌수는 없다.

그런데 보통 예외를 뭉뚱그려서사용하려고 할 경우에는 아래의 방법을 주로 사용한다.


3.Exception 사용

package com.jiharu.main;

import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (Exception e){
System.out.println("Exception이 발생");
}
System.out.println("프로그램의 마지막");
}
}

Exception도 상속을 탄다. 따라서 모든 Exception클래스의 최상위 클래스인 Exception클래스를 사용한다.

그러면 그 계열의 예외를 모두 처리할 수 있게된다.

모든 예외클래스의 조상인 Exception클래스는 모든 예외를 처리할 수 있다.

만약 몇가지만 골라서 사용하고 싶다면 위의 멀티캐치블록을 사용해야 할 것이다.


이제 catch블록의 파라메터를 한번보자.

파라메터로 항상 e라는 놈을 받는다.

이녀석을 주목하자. e를 사용하면 우리는 이 예제에대해서 조금더 많은 정보를 획득 할 수 있다.

아래와 같은 예제를 만들어 보자.

package com.jiharu.main;

import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
System.out.println("프로그램의 마지막");
}
}

파라메터 e에서 자주쓰는 메소드는 보통 위의 두개가 전부다.

printStackTrace는 화면에 무슨 Exception이 떴는지 알려준다.

따라서 여러 예외를 처리할때는 대부분의 상황에서 이 기능을 사용한다.

이 때 붉은 글씨로 예외를 알리는데 위의 예제를 실행시키면 우리가 늘상 보는 Exception메세지가 뜬다.



여기서 출력순서를 예측할 수 없는데 e.printStrackTrace는 쓰레드로 실행된다.

따라서 출력순서가 뒤죽박죽이다.

만약 쓰레드가 아니라 특정 현재 main에서 실행하고 싶다면 파라메터를 넘겨준다.


e.printStackTrace(System.out);


위처럼 실행할 경우 붉은 글씨에 출력되지도 않고 쓰레드로 실행되지 않아서 순서를 명확하게 확인할 수 있다.

getMessage메소드는 문제 발생의 원인을 간략히 알려준다.

이는 Exception경우에 따라서 알려줄 수도 있고 않알려줄 수도 있다.

안 알려줄 경우 null을 반환한다.


이제 예외를 대략 처리하는 방법을 알았지만 가장 중요한 부분을 설명하지 않았다.

다시 다른 상황을 보자.

package com.jiharu.main;

import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (ArithmeticException e){
e.printStackTrace(System.out);
}
System.out.println("프로그램의 마지막");
}
}

다시 0으로 나누는 상황만 가정했다고 치자.

만약 여기서 무슨일이 있어도 "try문이 끝났습니다."라는 문자를 출력하고 싶다고 가정하자.

이건 조금 골치아픈 일이다. 어디 넣을것인가?

catch문에 넣는 방법이 있긴한데 만약 예외가 발생하지 않는다면? 실행되지 않을것이다.

그럼 반대로 밖에 빼면 어떻게 되는가? 다른 예외가 발생한다면 어짜피 프로그램이 종료되므로 실행되지 않는다.

이 경우에는 모든 상황을 충족할 해결책은 없는것 같다.

그러나 자바에서는 이러한 상황에서 제공해주는 특별한 문법이 있다. 바로 finally이다.


package com.jiharu.main;

import java.util.Scanner;

public class Main {
static Scanner sc = new Scanner(System.in);

public static void main(String[] args) {
int num1;
int num2;
try {
num1 = sc.nextInt();
num2 = sc.nextInt();
System.out.println(num1 / num2);
} catch (ArithmeticException e){
e.printStackTrace(System.out);
} finally {
System.out.println("try문이 끝났습니다.");
}
System.out.println("프로그램의 마지막");
}
}

이제 예세를 실행시켜보자. 어떠한 경우라도 try문이 끝났음을 알릴수 있다.

이러한 코드는 DB나 파일등을 다룰때 매우 중요하다.

그래서 제공해주는 문법이다. 아직 이 둘을 다루지 않으므로 간략히 설명만하고 넘어가겠다.


예외에 대해서 간략한 설명만 했는데도 설명이 매우 길어졌다.

더 설명을 하기에는 지문이 좁은것 같기에 분량을 나누게 되었다.

다음장에서 예외를 보충해서 설명하도록 하겠다.


+ Recent posts