예외처리

윤재열·2022년 1월 10일
0

Java

목록 보기
16/71
post-custom-banner

예외처리(exception handling)

프로그램 에러

  • 컴파일 에러(compile-time error)와 런타임 에러(runtime error)
    -컴파일 에러 - 컴파일할 때 발생하는 에러
    -런타임 에러 - 실행할 때 발생하는 에러
    -논리적 에러 - 의도와 다르게 동작(실행시)
  • Java의 런타임 에러 - 에러(error)와 예외(exception)
    -에러(error) - 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
    -예외(exception) - 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

예외는 언제 발생하는가?

오류를 처리하는 방버을 알기 전에 어떤 상황에서 오류가 발생하는지를 알아야한다.
오타로 인해 발생하는 구문 오류 말고 실제 프로그램에서 잘 발생하는 오류들에 대해서 알아보자.

//다음처럼 존재하지 않는 파일을 열려고 시도해보자.
BufferedReader br = new BufferedReader(new FileReader("나없는파일"));
br.readLine();
br.close();

위 코드를 실행하면 다음과 같은 오류가 발생한다.

Exception in thread "main" java.io.FileNotFoundException: 나없는파일 (지정된 파일을 찾을 수 없습니다)
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init>(Unknown Source)
    at java.io.FileInputStream.<init>(Unknown Source)
    at java.io.FileReader.<init>(Unknown Source)
    ...

존재하지 않는 파일을 열려고 시도하면 FileNotFoundException라는 이름의 예외가 발생한다.

ArithmeticException예외

//이번에는 0으로 다른 숫자를 나누는 경우를 살펴보면,
int a = 4 / 0;
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:14)

4를 0으로 나누면 ArithmeticException 예외가 발생한다.

ArrayIndexOutOfBoundsException 오류

int[] a = {1, 2, 3};
System.out.println(a[3]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
    at Test.main(Test.java:17)

a[3]은 a배열의 4번째 값이므로 a배열에서 구할 수 없는 값이다.그래서 ArrayIndexOutOfBoundsException 오류가 발생했다.
자바는 이와같은 예외가 발생하면 프로그램을 중단하고 오류메세지를 보여준다.

예외처리의 정의와 목적

에러는 어쩔 수 없지만, 예외는 처리해야한다.

  • 정의 : 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
  • 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

예외 처리구문 -try-catch

예외를 처리하려면 try-catch문을 사용해야 한다.

try 문안의 수행할 문장들에서 예외가 발생하지 않는다면 catch문 다음의 문장들은 수행이 되지 않는다. 하지만 try 문안의 문장을 수행하는 도중에 예외가 발생하면 예외에 해당되는 catch문이 수행된다.

  • 숫자를 0으로 나누었을 때 발생하는 예외를 처리하려면 다음과 같이 할 수 있다.
int c;
try {
    c = 4 / 0;
} catch(ArithmeticException e) {
    c = -1;  // 예외가 발생하여 이 문장이 수행된다.
}

ArithmeticException이 발생하면 c에 -1을 대입하도록 예외를 처리한 것이다.

try블럭 내에서 예외가 발생한 경우
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인.
2. 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다.만약 일치하는 catch블럭을 찾지 못하면 ,예외는 처리되지 못한다.

try블럭 내에서 예외가 발생하지 않은 경우,
catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.

public class ExceptionEx {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);

        try{
            System.out.println(3);
            System.out.println(4);
        }catch (Exception e){
            System.out.println(5);
        }   //try-catch끝
        System.out.println(6);
    }   //main메서드의 끝
}
//1,2,3,4,6 출력.
public class ExceptionEx5 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        }catch (ArithmeticException ae){
            System.out.println(5);
        }   //try-catch 끝
        System.out.println(6);
    }   //main메서드의 끝
}
//1,2,3,5,6 출력

예외 발생시키기

  1. 먼저,연산자 new를 이용하여 발생시키려는 예외 클래스의 객체를 만든다.
Exception e = new Exceoption("고의로 발생시킴")
  1. 키워드 throw를 이용하여 예외를 발생시킨다.
