우테코 3주차를 진행하면서 예외 처리 문제로 고생을 했던 일이 있어서 따로 포스트를 합니다.
예외 처리에 대한 포스트는 이전 포스트를 참고해 주세요.
우아한 테크코스 3주차 도중 겪은 문제점
발생했던 원인 : printStackTrace() vs getMessage()
[ Application.test ]
@Test
void 예외_테스트() {
assertSimpleTest(() -> {
runException("1000j");
assertThat(output()).contains(ERROR_MESSAGE);
});
}
위와 같은 테스트 코드를 실행 중이었습니다. 여기서 출력값에 ERROR_MESSAGE값이 포함되어 있으면 예외를 통과하게 되는 코드였습니다.
[ Application.java ]
try {
startLotto();
} catch (IllegalArgumentException exception) {
exception.printStackTrace();
}
처음에는 위 코드와 같이 예외를 잡아서 던지는 것으로 처리를 했습니다.
따라서 IllegalArgumentException 예외를 만나게 되면 Call 스택에 있는 함수들에 대한 내용과 함께 출력을 했습니다.
하지만 여기서 에러가 발생하였습니다.
반대로
try {
startLotto();
} catch (IllegalArgumentException exception) {
System.out.println(exception.getMessage());
}
이렇게 메세지를 출력했을때는 통과를 하였습니다.
printStackTrace(), getMessage() 차이
결과로 보는 차이
[ printStackTrace() ]
class Main {
public static void main(String[] args) {
try {
test1();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} finally {
System.out.println("Finally");
}
System.out.println("finally 이외 끝");
}
public static void test1() {
System.out.println("Before test");
test2();
System.out.println("After test");
}
public static void test2() {
throw new IllegalArgumentException("에러");
}
}

[ System.out을 통한 getMessage() 출력 ]

두 메서드에서는 출력의 차이와 함께 순서또한 차이가 있었습니다. 왜 그럴까?
printStackTrace()
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
|
public class Throwable implements Serializable {
// ... 중략
public void printStackTrace() {
printStackTrace(System.err);
}
/**
* Prints this throwable and its backtrace to the specified print stream.
*
* @param s {@code PrintStream} to use for output
*/
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
dejaVu.add(this);
synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
private void printEnclosedStackTrace(PrintStreamOrWriter s,
StackTraceElement[] enclosingTrace,
String caption,
String prefix,
Set<Throwable> dejaVu) {
assert Thread.holdsLock(s.lock());
if (dejaVu.contains(this)) {
s.println(prefix + caption + "[CIRCULAR REFERENCE: " + this + "]");
} else {
dejaVu.add(this);
// Compute number of frames in common between this and enclosing trace
StackTraceElement[] trace = getOurStackTrace();
int m = trace.length - 1;
int n = enclosingTrace.length - 1;
while (m >= 0 && n >=0 && trace[m].equals(enclosingTrace[n])) {
m--; n--;
}
int framesInCommon = trace.length - 1 - m;
// Print our stack trace
s.println(prefix + caption + this);
for (int i = 0; i <= m; i++)
s.println(prefix + "\tat " + trace[i]);
if (framesInCommon != 0)
s.println(prefix + "\t... " + framesInCommon + " more");
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION,
prefix +"\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, prefix, dejaVu);
}
}
}
|
printStackTrace에 대한 코드입니다.
중점있게 볼 것은 5번째 줄이며, PrintStream으로 System.err를 준 상태입니다.
따라서 printStackTrace는 출력값을 System.err.println()으로 출력한 것을 볼 수 있습니다.
getMessage()
public String getMessage() {
return detailMessage;
}
getMessage는 예외에 대한 메세지 값을 반환해주는 함수입니다.
따라서 위 함수를 통해 System.out.println()을 사용하므로 PrintStream으로 System.out을 사용합니다.
따라서 두 메서드의 차이는 System.err를 사용하느냐 System.out을 사용하느냐로 나눌 수 있습니다.
System.out vs System.err
화면에 출력하는 PrintStream차이로 출력의 순서에 차이가 생겼습니다.
출력 순서의 차이
PrintStream에서 출력을 하게 될때 buffer가 존재하여 print요청이 들어오면 바로 출력하는 것이 아니라 buffer에 쌓아두었다가 flush를 통해 버퍼를 지우면서 화면에 출력하게 됩니다. 이렇게 진행하는 이유는 매번 모니터란 출력 장치에 접근하여 메세지를 출력하게 되면 속도상 저하를 일으키게 되므로 위와 같은 방식으로 진행합니다.
'그럼 출력 요청의 순서대로 나와야 하는거 아닌가?' 라고 생각할 수 있지만, System.out과 System.err의 경우 자신만의 buffer가 존재합니다. 또한 System.err의 경우 예외상황에 대한 stream이기 때문에 발생하면 바로바로 출력을 해줘야 하지만 System.out은 그럴 필요가 없으니 서로 다른 타이밍에 buffer를 flush하게 됩니다.
여기까지 아직은 예외 테스트를 실패한 이유가 없습니다.
Test의 output 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public abstract class NsTest {
private PrintStream standardOut;
private OutputStream captor;
@BeforeEach
protected final void init() {
standardOut = System.out;
captor = new ByteArrayOutputStream();
System.setOut(new PrintStream(captor));
}
@AfterEach
protected final void printOutput() {
System.setOut(standardOut);
System.out.println(output());
}
protected final String output() {
return captor.toString().trim();
}
// ... 중략
}
|
테스트 실행전 ( BeforeEach )
- OutputStream의 참조 변수에 내부적으로 저장 공간을 만들어 사용할 수 있는 ByteArrayOutputSream클래스를 할당
- 표준 출력 stream을 ByteArrayOutputSream 클래스를 할당받은 outputStream사용
- 이때 System.out의 출력 스트림만 변경하였다.
테스트 진행
- 테스트를 진행하며 나오는 출력 결과 값이 나온다
- 기본 프로그램 설명 출력 : System.out을 사용하였으므로 ByteArray에 쌓인다
- 예외 출력 : System.err를 사용하였기 때문에 바로 출력이 되고 ByteArray에 쌓이지 않느다.
이렇게 되는 이유는 System.out과 System.err에 대한 buffer가 분리되어 있으며 System.out stream만 변경하였기 때문이다.
따라서 결과적으로 captor에 대한 값을 가져왔다는 것은 ByteArray의 내용물을 가져왔다는 것입니다. 그렇기에 테스트 결과 output에 대해서 원하는 '[ERROR]'가 없었던 것이었습니다.
테스트 실행 후 ( AfterEach )
- 다시 System.setOut()메서드를 통해 출력 stream을 System.out으로 변경하여 출력했습니다.