본문 바로가기
스터디/[white-ship] 자바 스터디(study hale)

9주차 과제: 예외 처리

by doyoungKim 2021. 1. 16.

 

목표

자바의 예외 처리에 대해 학습하세요.

학습할 것 (필수)

  • 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

자바가 제공하는 예외 계층 구조

https://madplay.github.io/post/java-checked-unchecked-exceptions

예외(Exception)와 에러(Error)

비슷한 말 같지만 차이점이 있다.

에러 그림에서 보이는 것 과 같이 ThreadDeath 쓰레드가 죽어버렸다거나 VirtualMachineError JVM이 작동을 안한다거나 
개발자가 만든 코드 안에서 오작동이 난 것이 아닌 그 밖에 영역에 문제이다.
따라서 대비할 수 있는 방법이 많지 않다.

예외는 발생하더라도  개발자가 만든 코드 안에서 오작동이 난 것이고 경우의 수를 생각하여 대비 할 수 있는 방법이 다양하다.
이렇게 예외를 대비하는 것을 예외 처리라 한다.

 

예외처리

어떤 변수나 상황으로 개발자가 설계한대로 시스템이 흘러가지 않는 것이라고 생각한다.
자바에서 설계한 대로 흘러가지 않았을 경우를 대비하고 그 상황을 알려주는 Exception 이라는 객체를 사용해 예외 처리 할 수 있다.

 

Throwable 

우리말로 "던질 수 있는" 이라는 이 객체는 "어떤 상황이라면 오작동이다." 를 알려주는 것 처럼 예외의 대한 기본적인 개념은 throw 를 통해서 예외를 알려줄 수 있어야 하기 때문이다.
심지어 개발자가 건들 수 없는 스레드의 죽음 또한 어디선가 throw 되어 스레드가 죽었구나 하고 알 수 있다. 

 

Checked Exceptions VS Unchecked Exceptions

구분

Checked Exceptions

Unchecked Exceptions

처리 여부 강제로 예외처리 하도록 강요. 명시적인 처리를 강제하지 않음
시점 컴파일 시점 실행 시점

 

많은 코드가 생기면서 많은 메소드들이 생기고 서로 호출 하는 상호작용이 일어나는데, 호출하려고 하는 메소드가 어떤 위험부담을 가지고 있는지 명세하도록 강제되고 있다.  따라서 checkedException 은 꼭 예외처리 하여야 한다.

하지만 RuntimeException과 같이 Unchecked Exception 은 실행환경속에서 나타나는 변수에 의해 발생하는 오류이기 때문에 많은 상황에 많은 경우에 수로 나타나기 때문에 예외처리를 강제하지 않는다. 

 

예외처리를 하는 방법은 다음과 같다.

  1. 예외가 발생하면 다른 작업 흐름으로 유도하는 예외 복구
  2. 처리하지 않고 호출한 쪽으로 던져버리는 예외처리 회피
  3. 호출한 쪽으로 던질 때 명확한 의미를 전달하기 위해 다른 예외로 전환하여 던지는 예외 전환

 

예외복구

예외복구는 다른 작업 흐름으로 자연스럽게 유도해주는 것 

private void printStr(String str) {
    try {
        System.out.println("str length : " + str.length());
    } catch (NullPointerException e) {
        System.out.println("str is null!");
    }
}
printStr(null); 
printStr("hello"); 

// str is null! 
// 5 (정상 케이스)

 

원래라면 NullPointerException 이 발생하면서 두번째 printStr("hello") 메소드를 실행하지 않고 프로그램이 중단되지만,
try ~ catch 문으로 자연스럽게 다른 작업으로 유도할 수 있다.

try 영역에서 catch 구문에서 예상한 예외가 발생한다면 catch 구문을 이어가 프로그램이 중단되는 것 을 막을 수 있다.

 

예외처리 회피

예외처리 회피는 자신이 직접 예외처리하지 않고 호출하는 메소드로 전파시키는 것.

// throws로 회피하기
public void process() throws SQLException {
    // jdbc 로직...
}

// catch 후 로그 남기고 다시 throw
public void process2() throws SQLException {
    try {
        // jdbc 로직...
    } catch (SQLException e) {
        System.out.println(e.getMessage())
        throw e;
    }
}

 

checkedExcepton 을 던졌는데 try ~ catch 구문이 없다면 메소드 시그니처에 throws 와 함께 해당 Exception 을 명시해줘야 한다.

해당 메소드를 호출 할 때 위험부담을 알아야 하기 때문이다.

 

예외 전환

더 적절한 예외로 전환하여 던지는 것,

public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // code ..
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw new DuplicateUserIdException();
        }

        throw e;
    }
}

 

애매모호한 의미가 좀 더 명확한 예외로 전환 할 수 있다. throws 를 계속해서 하다보면 그 끝엔 JVM 이 있다.

 

Try With Resource

 

기존의 자바 1.7 이전에는 IO 관련 해서 작성하기 힘든 부분이 있었다.  어떤 파일을 복사하는 메소드를 예로 들겠다.

    static void copy(String src, String dest) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = new FileInputStream(src);
            out = new FileOutputStream(dest);
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }

 

위 코드에는 문제가 있다. 바로 close 부분에서 Exception 이 발생 할 수 있다는 것이다. 따라서 close 하는 부분에도 try ~ catch 를 해주자.

    static void copy(String src, String dest) throws IOException {
        InputStream in = null;
        OutputStream out = null;

        try {
            in = new FileInputStream(src);
            out = new FileOutputStream(dest);
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // do something
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // do something
                }
            }
        }

    }

 

