[혼공학습단 9기] #4. 3주차 예외, 200% 이해하기
안녕하세요, 다람쥐 입니다.
벌써 한빛미디어 혼공학습단 9기 혼공자바 3주차 진도를 나가게 됐네요!
혼공 학습단을 하며 혼자 공부하는 자바를 공부하다보니 시간이 참 빠르네요!
연휴가 다가오니 설레기도 하네요. ☺️
한빛미디어 혼공학습단 9기 혼공자바 3주차는 Chapter 10 ~ 11 입니다.
오늘 할 포스팅은 Chapter 10 예외 파트를 다루려고 하는데요~
예외가 무엇인지, 어떻게 쓰는지 설명하고 예외 잘 쓰는 꿀팁들을 풀려고 합니다!
안 보면 손해입니다~ 😁
자바 개발 하다 한 번 쯤은 꼭 만나는 예외
아래 자바 코드를 실행하면 어떤 결과가 나올까요?
public class Example {
public static void main(String[] args) {
int[] numberArray = new int[] { 1, 2, 3 };
for (int index = 0; index <= numberArray.length; ++index) {
System.out.println(numberArray[index]);
}
}
}
> Task :Example.main() FAILED
1
Exception in thread "main" 2
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
3
at Example.main(Example.java:6)
Execution failed for task ':Example.main()'.
> Process 'command '/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
public class Calculator {
public void calculate() {
// ...
}
}
public class Example {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// ...
calculator = null;
// ...
calculate(calculator);
}
public static void calculate(Calculator calculator) {
calculator.calculate();
}
}
> Task :Example.main() FAILED
Exception in thread "main" java.lang.NullPointerException
at Example.calculate(Example.java:13)
at Example.main(Example.java:9)
Execution failed for task ':Example.main()'.
> Process 'command '/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
분명 잘 될거라 생각해서 실행을 했는데 중간에 빨간 글씨로 오류가 뜨네요. 😭
자바 코드를 실행했는데 빨간 글씨가 보인다면 머리가 하애지는 경험 다들 있으시지 않나요~? 🥺
특히 배열이나 조건의 처음과 마지막, 즉 경계를 충분히 검토하지 않아 ArrayIndexOutOfBoundException 예외를 만나거나
메서드 매개변수로 객체를 받을 때 정말 상상도 못하게 NullPointerException 이 나타나기도 합니다.
자바 프로그래밍을 할 때 정말 가장 흔한 예외를 만나봤는데요.
예외가 무엇이고 왜 만든 걸까요?
도대체 예외가 뭔데!?
예외는 사용자가 잘못된 조작을 하거나 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류를 말합니다.
예외를 발생한다, 고 표현합니다.
예외가 발생하면 프로그램이 곧바로 종료된다는 점에서 오류(에러)와 비슷한데요~
'예외 처리' (Exception handling) 로 프로그램이 종료되지 않고 정상 실행 상태가 유지되도록 할 수 있습니다.
자바에선 예외가 발생할 가능성이 보이는 코드를 컴파일할 때 예외 처리가 되는지, 안되는지 확인합니다!
만약 예외 처리 코드가 없다면 컴파일이 되지 않습니다.
물론 모든 예외를 확인하는 건 아닌데요.
예외의 종류에 따라 달라집니다!
예외의 종류에 따라 어떻게 컴파일 시에 예외 처리를 하라고 강요할 수 있는지 알아보죠!
어떻게 컴파일 단에서 예외 처리를 강제할 수 있을까?
예외의 종류는 Checked Exception, Unchecked Exception 이 있습니다.
혼공자바에선 Checked Exception 을 일반 예외, 라고 부르고
Unchecked Exception 을 실행 예외(Runtime Exception)라고 부릅니다.
바로 Checked Exception 이 컴파일할 때 예외 처리가 되어있는지 확인을 하는데요~
반드시 try ~ catch 으로 예외 처리를 하거나 throws 키워드로 예외가 발생한 메서드를 호출한 메서드로 예외 처리 책임을 전가합니다.
calculate 메서드에서 throw Exception 으로, 호출한 메서드에게 Exception 예외를 처리해달라는 책임을 이동하고 있습니다.
그래서 IDE 단에서 이를 감지하여 문법 오류로 저희에게 알려줄 수 있습니다.
예외는 자바 언어상으로 어떤 구조로 구현되어 있을까?
Exception 클래스에서 연관된 다른 클래스를 정의 이동 (CMD + B, 또는 CMD + 클릭, 윈도우는 Control 키) 으로 찾아봤습니다.
Exception 클래스, Throwable 클래스, Error 클래스,. RuntimeException 클래스로 이루어져 있습니다.
/**
* The class {@code Exception} and its subclasses are a form of
* {@code Throwable} that indicates conditions that a reasonable
* application might want to catch.
*
* <p>The class {@code Exception} and any subclasses that are not also
* subclasses of {@link RuntimeException} are <em>checked
* exceptions</em>. Checked exceptions need to be declared in a
* method or constructor's {@code throws} clause if they can be thrown
* by the execution of the method or constructor and propagate outside
* the method or constructor boundary.
*
* @author Frank Yellin
* @see java.lang.Error
* @jls 11.2 Compile-Time Checking of Exceptions
* @since 1.0
*/
public class Exception extends Throwable {
...
}
/**
* The {@code Throwable} class is the superclass of all errors and
* exceptions in the Java language. Only objects that are instances of this
* class (or one of its subclasses) are thrown by the Java Virtual Machine or
* can be thrown by the Java {@code throw} statement. Similarly, only
* this class or one of its subclasses can be the argument type in a
* {@code catch} clause.
*
* For the purposes of compile-time checking of exceptions, {@code
* Throwable} and any subclass of {@code Throwable} that is not also a
* subclass of either {@link RuntimeException} or {@link Error} are
* regarded as checked exceptions.
*
* ...
*
* @author unascribed
* @author Josh Bloch (Added exception chaining and programmatic access to
* stack trace in 1.4.)
* @jls 11.2 Compile-Time Checking of Exceptions
* @since 1.0
*/
public class Throwable implements Serializable {
...
}
/**
* An {@code Error} is a subclass of {@code Throwable}
* that indicates serious problems that a reasonable application
* should not try to catch. Most such errors are abnormal conditions.
* The {@code ThreadDeath} error, though a "normal" condition,
* is also a subclass of {@code Error} because most applications
* should not try to catch it.
* <p>
* A method is not required to declare in its {@code throws}
* clause any subclasses of {@code Error} that might be thrown
* during the execution of the method but not caught, since these
* errors are abnormal conditions that should never occur.
*
* That is, {@code Error} and its subclasses are regarded as unchecked
* exceptions for the purposes of compile-time checking of exceptions.
*
* @author Frank Yellin
* @see java.lang.ThreadDeath
* @jls 11.2 Compile-Time Checking of Exceptions
* @since 1.0
*/
public class Error extends Throwable {
...
}
/**
* {@code RuntimeException} is the superclass of those
* exceptions that can be thrown during the normal operation of the
* Java Virtual Machine.
*
* <p>{@code RuntimeException} and its subclasses are <em>unchecked
* exceptions</em>. Unchecked exceptions do <em>not</em> need to be
* declared in a method or constructor's {@code throws} clause if they
* can be thrown by the execution of the method or constructor and
* propagate outside the method or constructor boundary.
*
* @author Frank Yellin
* @jls 11.2 Compile-Time Checking of Exceptions
* @since 1.0
*/
public class RuntimeException extends Exception {
...
}
Exception 클래스는 Throwable 클래스를 상속합니다.
Throwable 클래스는 자바 언어에 있는 모든 오류와 예외의 부모(슈퍼) 클래스인데요. 이 클래스와 이 클래스를 상속한 자식 클래스만 JVM 에서 throw 되거나 catch 절에 throw 될 수 있습니다.
Checked Exception 은 오류 클래스(java.lang.Error)와, RuntimeException(Unchecked Exception)이 아닌 Exception 클래스(java.lang.Exception) 를 의미한다고 하네요!
그림으로 정리하자면 아래와 같습니다.
Error 클래스와 Exception 클래스의 차이점이 정확히 뭘까?
Error(에러) 클래스와 Exception(예외) 클래스 주석 설명에서 차이를 알 수 있는데요.
에러 클래스는 심각한 문제를 설명하는 클래스이며 try ~ catch 로 잡을 수 없습니다. 비정상적인 조건일 때 try ~ catch 로 잡을 수 없는 에러가 발생한다고 합니다.
에러 클래스와 그 서브 클래스들은 `throws` 키워드로 명시를 해도 비정상적인 조건으로 발생하기 때문에 발생하지 않아 명시하지 않아도 된다고 하네요.
또 Error 클래스의 서브 클래스인 ThreadDeath 클래스의 경우 정상적인 조건임에도 발생될 수 있다고 합니다.
반면 예외 클래스는 try ~ catch 으로 잡을 수 있는 조건입니다.
RuntimeException 이거나 그의 자식 클래스가 아닌 예외 플래스와 예의 클래스의 자식 클래스는 모두 Checked Exception 에 해당됩니다.
예외 처리를 필수로 해야하며 컴파일 단에서 이를 잡을 수 있습니다.
RuntimeException 과 그의 자식 클래스는 모두 Unchecked Exception 에 해당됩니다.
try ~ catch 으로 예외 처리 하기
예외 처리를 해주는 방법은 try ~ catch 문법을 이용합니다!
try 블록에는 예외가 발생할 수 있는 코드를 넣습니다.
catch 절에 예외를 처리하고픈 예외 클래스를 명시하고서,
catch 블록에 프로그램을 종료하지 않고 어떻게 예외를 처리할지 코드를 작성합니다.
public class Example {
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.lang.String2");
} catch(ClassNotFoundException e) {
e.printStackTrace();
System.out.println("클래스가 존재하지 않습니다.");
}
}
}
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Class.forName 정적 메서드는 ClassNotFoundException 예외를 throw 합니다.
따라서 이 메서드를 사용하는 사람이 직접 예외를 처리를 해줘야 하는데요!
ClassNotFoundException 예외는 ReflectiveOperationException 예외를 상속하고,
ReflectiveOperationException 예외는 Exception 클래스를 상속하므로
Checked Exception 에 해당됩니다.
Checked Exception 이라 IDE 에서 문법 오류로 체크해주고 있습니다. 물론 컴파일 때에도 컴파일 오류를 확인할 수 있습니다!
그래서 try ~ catch 문으로 예외를 반드시 처리해줘야 합니다.
catch 절에는 ClassNotFoundException e 으로 예외 처리할 클래스를 받을 수 있는데요!
catch 블록에서 e.printStackTrace(); 코드로 어디서 예외가 발생했는지 System.err 스트림으로 출력해줍니다.
그리고 별도로 "클래스가 존재하지 않습니다." 라는 문구를 출력합니다.
실행 결과는 아래와 같습니다.
클래스가 존재하지 않습니다.
java.lang.ClassNotFoundException: java.lang.String2
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
at Example.main(Example.java:4)
중간에 예외가 발생하여 프로그램이 종료될 뻔 했으나
예외 처리로 프로그램은 정상적으로 실행이 마치게 됩니다.
catch 절과 블록은 여러 개 만들 수 있습니다.
위에서 아래로 차례대로 예외 클래스에 해당하는 catch 절을 찾게 되어 해당 블록 코드를 실행하는데요.
보통은 구체적인 예외 클래스를 먼저 검사하고 뒤로 갈수록 큰 범위의 예외 클래스로 범용적으로 처리해주게끔 작업하는 게 일반적입니다.
추가로 finally 블록이 있는데요!
try 블록을 정상적으로 실행했어도 finally 블록이 실행되고
중간에 예외가 발생하여 catch 블록으로 실행했어도 finally 블록이 실행됩니다.
finally 블록을 사용할 때는 리소스를 정리하는 용도로 쓰는 편입니다.
네트워크 연결을 정리하거나 파일을 닫거나, 스레드를 정리합니다.
나중에 나올 Try With Resources 문법으로 대체되긴 하지만 예전에는 이렇게 작성했다고 하네요!
이거는 나중에 알아보죠!
public class Example {
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.lang.String2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.out.println("클래스가 존재하지 않습니다.");
} catch (Exception e) {
e.printStackTrace();
System.out.println("예외가 발생했습니다.");
} finally {
System.out.println("finally 블록");
// ~~~.close();
}
}
}
Try ~ Catch 바이트 코드로 변환하면 어떻게 될까?
위 코드를 바이트 코드로 변환해보겠습니다.
$ javap -v Example
Classfile /~/Example.class
Last modified 2023. 1. 16.; size 1118 bytes
MD5 checksum 11e3d595a353ac188ec73035715808b9
Compiled from "Example.java"
public class Example
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #13 // Example
super_class: #14 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #14.#33 // java/lang/Object."<init>":()V
#2 = String #34 // java.lang.String2
#3 = Methodref #35.#36 // java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
#4 = Fieldref #37.#38 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #39 // finally 블록
#6 = Methodref #40.#41 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #42 // java/lang/ClassNotFoundException
#8 = Methodref #7.#43 // java/lang/ClassNotFoundException.printStackTrace:()V
#9 = String #44 // 클래스가 존재하지 않습니다.
#10 = Class #45 // java/lang/Exception
#11 = Methodref #10.#43 // java/lang/Exception.printStackTrace:()V
#12 = String #46 // 예외가 발생했습니다.
#13 = Class #47 // Example
#14 = Class #48 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 LExample;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 e
#25 = Utf8 Ljava/lang/ClassNotFoundException;
#26 = Utf8 Ljava/lang/Exception;
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 StackMapTable
#30 = Class #49 // java/lang/Throwable
#31 = Utf8 SourceFile
#32 = Utf8 Example.java
#33 = NameAndType #15:#16 // "<init>":()V
#34 = Utf8 java.lang.String2
#35 = Class #50 // java/lang/Class
#36 = NameAndType #51:#52 // forName:(Ljava/lang/String;)Ljava/lang/Class;
#37 = Class #53 // java/lang/System
#38 = NameAndType #54:#55 // out:Ljava/io/PrintStream;
#39 = Utf8 finally 블록
#40 = Class #56 // java/io/PrintStream
#41 = NameAndType #57:#58 // println:(Ljava/lang/String;)V
#42 = Utf8 java/lang/ClassNotFoundException
#43 = NameAndType #59:#16 // printStackTrace:()V
#44 = Utf8 클래스가 존재하지 않습니다.
#45 = Utf8 java/lang/Exception
#46 = Utf8 예외가 발생했습니다.
#47 = Utf8 Example
#48 = Utf8 java/lang/Object
#49 = Utf8 java/lang/Throwable
#50 = Utf8 java/lang/Class
#51 = Utf8 forName
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/Class;
#53 = Utf8 java/lang/System
#54 = Utf8 out
#55 = Utf8 Ljava/io/PrintStream;
#56 = Utf8 java/io/PrintStream
#57 = Utf8 println
#58 = Utf8 (Ljava/lang/String;)V
#59 = Utf8 printStackTrace
{
public Example();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LExample;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String java.lang.String2
2: invokestatic #3 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
5: astore_1
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #5 // String finally 블록
11: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: goto 76
17: astore_1
18: aload_1
19: invokevirtual #8 // Method java/lang/ClassNotFoundException.printStackTrace:()V
22: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #9 // String 클래스가 존재하지 않습니다.
27: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
33: ldc #5 // String finally 블록
35: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: goto 76
41: astore_1
42: aload_1
43: invokevirtual #11 // Method java/lang/Exception.printStackTrace:()V
46: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #12 // String 예외가 발생했습니다.
51: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
57: ldc #5 // String finally 블록
59: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: goto 76
65: astore_2
66: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
69: ldc #5 // String finally 블록
71: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
74: aload_2
75: athrow
76: return
Exception table:
from to target type
0 6 17 Class java/lang/ClassNotFoundException
0 6 41 Class java/lang/Exception
0 6 65 any
17 30 65 any
41 54 65 any
LineNumberTable:
line 4: 0
line 12: 6
line 13: 14
line 5: 17
line 6: 18
line 7: 22
line 12: 30
line 13: 38
line 8: 41
line 9: 42
line 10: 46
line 12: 54
line 13: 62
line 12: 65
line 13: 74
line 14: 76
LocalVariableTable:
Start Length Slot Name Signature
18 12 1 e Ljava/lang/ClassNotFoundException;
42 12 1 e Ljava/lang/Exception;
0 77 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 4
frame_type = 81 /* same_locals_1_stack_item */
stack = [ class java/lang/ClassNotFoundException ]
frame_type = 87 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 87 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 10 /* same */
}
SourceFile: "Example.java"
이전에 봤던 바이트코드에서 새로 생긴 게 있네요?!
Exception table 이란게 새로 등장했는데요.
Exception table:
from to target type
0 6 17 Class java/lang/ClassNotFoundException
0 6 41 Class java/lang/Exception
0 6 65 any
17 30 65 any
41 54 65 any
from 과 to 는 예외가 발생할 만한 구간으로 보이고
만약에 type 에 해당하는 예외가 발생하면 target 으로 이동한다라고 이해됩니다.
from, to, target 에 있는 상수값(?)은 어디에 있나 봤더니 바로 그 아래에
LineNumberTable 로 바이트코드 라인을 관리하고 있습니다.
LineNumberTable:
line 4: 0
line 12: 6
line 13: 14
line 5: 17
line 6: 18
line 7: 22
line 12: 30
line 13: 38
line 8: 41
line 9: 42
line 10: 46
line 12: 54
line 13: 62
line 12: 65
line 13: 74
line 14: 76
line 4 ~ line 12 까지 예외가 실행될만한 코드로 try 블록에 해당됩니다.
target 17 은 ClassNotFoundException 예외를 처리하는 catch 블록을 의미하고
target 41 은 Exception 예외를 처리하는 catch 블록을 의미합니다.
그리고 만약에 예외가 발생하지 않는다면 try 블록부터 catch 블록까지 모두 type any 으로 finally 블록으로 이동됩니다.
바이트 코드로 변환되면 Exception table 이란 걸로 예외를 관리한다는 걸 알게 됐네요.
Try with Resources 가 뭔가요~?
try-with-resources 는 자바 7에 나온 문법입니다.
try 절 안에 리소스를 사용하는 변수를 정의하면은 try 블록이 끝나면 자동으로 리소스를 정리해주는 문법이 있더라고요!
기존 try ~ catch 문법을 사용하면 모든 분기에 리소스를 닫아주거나 처리해주는 코드를 넣어야 합니다.
finally 블록으로도 해결할 수 있지만
문법적으로 자동으로 리소스를 정리해준다면 굳이 코드를 안 넣어도 됩니다!
코드를 안 넣는다는 건 그만큼 버그 발생이 적다는 의미로 개발할 때 고통을 적게 받는 다는 걸 의미합니다!
// Before
Scanner scanner = null;
try {
scanner = new Scanner(new File("test.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
// After
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
위 코드처럼 Scanner 스트림을 사용하는 객체를 try 블록 이전에 정의하고 finally 에서 null 체크 이후 닫아줍니다.
try 블록 안에서 어떤 코드까지 가다가 예외가 발생할 수도 있는 터라 객체 생성의 시점도 신경을 써야 합니다.
Scanner 객체가 try 블록 이전에 정의되기에, try ~ catch 가 끝나도 변수가 남아있는데요.
그 변수를 이용할 때 마다 null 체크를 해줘야 하므로 여간 귀찮은 것이 아니어 보이네요.
After 에선 try 절 안에 Scanner 객체 생성을 넣어주는데요!
try 블록 안에서만 스코프 범위로 사용이 가능하며, 그 뒤에선 전혀 신경을 쓸 필요가 없게 됩니다.
그리고 까먹기 쉬운 리소스 정리도 대신 해줘서 코드 작성할 때 편안한 기분으로 코딩할 수 있게 됩니다. ☺️
Try With Resources 리소스 정리 원리
그런데 try 절에 넣는다고 어떻게 자동으로 사용하던 리소스가 종료되는 걸까요?
Scanner 클래스 코드를 한 번 봐봅시다!
// Scanner.java
/**
* ...
* <p>When a {@code Scanner} is closed, it will close its input source
* if the source implements the {@link java.io.Closeable} interface.
* ...
*/
public final class Scanner implements Iterator<String>, Closeable {
...
// public methods
/**
* Closes this scanner.
*
* <p> If this scanner has not yet been closed then if its underlying
* {@linkplain java.lang.Readable readable} also implements the {@link
* java.io.Closeable} interface then the readable's {@code close} method
* will be invoked. If this scanner is already closed then invoking this
* method will have no effect.
*
* <p>Attempting to perform search operations after a scanner has
* been closed will result in an {@link IllegalStateException}.
*
*/
public void close() {
if (closed)
return;
if (source instanceof Closeable) {
try {
((Closeable)source).close();
} catch (IOException ioe) {
lastException = ioe;
}
}
sourceClosed = true;
source = null;
closed = true;
}
}
// Closable.java
/**
* A {@code Closeable} is a source or destination of data that can be closed.
* The close method is invoked to release resources that the object is
* holding (such as open files).
*
* @since 1.5
*/
public interface Closeable extends AutoCloseable {
/**
* Closes this stream and releases any system resources associated
* with it. If the stream is already closed then invoking this
* method has no effect.
*
* <p> As noted in {@link AutoCloseable#close()}, cases where the
* close may fail require careful attention. It is strongly advised
* to relinquish the underlying resources and to internally
* <em>mark</em> the {@code Closeable} as closed, prior to throwing
* the {@code IOException}.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException;
}
// AutoCloseable.java
/**
* An object that may hold resources (such as file or socket handles)
* until it is closed. The {@link #close()} method of an {@code AutoCloseable}
* object is called automatically when exiting a {@code
* try}-with-resources block for which the object has been declared in
* the resource specification header. This construction ensures prompt
* release, avoiding resource exhaustion exceptions and errors that
* may otherwise occur.
*
* @apiNote
* <p>It is possible, and in fact common, for a base class to
* implement AutoCloseable even though not all of its subclasses or
* instances will hold releasable resources. For code that must operate
* in complete generality, or when it is known that the {@code AutoCloseable}
* instance requires resource release, it is recommended to use {@code
* try}-with-resources constructions. However, when using facilities such as
* {@link java.util.stream.Stream} that support both I/O-based and
* non-I/O-based forms, {@code try}-with-resources blocks are in
* general unnecessary when using non-I/O-based forms.
*
* @author Josh Bloch
* @since 1.7
*/
public interface AutoCloseable {
/**
* Closes this resource, relinquishing any underlying resources.
* This method is invoked automatically on objects managed by the
* {@code try}-with-resources statement.
*
* <p>While this interface method is declared to throw {@code
* Exception}, implementers are <em>strongly</em> encouraged to
* declare concrete implementations of the {@code close} method to
* throw more specific exceptions, or to throw no exception at all
* if the close operation cannot fail.
*
* <p> Cases where the close operation may fail require careful
* attention by implementers. It is strongly advised to relinquish
* the underlying resources and to internally <em>mark</em> the
* resource as closed, prior to throwing the exception. The {@code
* close} method is unlikely to be invoked more than once and so
* this ensures that the resources are released in a timely manner.
* Furthermore it reduces problems that could arise when the resource
* wraps, or is wrapped, by another resource.
*
* <p><em>Implementers of this interface are also strongly advised
* to not have the {@code close} method throw {@link
* InterruptedException}.</em>
*
* This exception interacts with a thread's interrupted status,
* and runtime misbehavior is likely to occur if an {@code
* InterruptedException} is {@linkplain Throwable#addSuppressed
* suppressed}.
*
* More generally, if it would cause problems for an
* exception to be suppressed, the {@code AutoCloseable.close}
* method should not throw it.
*
* <p>Note that unlike the {@link java.io.Closeable#close close}
* method of {@link java.io.Closeable}, this {@code close} method
* is <em>not</em> required to be idempotent. In other words,
* calling this {@code close} method more than once may have some
* visible side effect, unlike {@code Closeable.close} which is
* required to have no effect if called more than once.
*
* However, implementers of this interface are strongly encouraged
* to make their {@code close} methods idempotent.
*
* @throws Exception if this resource cannot be closed
*/
void close() throws Exception;
}
Scanner 클래스 문서 주석을 보면 Closeable 인터페이스를 구현해서
입력 소스를 자동으로 닫아준다고 나와있는데요~
Scanner 클래스 선언을 보면 Closeable 인터페이스를 구현한 걸 확인할 수 있습니다.
아래 쪽엔 Closeable 인터페이스 코드를 달아두었는데요!
public void close() throws IOException;
사용하는 스트림을 닫거나 어떤 시스템 자원을 릴리즈하는 메서드를 구현해야한다고 나와있습니다.
Scanner 클래스에서 구현한 close 메서드를 보면 이미 닫았다면 메서드를 종료하고, 사용중인 리소스가 있다면 종료시키고 있습니다.
source 인스턴스 변수가 Closeable 인터페이스라면 close 메서드를 호출해줍니다.
Scanner 클래스 코드에서 인터페이스를 잘 활용하고 있는 걸 배울 수 있었네요..!
Try With Resources 문법과 관련한 설명은 Closeable 이 상속한 AutoClosable 에서 확인할 수 있었어요.
Closeable 과 다른 점도 문서 주석으로 열심히 설명해두었네요.
AutoCloseable 는 idempotent 하게 만드는 걸 권장하지만 idempotent 를 꼭 보장하지는 않아도 된다고 합니다.
반면 Closeable 은 idempotent 하다고 하네요.
idempotent 하다면 계속 close 메서드를 호출해도 사이드 이펙트가 없다고 나와 있습니다.
Scanner 클래스의 close 메서드에선 closed 플래그 변수를 활용하여 이미 리소스를 정리하고나서 다시 호출해도 영향이 가지 않도록 분기 처리를 해줬는데요~
AutoCloseable 을 구현한 객체에서 idempotent 하게 만들지 않았다면 예외가 발생하는 등의 사이드 이펙트가 발생할 수 있으니 주의해달라는 이야기 였네요~
idempotent 가 사전으로 '멱등법칙' 이라고 하며 수학이나 전산학에서 쓰이는 용어입니다.
연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미합니다.
예외 잘 쓰는 방법
예외를 잘 쓰는 방법이 따로 있을까 싶어서 여러 곳에서 이야기하는 베스트 프랙티스를 몇 가지 알아봤습니다.
예외를 제대로 활용한다면 프로그램의 가독성과 신뢰성, 유지보수성이 높아지지만
예외를 잘못 사용하면 그 반대의 효과가 나타난다고 하네요.
첫 번째로 예외는 진짜 예외 상황에서만 사용합니다!
코딩하는 입장에선 이런 예외를 일부러(?) 발생시켜서 일종의 제어 흐름으로 쓰는 유혹이 있을 수 있는데요.
조건문이나 반복문처럼 의도적으로 분기 처리를 위한 제어 흐름용으로 사용한다면 유지 보수가 무척 힘들어집니다.
일반적인 사용법이 아닌 일종의 '흑마법' 을 사용하면 사용할수록 엉뚱한 곳에서 눈치 채기 어려운 버그가 생길 수가 있기에
조금의 성능 개선이 실제로 있을 수 있을지라도, 극단적인 환경이 아니라면 최대한 자제하는 게 좋습니다.
잘 설계된 API 라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없어야 된다고 합니다.
특정 상태에서만 호출할 수 있는 메서드를 제공한다면 예외로 분기 처리를 유도하기 보단, 특정 상태임을 확인할 수 있는 메서드도 함께 제공해야만 합니다.
예외로 분기 처리를 유도하는 API 를 만들어서도 안된다고 합니다.
두 번째로 Checked exception 과 Unchecked exception 을 언제 사용해야 할까를 알아봅니다.
Checked exception 은 호출하는 쪽에서 복구하는 걸 기대하고 설계했다면 사용하라고 합니다.
물론 호출하는 사람이 아무런 예외 처리를 하지 않을 수 있지만 통상적으론 그래야하지 않아야 합니다.
복구에 필요한 정보도 함께 넘겨주는 메서드도 제공해줘야 호출하는 사람이 적절한 복구 로직을 작성할 수 있습니다.
Unchecked exception 은 프로그래밍 오류를 나타낼 때 사용합니다.
런타임 예외의 대부분은 코드를 실행하는 데 필요한 전제 조건을 만족하지 못했을 때 인데요~
단순히 클라이언트가 해당 API 의 명세에 기록된 제약을 지키지 못했다는 뜻입니다.
두 상황 모두 애매하다면 일단은 Unchecked exception 을 던지는 걸로 결정합니다.
세 번째로 표준 예외를 사용합니다.
표준 예외를 재사용하면 내가 만든 API 가 다른 사람이 익히기 사용하기 쉬울 수 있다.
예외 클래스가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸리는 성능적인 이점도 있습니다.
가장 많이 재사용되는 예외는 매개 변수가 잘못될 때 호출하는 IllegalArgumentException 과 객체의 상태가 적절하지 않을 때 호출하는 IllegalStateException 도 자주 사용합니다.
그 외에도 NullPointerException, IndexOutOfBoundsException, ConcurrentModificationException, UnsupportedOperationException 을 적재적소에 사용하는 걸 권장한다고 하네요!
Exception, RuntimeException, Throwable, Error 클래스는 안정적으로 테스트할 수 없어서 직접 사용하지 않는 걸 추천한다고 하네요.
만약 더 많은 정보를 제공하길 원한다면 표준 예외를 확장해도 좋다. 예외는 직렬화할 수 있으므로 직렬화 로직도 고려해야 한다.
직렬화에는 많은 버그가 발생할 수 있으므로 최대한 확장하지 않고 기존걸 쓰는 걸 추천하는 이유다.
인수 값이 어떤 것이든 실패한다면 IllegalStateException 을, 그렇지 않으면 IllegalArgumentException 을 throw 하는 걸 추천합니다.
마지막으로 메서드가 던지는 모든 예외를 문서화합니다.
Checked Exception 은 항상 따로 따로 선언하고 자바독의 @throws 태그를 사용하여 정확히 문서화해야 한다고 합니다.
공통 상위 클래스로 설명하는 건 피해야 한다고 합니다.
Exception, Throwable 으로 예외를 선언한다면 어떻게 예외를 처리해야하는 지 파악하기 어렵고
다른 예외까지 영향을 끼칠 수 있으므로 API 사용성을 크게 떨어뜨립니다.
유일한 예외는 main 메서드는 JVM만 호출하므로 Exception 을 던지도록 선언해도 괜찮습니다.
Unchecked Exception 도 마찬가지로 문서화를 꼼꼼하게 작성하지만 throws 목록에 넣지는 않아야 한다고 합니다.
Checked 인지, Unchecked 인지에 따라 API 사용자가 예외 처리해야할 일이 달라지므로 확실히 구분해 두는 게 좋다고 합니다.
혼공 용어 노트에 오늘 배운 용어 정리하기
한빛미디어 혼공자바 사이트를 보면 '용어노트'를 따로 PDF 로 다운로드 받을 수 있습니다.
PDF 파일을 제공하다보니 아이패드같은 태블릿으로 불러와서
가볍게 용어를 복습할 수 있고 또 배운 내용을 추가할 수도 있어서 편리하더라고요.
오늘 배운 idempotent 용어를 새로 한 번 필기해봤습니다!
마무리
오늘은 혼공자바 10장 예외에 대해 공부했습니다!
예외 클래스의 구조를 알아보고 어떻게 바이트코드로 변환이 되는지, try ~ catch 으로 예외 처리하는 방법을 배웠습니다.
다음에는 11장 기본 API 클래스를 공부하여 포스팅 해보도록 하겠습니다!
혼공학습단 9기 다른 글 보러가기
2023.01.10 - [자유/대외 활동] - [혼공학습단 9기] #3. 2주차 객체 지향 프로그래밍을 왜 쓸까?
2023.01.08 - [자유/대외 활동] - [혼공학습단 9기] #2. 자바 기본 정말 안다고 생각해?
2023.01.08 - [자유/대외 활동] - [혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정