티스토리 뷰

Java

Day2_ExceptionHandling 2부

DEV_GOLF 2021. 11. 20. 22:56
반응형

finally 블록

finally 블록은 보통 예외의 발생 여부와 상관없이 꼭 실행되어야 할 코드를 이 블록에 넣어 마지막에 출력하도록 처리한다.

try {
	
    // 예외 처리가 발생할 가능성이 있는 로직 작성
 } catch (Exception e) {
 	
    // 예외 발생시 실행되는 로직 보통 에러 메시지를 출력
 } finally {
 	
    // 예외 발생 여부와는 상관없이 수행되는 로직
    // finally 블럭은 try-catch문 마지막에 위치해야 함
}

예외 발생 시 try -> catch -> finally 순, 예외가 발생하지 않았을 땐 try -> finally 순으로 실행된다.

예제를 살펴보자

package exception;

public class FinallyEx1 {

    public static void main(String[] args) {

        int[] arr = {1, 5, 6, 7, 8};
        int max = 0; // 최댓 값

        try {
            for (int i : arr) {
                max = Math.max(max, i);
            }
            System.out.println(arr[8]); // ArrayIndexOutOfBoundException 발생
        } catch (ArrayIndexOutOfBoundsException arrayException) {
            System.out.println("범위를 벗어났습니다.");
        } finally {
            System.out.println(max);
        }
    }
}

다음은 finally 블럭을 활용한 예제이다. try 블럭에서 Index를 벗어난 예외가 발생하고 그에 따른 오류가 발생해야 한다. 그리고 finally에서 try 블럭이 실행되었다면 8이 출력이 되어야 한다. 그래서 출력을 해보았다.

보다 싶이 8이 출력 된 것을 확인할 수 있다. 이로서 우리는 finally 블럭의 흐름을 살펴 보았다.

 

자동 자원 반환 - try-with-resource문

try - with - resource문은 JDK 1.7에 나온  try-catch의 변형된 형태이다. 입출력과 관련된 클래스를 사용할 떄 유용한 구문으로 사용한 후에 꼭 닫아줘야하는 것들이 있다. 그래야 사용했던 자원이 반환 되기 때문이다.

 

대표 적으로 BufferedReader 클래스와 BufferedWriter가 존재한다. 

 

그렇다면 Resource를 썼을 때 안 썼을 때 코드를 비교하며 살펴보자

 

1. Recource문이 빠진 코드

package exception;

import java.io.*;

public class NoResourceEx {

    public static void main(String[] args) {
        BufferedReader br = null;
        BufferedWriter bw = null;
        
        int n = -1;
        
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            bw = new BufferedWriter(new OutputStreamWriter(System.out));
            
            n = Integer.parseInt(br.readLine());
           
            bw.write(n + "");
            bw.flush();
            bw.close();
            br.close();
        } catch (IOException e) {
            System.out.println("잘못된 입력 형식입니다.");
        } finally {
            try {
                if (n != -1) {
                    bw.close();
                    br.close();
                }
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

 

2. Resource문이 사용된 코드

package exception;

import java.io.*;

public class ResourceEx {

    public static void main(String[] args) {

        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
             BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))) {

            int n = Integer.parseInt(br.readLine());

            bw.write(n + "");
            bw.flush();
        } catch (IOException e) {
            System.out.println("잘못된 형식의 입력입니다.");
        }
    }
}

 

눈에 확 띌 정도로 코드가 Resource문을 사용했을 때 깔끔해 진 것을 알 수 있다. 보다 싶이 Resource문은 객체를 생성하는 문장을 넣으면 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출되어 catch나 finally 블럭이 실행된다. 

 

단, 주의하여야 할 것은 AutoCloseable이라는 인터페이스를 구현한 클래스만이 이러한 동작이 가능하다.

 

BufferedReader클래스의 상속 구조를 살펴보면

AutoCloseable인터페이스를 구현한 Closeable을 구현한 Reader 클래스의 subClass라는 것을 알 수  있다. 그렇기에 Resource문에 적용할 수 있었다.

 

AutoCloseable에 close() 메소드에도 Exception을 발생시킨다. 그렇다면 close()에서 예외를 발생시키면 어떻게 될까?

package exception.closeableTest;

public class CloseableResource implements AutoCloseable {

    public void exceptionWork(boolean exception) throws WorkException {
        System.out.println("CloseableResource.exceptionWork");

        if (exception) {
            throw new WorkException("WorkException 발생!");
        }
    }

    @Override
    public void close() throws Exception {
        System.out.println("close()가 호출 됨");
        throw new CloseException("CloseException 발생!");
    }
}

class CloseException extends Exception {
    public CloseException(String message) {
        super(message);
    }
}

class WorkException extends Exception {
    public WorkException(String message) {
        super(message);
    }
}
package exception.closeableTest;

public class CloseableEx1 {

    public static void main(String[] args) throws Exception {

        try (CloseableResource cr = new CloseableResource()) {
            cr.exceptionWork(false); // 예외가 발생하지 않는다.
        } catch (WorkException | CloseException exception) {
            exception.printStackTrace();
        }

        System.out.println();

        try (CloseableResource cr = new CloseableResource()) {
            cr.exceptionWork(true); // 예외가 발생한다.
        } catch (WorkException | CloseException exception) {
            exception.printStackTrace();
        }
    }
}

예제의 CloseableEx1에 main메소드는 첫 번째 try - catch 블럭에서는 close()에서만 예외를 발생시키고, 두 번째는 exceptionWork와 close()에서 예외를 발생시켰다. 

 