코드를 위 와 같이 각각  try ~ catch 를 해주었다. 하지만 또 ! 문제가 있다. 만약에 CheckedException 이 아니라 RuntimeException 이 발생한다면 어떻게 될까? 
다음 out.close 부분으로 넘어가지 않고 튕겨 나갈 것이다. 무조건 튕겨나 가기전에 finally 구문을 다음과 같이 추가해줘야한다.

    static void copy(String src, String dest) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {

            try {
                in = new FileInputStream(src);
                out = new FileOutputStream(dest);
                byte[] buf = new byte[1024];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        // do something
                    }
                }
            }
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // do something
                }
            }
        }
    }

 

 

 

위 와 같이 작성해야 이상없는 코드가 된다. 하지만 보기 좋지는 않다. 너무 많은 try 와 catch, finally 들...

따라서 자바 1.7 부터 AutoCloseable 개념이 나왔다. 자동적으로 간편하게 close 하는 코드를 구현할 수 있다.

    static void copy(String src, String dest) throws IOException {
        try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dest)) {
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        }
    }

 

compile 된  bytecode 를 보면 다음과 같이 잘 close 된 코드를 볼 수 있다.

    static void copy(String var0, String var1) throws IOException {
        FileInputStream var2 = new FileInputStream(var0);

        try {
            FileOutputStream var3 = new FileOutputStream(var1);

            try {
                byte[] var4 = new byte[1024];

                int var5;
                while((var5 = var2.read(var4)) >= 0) {
                    var3.write(var4, 0, var5);
                }
            } catch (Throwable var8) {
                try {
                    var3.close();
                } catch (Throwable var7) {
                    var8.addSuppressed(var7);
                }

                throw var8;
            }

            var3.close();
        } catch (Throwable var9) {
            try {
                var2.close();
            } catch (Throwable var6) {
                var9.addSuppressed(var6);
            }

            throw var9;
        }

        var2.close();
    }

 

커스텀한 예외 만드는 방법

 

Best Practices

  1. Java 표준 예외를 사용하는 것 보다 작성한 Custom 예외를 사용하는게 더 많은 이익을 얻는다고 생각할 경우에만
    Custom Exception을 구현한다. 
  2. 작성한 Custom Exception 클래스의 이름의 끝은 "Exception"으로 끝나도록 한다.
  3. API 메소드가 어떤 하나의 예외를 기술하고 있다면, 그 예외는 API의 한 부분이 되는 것이며 그 예외를 문서화 해야 한다.
  4. 예외의 원인을 알 수 있는 Throwable 타입의 생성자를 제공해야 한다.

Checked 는 Exception 계열, UnChcked 는 RuntimeException 계열을 상속받아 클래스를 정의하면 된다.

/**
 * The MyBusinessException wraps all checked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyBusinessException extends Exception {
    private static final long serialVersionUID = 7718828512143293558 L;
    private final ErrorCode code;
    public MyBusinessException(ErrorCode code) {
        super();
        this.code = code;
    }
    public MyBusinessException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
    public MyBusinessException(String message, ErrorCode code) {
        super(message);
        this.code = code;
    }
    public MyBusinessException(Throwable cause, ErrorCode code) {
        super(cause);
        this.code = code;
    }
    public ErrorCode getCode() {
        return this.code;
    }
}



/**
 * The MyUncheckedBusinessException wraps all unchecked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyUncheckedBusinessException extends RuntimeException {
    private static final long serialVersionUID = -8460356990632230194 L;
    private final ErrorCode code;
    public MyUncheckedBusinessException(ErrorCode code) {
        super();
        this.code = code;
    }
    public MyUncheckedBusinessException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
    public MyUncheckedBusinessException(String message, ErrorCode code) {
        super(message);
        this.code = code;
    }
    public MyUncheckedBusinessException(Throwable cause, ErrorCode code) {
        super(cause);
        this.code = code;
    }
    public ErrorCode getCode() {
        return this.code;
    }
}


 

API 통신 사이에서는 CustomExcepton 은 어떻게 만들까?

통신 사이에서는 경우의 수 가 많고 복구작업 로직을 작성하기 힘든 경우가 많아 대다수가 RuntimException 계열이다.

 

/*
 * Copyright (C) 2014 jsonwebtoken.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jsonwebtoken;

/**
 * Base class for JWT-related runtime exceptions.
 *
 * @since 0.1
 */
public class JwtException extends RuntimeException {

    public JwtException(String message) {
        super(message);
    }

    public JwtException(String message, Throwable cause) {
        super(message, cause);
    }
}

 

실제로 백기선님 방송에서 직접물어보기 전까지 RunTimeException 만 상속받아야지 생각하고 있었다.
하지만 항상 RuntimeException 계열만 있는 것은 아니다. 예를 들어 다음과 같은 복구작업이 가능하다면 CheckedException 으로 만들 수 있다.

"재 요청 할 수 있는 부분이 있다거나 기본값 으로 셋팅 해서 진행 한다거나 혹은 다른 서버에 요청을 한다면 복구 로직을 태우는게 맞다."

 

 

사고방식의 차이

이번 스터디를 하면서 기존에 먼저 작성하신 다른 분들의 스터디 내용을 참고 했다.
그러다 보니까 이상한 Rollback 까지 다루게 되어 스터디할래 방송에서 대차게 까였다.. 다음부터는 근본적인 문서도 같이 참고하고 실제로 눈으로 확인하도록 노력해야겠다.

 

 

출처

728x90

댓글