Programming/Java

[Java9 프로그래밍] 13. 예외 처리

빠모스 2020. 3. 21. 18:28
반응형

예외 처리를 해주는 상황 : 메소드가 해야 할 일을 수행할 수 없는 상황에 부딪힐 때

 

 

  • 예외와 에러는 다르다. 에러는 복구가 불가능하고, 예외는 복구가 가능한 에러다.
  • 예외라는것은 메소드에서 발생하는데, 이 때 에러코드 (001, 002 등)을 반환한다.
  • 자바에서 쓰이는 예외처리 메커니즘은 C++, 파이썬 등의 언어에서도 유사하게 쓰인다.

 

예외 클래스 계층 구조 

 

  • 우리가 처리하는 대상은 Exception이다. 
  • Exception의 종류
  1. RuntimeException (예외처리가 옵션이다)
  2. checked exception (반드시 예외처리가 필요)

 

예외 처리 방식

 

메소드 내부에서 예외가 발생할 때 처리할 수 있는 두 가지 방식

 

  • 예외를 직접 처리 (try ~ catch 사용)

- try를 사용하여 예외를 직접 잡는다.

- catch에서 예외를 종류를 명시하여 여러 가지 예외를 잡을 수 있다.

=> 이 예외는 사용자가 직접 만든 예외와 시스템적 예외가 있다. 

 

  • 예외를 직접 처리하지 않고 호출한 쪽으로 넘기는 방식

- 메소드 헤더에서 예외를 잡지 않고 던진다.

- throws 구문 사용

 

 

예외 던지기

 

예외를 직접 처리하지 않으려면 메소드 선언부에 throws 절을 사용

 

public void write(Object obj, String filename)

throws IOException, ReflectiveOperationException {…}

 

… 내의 메소드 내부에서 try, catch를 사용해 예외를 잡을수도 있지만 메소드 헤드에서 throws로 예외 종류를 명시해놓으면 저 두 가지의 예외를 내가 처리하지 않고 write 메소드에서 예외를 잡아달라고 던지는 것.

만약 write 메소드에서도 그 위 메소드로 넘긴다면 main 메소드까지 넘어오게 됨.

main 메소드에서도 처리를 못한다면 자바 가상머신에게까지 예외가 날라오게 된다.

그래서 최소한 메소드 엔트리 포인트인 main에서는 처리해줘야 한다.

 

예외가 발생하는 시점에서 throws로 예외를 날리게 되고 예외를 나중에 처리하는 것이 좋은 방법이라고 얘기한다. 하지만 상황에 따라 달라진다.

 

 

예외 잡기

 

예외를 잡으려면 try 블럭을 이용한다.

 

try {

        문장

} catch (예외클래스1 ex) {

        핸들러 1

} catch (예외클래스2 ex) {

        핸들러 2

} catch (예외클래스3 ex) {

        핸들러 3

} finally {

        예외 발생 유무에 상관없이 마지막에 반드시 실행

}

 

  • 예외클래스 1이 발생하면 핸들러 1이 수행, 예외클래스 2가 발생하면 핸들러 2가 수행되는 원리이다.
  • finally 구문은 예외가 발생하는 try 문장 내부에서 자원을 사용하는 경우가 있다. 자원이란 외부 자원인 file이나 데이터베이스 등이다. 이 자원을 사용하기 위해 자원을 오픈하고 모두 사용한 후 close라고, 닫아줘야 한다. 근데 자원을 오픈해놓고 예외가 발생하면 이 자원을 미처 닫아주지 못한다. 그러면 이 자원을 다른 프로세스에서 사용을 못하게 된다. 예외때문에 자원을 닫아주지 못하는걸 막아주기 위해 finally에서 자원을 close 해준다.
  • catch는 여러개, finally는 단 하나만 가능하다. catch는 문법적으로 생략 가능하기 때문에 try와 finally만으로 구성된 코드도 가능하다.  
  • try 블럭의 문장이 실행되다가 지정한 클래스의 예외가 발생하면 제어가 핸들러로 이동
  • catch 문은 여러 개, finally 문은 하나만 정의 가능, 둘 중 어느 한쪽만 정의되어 있으면 됨
  • finally 구문은 일반적으로 자원을 해제하기 위해 사용된다.

 