thorw e;
public class ExceptionEx {
    public static void main(String[] args) {
        try{
            Exception e = new Exception("고의로 발생시킴");
            throw e;    //예외를 발생시킴
            //throw new Excoption("고의로 발생시킴"); 위의 두줄을 한줄로 줄여쓸수 있다.
        } catch (Exception e){
            System.out.println("에러 메세지 : " +e.getMessage());
            e.printStackTrace();
        }
        System.out.println("프로그램이 정상종료되었습니다.");
    }
}

RuntimeException 클래스들 : 프로그래머의 실수로 발생하는 예외 - 예외처리 선택
Exception 클래스들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외 - 예외처리 필수

예외의 발생과 catch블럭

  1. try블럭에서 예외가 발생하면, 발생한 예외를 처리할 catch블럭을 찾는다.
  2. 첫번째 catch블럭부터 순서대로 찾아 내려가며, 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.
  3. 예외의 최고 조상인 Exception을 처리하는 catch블럭은 모든 종류의 예외를 처리할 수 있다.(반드시 마지막 catch블럭이어야 한다.)
public class ExceptionEx11 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(0/0);    //0으로 나눠서 ArithmeticException을 발생
            System.out.println(4);  //실행되지 않는다.
        }catch (ArithmeticException ae){
            System.out.println("true");
            System.out.println("ArithmeticException");
        }catch (Exception e){   //ArithmeticException을 제외한 모든 예외가 처리된다.
            System.out.println("Exception");
        }   //try-catch의 끝
        System.out.println(6);
    }   //main메서드의 끝
}

finally

예외의 발생여부와 상관없이 실행 되어야 하는 코드를 넣는다.
선택적으로 사용할 수 있으며,try-catch-finally의 순서로 구성된다.
예외 미발생시,try-finally의 순서로 실행된다.
try 또는 catch블럭에서 return문을 만나도 fianlly블럭은 수행된다.

프로그램 수행 도중 예외가 발생하면 프로그램이 중지되거나 예외 처리에 의해 catch 구문이 실핸된다.하지만 어떤 예외가 발생하하더라도 반드시 실행되어야 하는 부분은 어떻게 해야하는가.??

public class Sample {
    public void shoudBeRun(){
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try{
            c =4/0;
            sample.shoudBeRun();    //이코드는 실행되지 않는다.
        }catch(ArithmeticException e ){
            c=-1;
        }
    }
}
  • 위의 예를 보면 sample.shuldBeRun()메서드는 절대로 실행이 될수가 없다.
    왜냐하면 4/0;에 의해 ArithmeticException이 발생하여 catch구문으로 넘어가기 때문이다.

  • shouleBeRun() 메서드는 반드시 실행되어야 하는 메서드라고 가정해 보면.이런 경우를 처리하기 위해 자바는 finally 구문을 제공한다.

public class Sample {
    public void shoudBeRun(){
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try{
            c =4/0;
            sample.shoudBeRun();    //이코드는 실행되지 않는다.
        }catch(ArithmeticException e ){
            c=-1;
        }finally{
            sample.shoudBeRun();    //예외에 상관없이 무조건 수행한다.
        }
    }
}
  • finally 구문은 try 문장 수행 중 예외발생 여부에 상관없이 무조건 실행된다. 따라서 위 코드를 실행하면 sample.shouldBeRun() 메소드가 수행되어 "ok, thanks" 문장이 출력될 것이다.

RuntimeException과 Exception

