** 글을 작성하게 된 계기**
사람들과 스프링 서버를 만들어보는 프로젝트를 진행하며 @SneakyThrows에 대해 질문받았습니다. 정확히 모르고 사용했던 부분도 있었는데, 이에 대해 정리하고 싶어 글을 작성하게 되었습니다.
프로젝트는 해당 링크에서 보실 수 있습니다.
2. @SneakyThrows
@SneakyThrows는 메서드의 throws 선언부에 실제 예외를 선언하지 않고 검사 예외(checked exceptions)를 던질 수 있게 해주는 어노테이션입니다.
즉, throws 나 try-catch 구문을 사용해 명시적 예외 처리를 해주지 않고, @SneakyThrows 어노테이션을 사용해 예외를 처리할 수 있는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
class Test {
public static void main(String[] args) {
readFile("hello-world.txt");
}
// throws에 IOException을 선언하지 않아도 된다.
@SneakyThrows(IOException.class)
private static void readFile(String file) {
BufferedReader reader = new BufferedReader(new FileReader(file));
log.info("Content: {}", reader.readLine());
reader.close();
}
}
이는 지원되지 않은 인코딩 타입에 대해서는 UnsupportedEncodingException이 발생할 수 있다고 선언되어 있습니다.
하지만 JVM defaultCharset을 확인해 보면 UTF-8인 것을 알 수 있기에, 이를 걱정할 필요는 없습니다.
1
2
3
4
5
6
class Test {
public static void main(String[] args) {
Charset defaultCharset = Charset.defaultCharset();
System.out.println("Default character encoding: " + defaultCharset);
}
}
1
Default character encoding: UTF-8
3. 언제 사용할까?
@SneakyThrows 어노테이션은 인터페이스를 오버라이드 할 때, 상위 메서드로 예외를 전파하고 싶지 않을 때 사용할 수 있습니다. 이를 통해 코드의 간결성을 유지할 수 있습니다.
- 인터페이스를 오버라이드 할 때
- 상위 메서드로 전파하고 싶지 않을 때
3-1. 인터페이스를 오버라이드 할 때
Runnable 인터페이스를 구현할 때, 검사 예외를 throws할 수 없습니다. 메서드 내부에서 발생한 예외는 호출한 쓰레드로 제대로 전파되지 않기 때문인데, 인터페이스를 보더라도 throws가 없는 것을 볼 수 있습니다.
1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
따라서 아래와 같이 run 메서드 내부에서 이를 적절히 처리해 줘야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
class Example {
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
public void run() {
try {
throw new IOException("IOException");
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
};
new Thread(task).start();
}
}
try/catch를 사용하지 않기 위해서는 아래와 같이 @SneakyThrows를 사용해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
class Example {
private static final boolean ERROR = true;
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
@SneakyThrows
public void run() {
if (ERROR) {
throw new IOException("IOException.");
}
}
};
new Thread(task).start();
}
}
이를 사용하지 않으면 컴파일 시점에 다음과 같은 오류 메시지가 뜨게 됩니다.
3-2. 상위 메서드로 예외를 전파하고 싶지 않을 때
@SneakyThrows 어노테이션을 사용하면 검사 예외에서도 throws 선언, 또는 try-catch 처리를 하지 않아도 됩니다. 따라서 상위 메서드로 예외를 전파하지 않아도 되며, 이 경우 이를 사용하기에 적합합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
class Test {
public static void main(String[] args) {
readFile("hello-world.txt");
}
// throws에 IOException을 선언하지 않아도 된다.
@SneakyThrows(IOException.class)
private static void readFile(String file) {
BufferedReader reader = new BufferedReader(new FileReader(file));
log.info("Content: {}", reader.readLine());
reader.close();
}
}
4. 컴파일 시점에는?
그렇다면 @SneakyThrows을 사용했을 때, 바이트 코드는 어떻게 처리될까요? 아래 예시 코드를 살펴보겠습니다.
1
2
3
4
5
6
7
8
9
class Test {
@SneakyThrows
public void test(String parameter) {
if (parameter == null) {
throw new IOException("Exception");
}
}
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class project/jpa/compositekey/core/domain/student/Example {
// compiled from: TransactionInterceptor.java
// access flags 0x1A
private final static Lorg/slf4j/Logger; log
// access flags 0x1A
private final static Z ERROR = 1
// access flags 0x0
<init>()V
L0
LINENUMBER 19 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lproject/jpa/compositekey/core/domain/student/Example; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public test(Ljava/lang/String;)V
TRYCATCHBLOCK L0 L1 L2 java/lang/Throwable
L0
LINENUMBER 24 L0
ALOAD 1
IFNONNULL L1
L3
LINENUMBER 25 L3
NEW java/io/IOException
DUP
LDC "Exception"
INVOKESPECIAL java/io/IOException.<init> (Ljava/lang/String;)V
ATHROW
L1
LINENUMBER 22 L1
FRAME SAME
GOTO L4
L2
FRAME SAME1 java/lang/Throwable
ASTORE 2
L5
ALOAD 2
ATHROW
L4
LINENUMBER 27 L4
FRAME SAME
RETURN
L6
LOCALVARIABLE $ex Ljava/lang/Throwable; L5 L4 2
LOCALVARIABLE this Lproject/jpa/compositekey/core/domain/student/Example; L0 L6 0
LOCALVARIABLE parameter Ljava/lang/String; L0 L6 1
MAXSTACK = 3
MAXLOCALS = 3
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 30 L0
NEW project/jpa/compositekey/core/domain/student/Example
DUP
INVOKESPECIAL project/jpa/compositekey/core/domain/student/Example.<init> ()V
ASTORE 1
L1
LINENUMBER 31 L1
ALOAD 1
ACONST_NULL
INVOKEVIRTUAL project/jpa/compositekey/core/domain/student/Example.test (Ljava/lang/String;)V
L2
LINENUMBER 32 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE example Lproject/jpa/compositekey/core/domain/student/Example; L1 L3 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 18 L0
LDC Lproject/jpa/compositekey/core/domain/student/Example;.class
INVOKESTATIC org/slf4j/LoggerFactory.getLogger (Ljava/lang/Class;)Lorg/slf4j/Logger;
PUTSTATIC project/jpa/compositekey/core/domain/student/Example.log : Lorg/slf4j/Logger;
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
바이트 코드를 보면 똑같이 컴파일된 것을 볼 수 있는데, 즉, 단순히 try/catch 블록을 사용하지 않도록 우회만 해주는 것입니다.
1
2
3
4
5
NEW java/io/IOException
DUP
LDC "Exception"
INVOKESPECIAL java/io/IOException.<init> (Ljava/lang/String;)V
ATHROW
5.권장하지 않는 이유
하지만 이는 사용을 권장하지는 않는데요, 예외를 명시적으로 처리할 수 없기 때문입니다. 자바에서 검사 예외는 개발자가 예외적 상황을 적절하게 처리하도록 강제합니다. 이를 우회하면 예외가 발생했을 때 이를 알 수 없어, 의도한 대로 동작하지 않을 가능성이 존재하므로 안정성과 유지보수성이 저하될 수 있습니다.
또한 암시적인 것보다 명시적인 것이, 즉 코드 레벨에서 드러난 것이 훨씬 더 명확히 의도를 인지할 수 있습니다.
6. 정리
@SneakyThrows에 대해 살펴보았습니다. 이는 암시적으로 예외를 처리하기 때문에 그다지 권장되는 방법은 아닌데요, 빠르게 개발해 결과를 봐야 할 때 정도만 사용하는 게 좋지 않을까 생각해 봅니다.