여러 개의 예외를 같이 잡기

  • 자바7 이후에서 |(필터)을 사용
try {
… 문장
} catch (ParseException | IOException e) {
	System.out.println(“예외발생: “ + e.getMessage());
}

=> |를 이용해 여러개의 예외 클래스를 한꺼번에 기술할 수 있다.

But, 상속관계에 있는 예외는 같이 잡을 수 없다.

 

 

try-with-resources 문

  • 예외처리의 문제점은 리소스 관리
  • 예외처리에서 가장 많은 문제가 발생하는게 리소스 관리다. 원래는 finally를 쓰는데 코드가 복잡해지기때문에 try구문 내에서 리소스를 선언하게 되면 반드시 닫아준다. finally를 묵시적으로 사용한다 보면 된다.
  • 자바7에서 리소스를 확실하게 닫아준다.
try (FileInputStream in = new FileInputStream(“text.txt”)) {
	… FileInputStream 으로부터 읽기 처리 …
}

 

자바9에서 좀 더 나은 문법을 제공한다.

 

FileInputStream in = new FileInputStream(“text.txt”);

try (in) {
	… FileInputStream 으로부터 읽기 처리 …
}

=> 자원을 먼저 정의하고 먼저 오픈을 시켜놓고 변수 in만 객체화해서 쓴다.

 

 

예외 다시 던지기, Exception re-throwing

예외를 다시 던지는 경우

  • 예외 발생시 당장 처리 방법을 모르는 경우
  • 시스템 예외를 잡아서 사용자 정의 예외로 다시 던질 때
  • Checked Exception을 잡아서 Unchecked Exception으로 다시 던질 때
try {
	… 데이터 베이스에 접근 …
} catch (SQLException se) {
	throw new MyException(se);
}

 

=> 예외는 당장 처리하는것도 좋지만 예외를 모아서 한꺼번에 처리하는 방식을 실무에서 주로 사용한다. 그러면서 예외를 다시 던지는 기법을 많이 사용한다.

 

=> 데이터베이스 접근과 관련된 에러는 대부분 SQLException이다. 이 예외를 MyException이라는 사용자 정의 예외로 SQLException 객체인 se를 다시 넘여주는 것이다.

이렇게 되면 뒷단에서 발생하는 예외가 앞단에서 처리하게 된다.

 

=> Checked Exception이 난다고 명시된 메소드는 반드시 try catch 구문으로 잡아줘야 함. 하지만 매번 잡아주기가 번거로울 때 최초로 발생되는 Checked를 Unchecked Exception으로 다시 만들어준다. 일반적으로 사용자정의 예외는 Unchecked Exception, 즉 runtime exception으로 상속받아서 사용하는 경우가 많은데, unchecked exception으로 rethrowing 해주면 try catch로 안잡아줘도 되기 때문에 코드가 좀 더 심플해진다. 그리고 앞단에서 MyException 즉 비즈니스 Exception으로 처리해준다.

 

 

ThinkPoint

예외는 명시적으로 try~catch로 처리해줘야하는 checked exception과 명시저으로 처리가 불필요한 unchecked exception이 있다. 둘 중에 어떤것을 사용하는 것이 더 좋을까?

Checked 예외와 Unchecked 예외 중 무엇을 사용해야 하냐는 것은 오랜 논란거리이다. 예외가 발생하는 즉시 처리해야 된다는 주의와 예외를 최정적으로 한곳에 모아서 처리해야 된다는 주장이 있는데, C# 같은 경우는 모든 예외가 Unchecked 예외로 되어있다. 즉, try~catch를 통해 처리하지 않으면 에러가 발생하지 않는다는 뜻이다.