public class Sample2 {
    public void sayNick(String nick) {
        if("fool".equals(nick)) {
            return;
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample2 test = new Sample2();
        test.sayNick("fool");
        test.sayNick("genious");
    }
}//sayNick 메서드는 fool이라는 문자열이 입력되면 reuturn으로 메서드를 종료하여 별명이 출력되지 못하도록 하고있다.

RuntimeException

-이제 "fool"문자열이 입력되면 단순히 return으로 종료하지 말고 적극적으로 예외를 발생해보면
다음과 같은 FoolException 클래스를 Sample2.java 파일에 작성해보자.

class FoolException extends RuntimeException {
}
  • 그리고 다음과 같이 예제를 변경해보면
class FoolException extends RuntimeException{}

public class Sample2 {
    public void sayNick(String nick) {
        if("fool".equals(nick)) {
            throw new FoolException();
            //return 했던 부분을 throw new FoolException()으로 변경
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample2 test = new Sample2();
        test.sayNick("fool");
        test.sayNick("genious");
    }

이제 위 프로그램을 실행하면 "fool"이라는 입력값으로 sayNick메서드 실행시 다음과 같은 예외가 발생한다.

Exception in thread "main" FoolException
    at Sample.sayNick(Sample.java:7)
    at Sample.main(Sample.java:14)

- 여기서 FoolException이 상속받은 클래스는 RuntimeException이다.Exception은 크게 두가지로 구분한다.
1. RuntimeException
2. Exception

RuntimeException은 실행시 발생하는 예외이고, Exception은 컴파일시 발생하는 예외이다.
즉, Exception은 프로그램 작성시 이미 예측가능한 예외를 작성할 때 사용하고
RuntimeException은 발생 할 수도,발생하지 않을 수도 있는 경우에 작성한다.

그래서 Exception을 Checked Exception, RuntimeException을 Unchecked Exception이라고도 한다.

Exception

-이번에는 FoolException을 다음과 같이 변경해보면

class FoolException extends Exception {
}
  • RuntimeException을 상속하던 것을 Exception을 상속하도록 변경했다. 이렇게 하면 Sample 클래스에서 컴파일 오류가 발생할 것이다. 예측 가능한 Checked Exception이기 때문에 예외처리를 컴파일러가 강제하기 때문이다.

다음과 같이 변경해야 정상적으로 컴파일이 될 것이다.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) {
        try {
            if("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}

예외 던지기 (throws)

위 예제를 보면 sayNick 메서드에서 FoolException을 발생시키고 예외처리도 sayNick 메서드에서 했는데 이렇게 하지 않고 sayNick을 호출 한 곳에서 FoolException을 처리하도록 예외를 위로 던질 수 있는 방법이 있다.

public class Sample {
    public void sayNick(String nick) throws FoolException {
        //~~사이의 부분을 지워보자.
        ~~try {~~
            if("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
       ~~ }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }~~
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}

~~부분을 지워보면,

public class Sample {
    public void sayNick(String nick) throws FoolException {
           if("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
   
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}
  • sayNick 메서드 뒷부분에 throws 라는 구문을 이용하여 FoolException을 위로 보낼수 있다.
    ("예외를 뒤로 미루기"라고도 한다.)
  • 위와 같이 sayNick 메서드를 변경하면 main메서드에서 컴파일 에러가 발생할 것이다.
    thorws 구문 때문에 FoolExceptrion의 예외를 처리해야 하는 대상이
    sayNick 메서드에서 main메서드(sayNick 메서드를 호출하는 메서드)로 변경되었기 때문이다.
    따라서, 컴파일 오류를 해결하려면 다음과 같이 main메서드를 변경해야 한다.
class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) throws FoolException {
        if("fool".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("fool");
            sample.sayNick("genious");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}
  • main 메소드에서 try... catch로 sayNick 메소드에 대한 FoolException 예외를 처리하였다.

  • 자, 이제 한가지 고민이 남아있다. FoolException 처리를 sayNick 메소드에서 하는것이 좋을까? 아니면 main 메소드에서 하는것이 좋을까? sayNick 메소드에서 처리하는 것과 main 메소드에서 처리하는 것에는 아주 큰 차이가 있다.

sayNick 메소드에서 예외를 처리하는 경우에는 다음의 두 문장이 모두 수행이된다.

sample.sayNick("fool");
sample.sayNick("genious");
  • 물론 sample.sayNick("fool"); 문장 수행 시에는 FoolException이 발생하겠지만 그 다음 문장인 sample.sayNick("genious"); 역시 수행이 된다.

  • 하지만 main 메소드에서 예외 처리를 한 경우에는 두번 째 문장인 sample.sayNick("genious");가 수행되지 않을 것이다. 이미 첫번 째 문장에서 예외가 발생하여 catch 문으로 빠져버리기 때문이다.

try {
    sample.sayNick("fool");
    sample.sayNick("genious");  // 이 문장은 수행되지 않는다.
}catch(FoolException e) {
    System.err.println("FoolException이 발생했습니다.");
}

이러한 이유로 프로그래밍시 Exception을 처리하는 위치는 대단히 중요하다. 프로그램의 수행여부를 결정하기도 하고 트랜잭션 처리와도 밀접한 관계가 있기 때문이다.

트랜잭션(Transaction)

갑자기 "트랜잭션"이라는 것이 나와서 뜬금 없다고 생각할 수있지만,트랜잭션과 예외처리는 매우 밀접한 관련이 있다. 트랜잭션과 예외 처리가 서로 어떤 관련이 있는지 알아보도록 하자.

  • 트랜잭션은 하나의 작업 단위를 뜻한다.
  • 예를들어 "상품발송"이라는 트랜잭션을 가정해보면."상품발송"이라는 트랜잭션에는
    -포장,영수증발행,발송 같은 작업들이 있을 수있다.
  • 쇼핑몰의 운영자는 이 3가지 일들 중 하나라도 실패하면 3가지 모두취소하고 "상품발송"전의 상태로 되돌리고 싶을것이다.
    *모두 취소하지 않으면 데이터의 정합성이 크게 흔들리게 된다. 이렇게 모두 취소하는 행위를 전문용어로 롤백(Rollback)이라고 한다.

수도코드란?
-수도코드(슈도코드, pseudocode)는 특정 프로그래밍 언어의 문법을 따라 씌여진 것이 아니라, 일반적인 언어로 코드를 흉내내어 알고리즘을 써놓은 코드를 말한다. 수도코드는 말그대로 흉내만 내는 코드이기 때문에, 실제적인 프로그래밍 언어로 작성된 코드처럼 컴퓨터에서 실행할 수 없으며, 특정 언어로 프로그램을 작성하기 전에 알고리즘의 모델을 대략적으로 모델링하는 데에 쓰인다.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장() {
   ...
}

영수증발행() {
   ...
}

발송() {
   ...
}
  • 쇼핑몰 운영자는 포장, 영수증발행, 발송이라는 세가지 중 1가지라도 실패하면 모두 취소하고 싶어한다. 이런경우 어떻게 예외처리를 하는 것이 좋을까?

  • 다음과 같이 포장, 영수증발행, 발송 메서드에서는 예외를 throw하고 상품발송 메서드에서 throw된 예외를 처리하여 모두 취소하는 것이 완벽한 트랜잭션 처리 방법이다.

상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    }catch(예외) {
        모두취소();  // 하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}
  • 위와 같이 코드를 작성하면 포장, 영수증발행, 발송이라는 세개의 단위작업 중 하나라도 실패할 경우 "예외"가 발생되어 상품발송이 모두 취소 될 것이다.

만약 위 처럼 "상품발송" 메서드가 아닌 포장, 영수증발행, 발송메소드에 각각 예외처리가 되어 있다고 가정 해 보자.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    }catch(예외) {
       포장취소();
    }
}

영수증발행() {
    try {
       ...
    }catch(예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    }catch(예외) {
       발송취소();
    }
}
  • 이렇게 각각의 메소드에 예외가 처리되어 있다면 포장은 되었는데 발송은 안되고 포장도 안되었는데 발송이 되고 이런 뒤죽 박죽의 상황이 연출될 것이다. 실제 프로젝트에서도 두번째 경우처럼 트랜잭션관리를 잘못하여 고생하는 경우를 많이 보았는데 이것은 일종의 재앙에 가깝다.

이번 챕터에서는 자바의 예외처리에 대해서 알아보았다. 사실 예외처리는 자바에서 좀 난이도가 있는 부분에 속한다. 보통 프로그래머의 실력을 평가할때 이 예외처리를 어떻게 하고 있는지를 보면 그 사람의 실력을 어느정도 가늠해 볼 수 있다고들 말한다. 예외처리는 부분만 알아서는 안되고 전체를 관통하여 모두 알아야만 정확히 할 수 있기 때문이다.

profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글