주목해야할 점은 Suppressed(억제된)라는 메시지이다. 두 예외가 동시에 발생할 수는 없기에 실제 발생한 예외를 WorkException으로하고, CloseException은 억제된 예외로 다뤄 실제로 발생한 예외인 WorkException에 저장된다.

 

Throwable에는 다음과 같은 억제된 예외와 관련된 메소드가 존재한다.

void addSuppressed(Throwable exception) // 억제된 예외를 추가
Throwable[] getSuppressed() 억제된 예외(배열)를 반환

 

사용자 정의 예외 만들기

기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception 클래스나 RuntimeException클래스로부터 상속받아 클래스를 만들지만, 필요에 따라 알맞은 예외 클래스를 선택할 수 있다.

package exception;

public class MyException extends Exception {
    
    public MyException(String message) { // 문자열을 매개변수로 받는 생성자
        super(message); // 상위 클래스인 Exception클래스의 생성자를 호출
    }
}

Exception 클래스로부터 상속받아 MyException 클래스를 만들었다. Exception 클래스는 생성 시 String으로 값을 받아 메시지로 저장할 수 있다. 예제를 살펴보자

package exception;

public class MyException extends Exception {
    
    private final int ERR_CODE; // 생성자를 통해 초기화 한다.

    public MyException(String message, int errCode) { // 문자열을 매개변수로 받는 생성자
        super(message); // 상위 클래스인 Exception클래스의 생성자를 호출
        ERR_CODE = errCode;
    }
    
    public MyException (String msg) {
        this(msg, 100); // ERR_CODE를 100(default)로 초기화한다.
    }

    public int getERR_CODE() {
        return ERR_CODE; // 이 메소드는 주로 getMessage와 함께 사용될 것이다.
    }
}

기존의 코드에서 좀 더 개선하여 메시지 뿐만 아니라 에러 코드 값도 저장할 수 있도록 멤버로 gerErr_CODE와 ERR_CODE를 멤버로 추가하여 catch 블럭에서 에러 코드와 메시지 모두를 얻을 수 있게 하였다.

 

기존의 예외 클래스는 주로 Exception을 상속 받아 "checked예외"로 작성하는 경우가 많았지만, 요즘은 예외 처리를 선택적으로 할 수 있도록 Runtime Exception을 상속 받아서 작성하는 쪽으로 바뀌는 추세이다. 

 

이유는 간단하다. checked예외로 처리 시 try-catch문을 넣어서 어지러운 코드를 만들 수 있기 때문이다.

그렇기에 요즘은 "unchecked예외"를 통해 예외 처리의 여부를 선택할 수 있기에 이러한 방식이 각광 받고 있다.

package exception;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class CheckedException {

    private static final String URL = "jdbc:mysql://127.0.0.1:3306/servletex?serverTimezone=Asia/Seoul";
    private static final String USER = "root";
    private static final String PW = "root";

    private static Connection conn;
    private static PreparedStatement pstmt;

    List<String> listMembers() {
        List<String> list = new ArrayList<>();

        try {
            getConnection();

            pstmt = conn.prepareStatement("SELECT * FROM MEMBER");
            ResultSet rs = pstmt.executeQuery();

            /*
            * 멤버 데이터 순환 로직
            */
        } catch (SQLException | ClassNotFoundException e) {
            e.getMessage();
        }
        return list;
    }

    private static void getConnection() throws ClassNotFoundException {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(URL, USER, PW);
        } catch (SQLException e) {
            e.getMessage();
        }
    }
}

SQL Exception과 ClassNotFoundException은 대표적인 Exception을 상속받는 클래스이다. 보다 싶이 반드시 예외에 대한 처리가 필요한 것을 볼 수 있다.

package exception;

import java.util.LinkedList;
import java.util.Queue;

public class UnCheckedExceptionEx {

    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        
        queue.add("java");
        queue.add("is");
        queue.add("king");
        
        if (!queue.isEmpty()) {
            queue.poll();
        }
    }
}

이 코드에서는 !queue.isEmpty()에 주목해 보자 만약 이게 없다면 자바 컴파일러에서는 빨간 줄이 아닌 노란 줄로 경고를 표시할 것이다. 왜냐하면 NullPointerException에 대한 위험성이 있기 때문이다. 그렇지만 RuntimeException을 상속받은  클래스로 처리를 해주지 않더라도 개발자가 판단했을 때 문제가 되지 않으면 넘길 수 있는 오류이다. 그렇기에 !queue.isEmpty()로 개발자가 NullPointerException을 방지하면 그만이다.

 

예외 되던지기

한 메소드에서 발생할 수 있는 예외가 여럿인 경우 try-catch문을 통해서 메소드 내에서 자체적으로 처리하고, 그 나머지 선언 부에 지정하여 호출한 메소드에서 나뉘어 처리되도록 할 수 있다.

 

그리고 심지어는 단 하나의 예외에 대해서도 예외가 발생한 메소드와 호출한 메소드, 양쪽에서 처리할 수 있다.

 

이것은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능하다. 예제를 살펴보자 

package exception;

public class ReException {

    public static void main(String[] args) {
        try {
            method();
        } catch (Exception e) {
            System.out.println("main 메서드에서 예외가 처리되었습니다.");
        }
    }

    private static void method() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method에서 예외가 처리되었습니다.");
            throw e;
        }
    }
}

다음 예제는 method()와 main 메소드 양쪽에서 catch 문이 수행 되었음을 알 수 있다. method()의 catch블럭에서 예외를 처리하고도 throw문을 통해 다시 예외를 발생시키고 이 예외를 main에서 한번 더 처리한다.

실행을 해보면 출력된 메시지를 통해 두 메소드에서 catch문이 실행된 다는 것을 쉽게 알 수 있다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함