-
9주차 과제: 예외 처리Programming/Java live study 2021. 1. 13. 14:14
목표
자바의 예외 처리에 대해 학습하세요.
학습할 것
- 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
- 자바가 제공하는 예외 계층 구조
- Exception과 Error의 차이는?
- RuntimeException과 RE가 아닌 것의 차이는?
- 커스텀한 예외 만드는 방법
1. 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
1.1 try-catch-finally
Java에서 실행 도중에 발생하는 예외 처리를 위해 try-catch-finally 문 사용이 가능하다.
try { 예외가 발생할 가능성이 있는 실행코드 catch (처리할 예외 타입 선언) { 예외 처리코드 } finally { 예외 발생 여부와 상관없이 무조건 실행되는 문장 }
try 예외가 발생할 가능성이 있는 실행문들을 try 블록에 묶는다.
catch try블록에서 발생한 예외를 처리한다. 선택적 구문이기 때문에 생략되어도 된다. try에서 호출한느 메소드가 어떤 예외를 던지는지에 따라 catch에 들어가는 예외가 달라진다.
finally 예외 발생 여부와 상관 없이 무조건 실행된다. 선택적 구문이기 때문에 생략되어도 된다.
catch와 finally 모두 선택적 구문이지만 try만 사용할 순 없다. 밑의 코드는 간단한 사용 예시이다.
public static void main(String[] args) { int[] intArray = new int[5]; try { intArray[5] = 10; // 예외 발생 } catch (ArrayIndexOutOfBoundsException e){ System.out.println("배열의 범위를 초과하였습니다."); } finally { System.out.println("finally 블록"); } }
finally 블록에서 return을 하는 것은 안티패턴이다. try에서 정상적으로 처리되어 반환을 하여도 finally에 있는 것을 반환하게 된다.
public class TryCatchFinallyTest { public static void main(String[] args) { System.out.println(test()); } static String test() { try { System.out.println("try 블록 정상 수행"); return "try 블록"; } catch (Exception e) { System.out.println("에러 발생"); } finally { return "finally 블록"; } } }
try 블록 정상 수행 finally 블록
try 블록이 출력될 것이 예상되지만 finally 블록에 있는 return을 출력한다.
1.2 catch 문을 여러번 사용할 때
public static void main(String[] args) { int[] intArray = new int[5]; try { intArray[5] = 10; // 예외 발생 } catch (ArrayIndexOutOfBoundsException e){ System.out.println("배열의 범위를 초과하였습니다."); } catch (Exception e) { System.out.println("Exception 발생"); } }
catch문을 여러번 사용할 수 있다. 고려해야 할 점은 예시코드의 Exception은 모든 예외 클래스의 상위 클래스이다. 그렇기 때문에 어떤 예외가 발생하여도 처리가 가능하다. 이렇게 큰 범주의 예외를 포함하는 클래스를 catch 뒤쪽 순서로 미뤄야 한다. 만약 ArrayIndexOutOfBoundsException 처럼 구체적으로 명시한 것 보다 먼저 나오게 되면 모든 예외를 Exception이 catch하기 때문에 컴파일 에러를 유발한다.
1.3 중첩된 try-catch
public static void main(String[] args) { int[] intArray = new int[5]; try { try { intArray[5] = 11; // 예외 발생 } catch (Exception e) { System.out.println("try 블록에 중첩된 try-catch"); } intArray[5] = 10; // 예외 발생 } catch (ArrayIndexOutOfBoundsException e1){ try { intArray[5] = 12; // 예외 발생 } catch (Exception e2) { System.out.println("catch 블록에 중첩된 try-catch"); } } finally { System.out.println("finally 블록"); } }
try-catch-finally문은 어느 블록이든 중첩해서 사용이 가능하다. 블록의 범위에 따라 변수명만 달리해서 구별해줘야 한다.
1.4 멀티 catch 블록
Java 1.7 버전 부터는 멀티 catch 블록을 사용할 수 있다.
public static void main(String[] args) { int[] intArray = new int[5]; try { intArray[5] = 10; // 예외 발생 } catch (ArrayIndexOutOfBoundsException | ArrayStoreException e){ System.out.println("배열의 범위를 초과하였습니다."); } finally { System.out.println("finally 블록"); } }
catch문을 여러번 사용할 때와 마찬가지로 상위 타입의 에러 클래스와 하위 타입의 에러 클래스를 함께 사용하면 상위 클래스의 exception이 모든 에러를 catch하기 때문에 하위 타입의 에러 클래스는 의미가 사라진다. try 블록에서 던지는 예외에 따라서 e가 결졍된다.
1.5 try-with-resources
Java 1.7 이전에는 try-catch-finally 구문에서 자원 해제를 위해서는 finally에 명시 해야 했다.
public static void main(String[] args) { BufferedReader bufferedReader = null; BufferedWriter bufferedWriter = null; try { bufferedReader = new BufferedReader(new InputStreamReader(System.in)); bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out)); String s = bufferedReader.readLine(); bufferedWriter.write(s); } catch (IOException e) { e.printStackTrace(); } finally { try { bufferedReader.close(); bufferedWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
BufferedReader와 BufferedWriter의 경우 close를 위한 try-catch도 진행해야 한다. 이렇게 부가적으로 작성해야 하는 코드가 늘어나기 때문에 실행해야 할 코드가 매우 지전분해진다.
Java 1.7 이후 부터는 Try-with-resources로 자원을 쉽게 해제할 수 있다. 위 코드를 수정하여 try-with-resources로 작성해보았다.
public static void main(String[] args) { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out))) { String s = bufferedReader.readLine(); bufferedWriter.write(s); } catch (IOException e) { e.printStackTrace(); } }
위 try-with-resources를 적용한 코드를 디컴파일 해보았다.
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; public class TryWithResources { public TryWithResources() { } public static void main(String[] args) { try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); try { BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out)); try { String s = bufferedReader.readLine(); bufferedWriter.write(s); } catch (Throwable var7) { try { bufferedWriter.close(); } catch (Throwable var6) { var7.addSuppressed(var6); } throw var7; } bufferedWriter.close(); } catch (Throwable var8) { try { bufferedReader.close(); } catch (Throwable var5) { var8.addSuppressed(var5); } throw var8; } bufferedReader.close(); } catch (IOException var9) { var9.printStackTrace(); } } }
catch 블록이 추가되어 예외 발생 시 던져주고, 정상적으로 실행되어도 close가 보장 되도록 수정되었다. 여기서 알 수 있는 점은 추가적으로 finally 블록을 추가하여도 문법적으로 오류를 범하지 않게 된다.
try-with-resources를 사용한 코드를 보면 close 메소드를 어디에도 명시하지 않았다. 코드를 짧고 간결하게 만들고 그만큼 읽기가 쉬워진다. try ( ) 안에 명시된 객체는 자동적으로 close 되는 것을 보장한다. 하지만 try ( ) 안에 객체를 생성하기 위해서는 조건이 있다. 바로 AutoCloseable을 구현한 객체만 사용이 가능하다.
BufferedReader를 확인해보았다. BuffereedReader는 추상 클래스 Reader를 상속받고 있다.
추상 클래스 Reader는 Readable과 Closeable 인터페이스를 구현하고 있다.
Closeable 인터페이스는 AutoCloseable을 상속받는다. AutoCloseable 또한 Java 1.7 부터 지원한다.
직접 만든 클래스에 try-with-resources에서 자동으로 자원이 해제되길 원하면 AutoCloseable 인터페이스를 implements 하기만 하면 된다.
1.6 throw
throw 키워드는 특정한 시점에 의도적으로 예외를 던지기 위해 사용된다.
public static void test() throws Exception { Exception e = new Exception("의도적인 예외"); throw e; }
throw를 사용한 시점에서 무조건 예외가 발생한다. 이 예외는 try-catch로 처리하거나 혹은 throws 키워드를 사용하여 메소드를 사용한 곳에 예외를 던져줘야 한다.
1.7 throws
예외가 발생할 것으로 예상되는 메소드를 try-catch를 사용하지 않고 해당 메소드를 사용한 곳으로 예외를 전달한다. 단순히 말하면 예외 처리를 미루고 사용한 곳으로 책임을 전가하는 것이다.
BufferedReader에서 사용한 readLine 메소드를 예로 들어 보았다.
throws IOException을 사용하고 있다. readLine 메소드를 실행하던 도중 IOException이 생길 것이 우려되기 때문에 만약 예외가 발생하면 catch하여 readLine을 사용하고 있는 곳으로 예외를 던져준다.
public static void main(String[] args) { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(System.out))) { String s = bufferedReader.readLine(); // 예외가 발생하면? throws IOException! bufferedWriter.write(s); } catch (IOException e) { e.printStackTrace(); } }
해당 예외를 catch하여 처리해줘야 한다.
2. 자바가 제공하는 예외 계층 구조
모든 예외클래스는 Throwable 클래스를 상속받는다. Throwable을 상속받는 클래스는 Error와 Exception이 있다. Error는 시스템 레벨의 심각한 수준의 에러이기 때문에 시스템에 변화를 주어 문제를 처리해야 한다. 반면 Exception은 개발자가 코드를 추가하여 처리가 가능하다.
Exception에 종류에는 Checked와 Uncheck가 있다.
Checked Exception Unchecked Exception 처리에 대한 여부 반드시 예외를 처리해야 한다. 명시적인 처리를 강제하지 않는다. 확인되는 시점 컴파일 단계에서 확인 가능하다. 실행 단계에서 확인 가능하다. 대표적인 예외 Exception을 상속 받는 하위 클래스 중 Runtime Exception을 제외한 모든 예외는 Checked Exception으로 분류 된다.
- IOException
- SQLExceptionRuntime Exception의 하위 클래스
- NullPointerException
- IllegalArgumentException
- SystemExceptionChecked와 Unchecked의 가장 큰 구분 기준은 꼭 처리 해야 하는지에 대한 여부이다.
2.1 Checked Exception
발생할 가능성이 있는 메소드라면 반드시 코드를 try-catch로 감싸거나 throws로 던져서 처리해야 한다. 밑은 Checked Exception의 한 종류인 IOException이다. 컴파일 에러가 발생한 것을 알 수 있다.
적절하게 예외를 던지고 try-catch하여 컴파일 가능하게 수정하였다.
2.2 Unchecked Exception
명시적인 예외처리를 하지 않아도 된다. 예외를 피할 수 있지만 개발자가 부주의해서 발생할 가능성이 높다. 예측하지 못한 상황에서 발생하는 예외가 아니기 때문에 로직으로 처리할 필요가 없도록 만들어졌다. RuntimeException과 하위 클래스가 이에 속한다.
2.3 Checked Exception과 UncheckedException으로 나눈 이유
오라클 공식 문서와 live study에 참여하고 계신 선원의 글을 참고하여 작성하였다.
https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
wisdom-and-record.tistory.com/46
간단하게 요약하면 예외도 메소드의 시그니처와 번환값 만큼 중용한 공용 인터페이스이다. 메소드를 호출하는 부분은 해당 메소드에서 어떤 예외를 던질 수 있는지 반드시 알아야 한다. 그렇기 때문에 Checked Exception으로 해당 메소드가 발생할 수 있는 예외를 명시하도록 강제한다.
하지만 Unchecked Exception, 즉 Runtime Exception은 예외를 명시하지 않아도 된다. Runtime Exception은 프로그램의 코드 문제로 발생하는 예외이다. 그렇기 때문에 클라이언트 쪽에서 복구하거나 대처할 수 있다고 예상할 수 없고, 어디서나 일어날 수 있기 때문에 메서드에 명시할 경우 프로그램의 명확성을 떨어뜨릴 수 있다.
정리하며느 클라이언트가 Exception을 적절하게 회복할 수 있으면 Checked Exception으로 만들고, 그렇지 않은 경우 Unchecked Exception으로 만드는 것이 좋다.
2.4 Unchecked Exception에 대한 오해
live study에 참여하고 계신 선원의 글을 참고하여 작성하였다.
Spring framework에서 Transaction 설정과 관련하여 Unchecked Exception에 대해 roll-back기능을 지원한다. 이것은 Spring framework의 transaction 설정이 제공하는 것이다. 순수 Java 언어에서 지원하는 기능이 아니다. Java가 제공하는 Unchecked Exception에는 roll-back 기능이 없다.
스터디 과제를 작성할 때는 신뢰성 있는 자료가 바탕이 되어야 할 것 같다. 단순히 자료를 가져다 배껴 적는 것이 아닌, 실제로 코드를 작성하며 검증해보고 공식 문서와 비교해보며 신중하게 작성해야 겠다.
3. Exception과 Error의 차이는?
3.1 예외 Exception
개발자가 구현한 코드에서 발생한다. 발생할 상황을 미리 예측하여 처리할 수 있다. 예외 처리는 개발자가 할 수 있기 때문에 예외를 구분하고 그에 맞는 처리 방법을 적용해야 한다.
Exception이 발생하면 Exception Handler가 이를 처리하게 된다. Call stack을 이용하여 역방향으로 Exception Handler를 탐색한다. 탐색하면서 Exception Handler가 처리할 수 있는 Exception 타입인지 체크하고 아니라면 상위로 전달된다.
정리하면 발생한 Exception에 대해서 처리가 가능한 Handler를 찾을 때 까지 상위로 전달되면서 찾아간다. 적절한 Handler를 찾지 못한다면 JVM까지 전달되고, 최종적으로 JVM이 Exception을 처리하게 된다.
3.2 오류 Error
시스템에 비정상적인 상황인 메모리 부족, 스택오버플로우 등이 생겼을 때 발생한다. 시스템 레벨에서 발생하기 때문에 심각한 수준의 오류를 야기한다. 개발자가 미리 예측하여 처리할 수 없기 때문에 예외 처리 방법을 적용할 수 없다.
여기서 이야기 하는 예외와 오류는 모두 실행중에 발생하였기 때문에 런타임 에러에 해당한다.
4. RuntimeException과 RE가 아닌 것의 차이는?
RuntimeException에 해당하는 예외 클래스는 Unchecked Exception이 존재한다. Unchecked Exception은 실행 시점에서 확인이 가능하고 예외 처리를 강제하지 않기 때문에 컴파일 시점에서는 확인 할 수 없다. 예외 발생 시 트랜잭션을 roll-back한다.
RuntimeException이 아닌 것에는 Checked Exception이 있다. Checked Exception에 경우 치명적인 예외 상황이 발생하기 때문에 반드시 예외를 처리해줘야 한다. 컴파일 시점에서 확인 할 수 있다. 또한 예외 발생 시 트랜잭션을 roll-back하지 않는다.
5. 커스텀한 예외 만드는 방법
5.1 커스텀 예외를 만들 때 참고해야 할 4가지 Best Practices
live study에 참여하고 계신 선원의 글을 참고하여 작성하였다.
www.notion.so/3565a9689f714638af34125cbb8abbe8
dzone.com/articles/implementing-custom-exceptions-in-java?fromrel=true
m.blog.naver.com/sthwin/221144722072
- Always Provide a Benefit
Java 표준 예외들에는 다양한 장점을 가지는 기능이 있다. JDK가 이미 제공하는 예외들과 비교했을 때 커스텀한 예외와 기능적으로 별 차이가 없다면 만드는 이유에 대하여 고민해볼 필요가 있다. 이점이 없는 예외를 만드는 것 보다 표준 예외를 사용하는 것이 권장된다.
- Follow the Naming Convention
JDK가 제공하는 예외 클래스들을 보면 클래스의 이름이 모두 Exception으로 끝나는 것을 알 수 있다. 만들고자 하는 커스텀 예외 클래스도 이러한 네이밍 규칙을 따르는 것이 좋다.
- Provide javadoc Comments for your Exception class
많은 커스텀 예외들은 javadoc 코멘트도 없이 만들어진 경우가 많다. 기본적으로 API의 모든 클래스와 멤버 변수, 생성자에 대하여 문서화 하는 것이 일반적인 Best Practices이다. 그렇기 때문에 클라이언트와 직접 관련된 메소드들 중 하나가 예외를 던지면 그 예외는 바로 예외의 일부가 된다. Javadoc와 문서화가 필요하다.
Javadoc에는 예외가 발생할 수 있는 상황과 예외의 일반적인 의미를 기술한다. 기술 목적은 다른 개발자들이 API를 이해하고 에러상황들을 피하도록 돕는 것이다.
/** * 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 { ... }
- Provide a Contructor That Sets the Cause
대부분의 코드는 커스텀 예외를 던지기 전에 표준 예외를 캐치하는 케이스가 많다. 보통 캐치된 예외에는 오류를 분석하는데 필요한 중요 정보가 포함되어 있다. 그렇기 때문에 원인을 가지고 있는 Throwable을 받을 수 있는 생성자 메소드를 제공해야 한다. 발생한 Throwable을 파라미터를 통해 가져올 수 있는 생성자를 최소한 하나를 구현하고 super클래스에 Throwable을 전달해줘야 한다.
public class MyBusinessException extends Exception { public MyBusinessException(String message, Throwable cause, ErrorCode code) { super(message, cause); this.code = code; } ... }
5.2 커스텀한 Checked Exception
Checked Exception 구현을 위해서는 Exception 클래스를 상속 받아야 한다. 4가지의 Best practices를 적용하여 작성하였다.
- 예외를 기술하는 Javadoc 추가
- 슈퍼클래스에서 발생한 예외를 주입하는 생성자 메소드 구현
- 표준 예외보다 더 나은 장점을 제공하기 위해 MyBusinessException은 문제 식별을 위한 에러코드를 저장하는 커스텀 enumeration을 사용한다.
/** * 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; } }
public void handleExceptionInOneBlock() { try { wrapException(new String("99999999")); } catch (MyBusinessException e) { // handle exception log.error(e); } } private void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException("A message that describes the error.", e, ErrorCode.INVALID_PORT_CONFIGURATION); } }
5.3 커스텀한 Unchecked Exception
구현 방식은 동일하다. 한가지 차이점은 Exception을 상속받는 것이 아닌 RuntimeException을 상속 받아야 한다.
/** * 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; } }
또한 Unchecked Exception 처리와 마찬 가지로 사용할 수 있다. 하지만 해당 예외를 처리하지 않아도 컴파일하는데에는 문제가 없다.
6. [더 알아보기] 예외처리 전략
live study에 참여하고 계신 선원의 글을 참고하여 작성하였다.
토비의 스프링 3.1 Vol.1 4장 예외
6.1 예외 복구
문제를 파악하고 해결하여 정상 상태로 돌려놓는 방법이다. 어떤 예외가 발생하였을 때 다른 작업 흐름으로 자연스럽게 유도하는 것이다.
public class ExceptionTest { static void printString(String str) { try { System.out.println(str.length()); } catch (NullPointerException e) { System.out.println("string in null"); } } public static void main(String[] args) { printString("hello"); // 5 printString(null); // string is null } }
6.2 예외처리 회피
자신이 직접 예외처리하지 않고 호출하는 메소드로 전파시키는 방법이다. throws로 예외를 던지거나 catch로 예외를 잡은 후 적당한 로그를 남기고 throw하는 방법이 있다.
void read() throws IOException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); bufferedReader.readLine(); }
void read() throws IOException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); try { bufferedReader.readLine(); } catch (IOException e) { e.printStackTrace(); throw e; } }
6.3 예외 전환
발생한 예외를 그대로 넘기지 않고 더 적절한 예외로 전환하여 던진다.
void process() { try { // code ... } catch (Exception e) { throw new RuntimeException(e); } }
이때 기존 예외를 담아서 중첩 예외로 만드는 것이 좋다.
7. [더 알아보기] Java가 제공하는 기본 예외들
live study에 참여하고 계신 선원의 글을 참고하여 작성하였다.
https://velog.io/@youngerjesus/자바-예외-처리
ArithmeticException
- 산술연산에 예외 조건이 발생 했을 때 발생한다.
ArrayIndexOutOfBoundsException
- 잘못된 인덱스로 Array에 접근했을 때 발생한다.
- 인덱스가 음수이거나 배열 크기보다 크거나 같을 때 발생한다.
ClassNotFoundException
- 정의한 클래스를 찾을 수 없을 때 발생한다.
FileNotFoundException
- 파일에 접근권한이 없거나 열리지 않는 경우 발생한다.
IOException
- 입출력 작업이 실패하거나 중간에 Connection이 중단되면 발생한다.
InterruptedException
- Thread가 waiting, sleeping 또는 어떤 처리를 하고 있을 때 interrupt가 되면 발생한다.
NoSuchMethodException
- 찾을 수 없는 메소드에 접근할 때 발생한다.
NullPointerException
- null 객체의 멤버를 참조할 때 발생한다.
StringIndexOfBoundsException
- 문자열에 접근하는 인덱스가 문자열보다 큰 경우 발생한다.
- 혹은 음수일 때 발생한다.
References.
https://wisdom-and-record.tistory.com/46
www.notion.so/3565a9689f714638af34125cbb8abbe8
dzone.com/articles/implementing-custom-exceptions-in-java?fromrel=true
'Programming > Java live study' 카테고리의 다른 글
11주차 과제: Enum (0) 2021.01.27 10주차 과제: 멀티쓰레드 프로그래밍 (0) 2021.01.21 8주차 과제: 인터페이스 (0) 2021.01.05 7주차 과제: 패키지 (0) 2020.12.30 6주차 과제: 상속 (0) 2020.12.24