예를 들어 스트링 프레임워크에서는 예외를 컨트롤러 단에서 글로벌하게 처리할 수 있도록 @ControllerAdvice 어노테이션을 제공하고 있고, 서비스 단과 DAO단에서 Uchecked 예외로 바꿔서 컨트롤러 단까지 예외를 보내주는 방식으로 처리하는 것이 전반적으로 코드를 단순하게 만들 수 있다.

 

 

실습 코드

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;

public class LylicReader {
    public void doJob() {
        Path path = Paths.get("yesterday.txt");
        // 파일을 연다.

        Scanner in = null;
        // 의도적으로 파일 명을 다르게 하고 예외를 잡아본다.
        try {
            in = new Scanner(path, "UTF-8");
            // 파일을 읽어들인다. 이 때, Scanner에 IOException이 뜨게 된다. 이 때, 메소드 헤드에서 던져주면 에러가 사라진다.
            in.useDelimiter("\n");
            while (in.hasNext()){
                System.out.println(in.next());
            }
            //in.close();  scanner처럼 자원을 사용하는 코드는 파일을 꼭 닫아줘야 한다. 아니면 finally를 적어준다.
        } catch (IOException ie) {
            System.out.println("error occurred");
        } finally {
            if (in != null) in.close(); // finally로 닫아줘도 되지만, 코드가 좀 복잡하다.
        }
    // => 이렇게 되면 에러가 일어났다는 문장을 출력하고 정상적으로 프로그램을 종료시킨다.

    }
}
package com.acompany.exceptions;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
//// 자바7에서 추가된 좀 더 편하게 리소스 닫는법 ////
public class LylicReader2 {
    public void doJob() {
        Path path = Paths.get("yesterday.txt");
        // 파일을 연다.

        // try 구문 헤드에 리소스를 읽어들이게 되면 예외가 일어나든 말든 반드시 파일을 닫아주겠다는 의미. 코드가 심플해지고 가독성이 좋아짐.
        try (Scanner in = new Scanner(path, "UTF-8");
            // 파일을 읽어들인다. 이 때, Scanner에 IOException이 뜨게 된다. 이 때, 메소드 헤드에서 던져주면 에러가 사라진다.
            in.useDelimiter("\n");
            while (in.hasNext()){
                System.out.println(in.next());
            }
            //in.close();  scanner처럼 자원을 사용하는 코드는 파일을 꼭 닫아줘야 한다. 아니면 finally를 적어준다.
        } catch (IOException ie) {
            // 사용자 정의 에러, 비즈니스예외 만들기
            throw new BizException("file doesn't exist", ie); // 예외의 내용과 예외 객체를 파라미터로 넘겨서 다시 던져버린다.
            // BizException 클래스에서 에러를 런타임에러로 만들었기 때문에 try구문 헤드에서 던져주지 않아도 된다.
        }

    }
}
package com.acompany.exceptions;

import java.io.IOException;

public class ExceptionTest {
    public static void main(String[] args) {
        LylicReader reader = new LylicReader();
        try {
            reader.doJob();
        } catch (BizException e){
            System.out.println(e);
        } // 사용자 정의함수로 리쓰로잉 하게되면 마지막 말단에서 BizException만 예외로 잡아주면 됨.
        // LylicReader 클래스의 doJob에서 예외를 던져버렸기 때문에 doJob을 호출한 ExceptionTest 클래스에서 예외가 다시 발생하게 됨.
        // 여기서 또 예외를 던져버리게된다면 main함수에 예외가 발생한다.
    }
}
package com.acompany.exceptions;
//// 사용자 정의 에러 만들기, 예외 rethrowing ////
public class BizException extends RuntimeException {
    // RuntimeException을 상속받고 여러 생성자를 만든다.
    public BizException() {
        super();
    }

    public BizException(String msg) {
        super(msg);
    }

    public BizException(Exception e) {
        super(e);
    }

    public BizException(String msg, Exception e){
        super(msg, e);
    }
}
반응형