Home @SneakyThrows
Post
Cancel

@SneakyThrows

** 글을 작성하게 된 계기**


사람들과 스프링 서버를 만들어보는 프로젝트를 진행하며 @SneakyThrows에 대해 질문받았습니다. 정확히 모르고 사용했던 부분도 있었는데, 이에 대해 정리하고 싶어 글을 작성하게 되었습니다.

프로젝트는 해당 링크에서 보실 수 있습니다.







2. @SneakyThrows


@SneakyThrows는 메서드의 throws 선언부에 실제 예외를 선언하지 않고 검사 예외(checked exceptions)를 던질 수 있게 해주는 어노테이션입니다.

@SneakyThrows can be used to sneakily throw checked exceptions without actually declaring this in your method’s throws clause. This somewhat contentious ability should be used carefully, of course. The code generated by lombok will not ignore, wrap, replace, or otherwise modify the thrown checked exception; it simply fakes out the compiler. On the JVM (class file) level, all exceptions, checked or not, can be thrown regardless of the throws clause of your methods, which is why this works.







즉, 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이 발생할 수 있다고 선언되어 있습니다.

An ‘impossible’ exception. For example, new String(someByteArray, “UTF-8”); declares that it can throw an UnsupportedEncodingException but according to the JVM specification, UTF-8 must always be available.







하지만 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 어노테이션은 인터페이스를 오버라이드 할 때, 상위 메서드로 예외를 전파하고 싶지 않을 때 사용할 수 있습니다. 이를 통해 코드의 간결성을 유지할 수 있습니다.

  1. 인터페이스를 오버라이드 할 때
  2. 상위 메서드로 전파하고 싶지 않을 때





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();
    }
}







이를 사용하지 않으면 컴파일 시점에 다음과 같은 오류 메시지가 뜨게 됩니다.

image







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에 대해 살펴보았습니다. 이는 암시적으로 예외를 처리하기 때문에 그다지 권장되는 방법은 아닌데요, 빠르게 개발해 결과를 봐야 할 때 정도만 사용하는 게 좋지 않을까 생각해 봅니다.


This post is licensed under CC BY 4.0 by the author.