이번 3주차 과제에서 가장 큰 허들이자 가장 많은 고민을 던져준 부분은 아마 Application의 테스트 케이스를 통과하지 못하는 부분이었을 것이다. 해당 부분들을 다른 우테코 크루 분들의 논의 한 것을 기반으로 정리하였다.
실제 크루 분들의 말씀들
try catch 사용 목적이 예외 발생으로 인한 비정상 종료를 방지하는 것이라는 걸 알았고, 예외를 catch하고 메시지를 출력 후 정상 종료되는 것이 요구사항의 의도라는 결론을 내렸습니다. (물론 여기까지 오는데 수시간 소요..)그래서, 에러 메시지 출력 후 더이상 실행할 코드가 없으면 되겠다고 생각했고, main 메서드에 try catch 구문을 사용했더니 테스트가 동작 하더라고요 휴
notNumber를 확인하는 부분에서는 throw만 던지고, 나중에 Controller나 Application의 main과 같이 전체 흐름을 컨트롤 하는 단계에서, notNumber를 포함한 로직을 수행하는 과정 중에 throw가 생기면 catch로 잡아서 print 해주는 방식으로 해결하였습니다.
그럼 정리 스톼뜨!
2 주차까지의 예외 처리의 문장 설명:
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
3 주차까지의 예외 처리의 문장 설명:
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.
미묘한 차이가 느껴지는가?! -> 2 주차까지는 "되어야한다"라는 표현이, 3 주차까지는 "한다."라는 표현으로 종료를 표현했다. 이는 2 주차까지는 에러 메세지 표시 후 강제 종료가 된다는 말인 반면, 3 주차부터는 정상 종료를 하라는 말?!?!
사실 이 부분 설명이 모호하다는 질문이 많았는지, 추후에 재공지가 왔다.
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다. 위 요구 사항은 예외 발생 시 로그를 남기고, 프로그램이 종료되는 상황을 생각해 보세요.
즉, 우리는 이번 과제가 예외 발생 시 로그를 남긴 후, 프로그램이 끝까지 돌아간 후 "정상 종료"되어야 한다는 것을 의미한다.
그렇기에 우리가 기본적으로 아래와 같이 코드를 짜면 정상 종료를 하지 못하게 된다.
try {
inputPrice = Integer.parseInt(userInput);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("[ERROR] 숫자를 입력해야 합니다");
}
위 코드는 결국 에러를 잡기만 하고 처리는 하지 않는...... 그런 상황에 해당된다.
위의 코드의 경우 숫자가 아닌 수를 파싱하여 발생한 예외(NumberFormatException)는 잡았으나, catch절에서 다시 IllgalArgumentException을 던져주는 상황.
우리가 필요한 건 이 inputPrice를 호출하는 메소드에서 try-catch로 처리를 후 어플리케이션을 "정상 종료" 해야 한다.
try {
int price = inputPrice();
} catch (IllegalArgumentException exception) {
System.out.println(exception.getMessage());
return;
}
자, 그러면 본질적으로 2 주차까지는 큰 문제가 없다가 갑자기 3 주차에서 갑작스럽게 이렇게 에러를 달리 하게 된 이유는 무엇일까? 그 이유는 바로, 현업에서 try-catch를 쓰는 이유가 바로 log를 찍기 위함이다.
서비스 종료시 나오는 빨간 경고문들, 즉 아래의 이미지와 같은 빨간 글씨로 뜨는 상황은 정상 종료의 상황이 아니다. 이와 같은 에러 발생시 어플리케이션이 작동 중 갑작스럽게 종료가 된 것이므로 정상 종료의 형태가 아니다.

우리가 필요한 것은, 어플리케이션이 어떠한 상황에서도 종료가 되지 않는 것이고, 이를 위해선 에러가 발생했을 시에 그 내용을 log로 남기고 어플리케이션이 "정상 종료" 되었을 때 그 로그를 띄우는 것이다.
이를 위해, 방어적인 프로그래밍인 try-catch를 한다는 것이다. 현업에서는 ERORR말고 다른 메세지도 더 많이 띄우고!!
[추가 공부]
우테코 크루 분들 중 내가 많이 고민했던 부분을 잘 정리한 분이 계셨다. 그 분은 java코드였기에, kotlin에서도 분석을 하고자 글을 남긴다. 주된 흐름은 https://github.com/orgs/woowacourse-precourse/discussions/1250 분을 참고했습니다.
3주차 과제를 함에 있어서 다른 것보다 어려웠던 건 Lotto 테스트 케이스는 통과가 되지만 Application Test의 통과가 계속 안되는 것이었다. 바로 이 부분.....!
@Test
void 예외_테스트() {
assertSimpleTest(() -> {
runException("1000j");
assertThat(output()).contains(ERROR_MESSAGE);
});
}
분명 [ERROR]을 띄우는 IllegalArgumentException으로 했는데 도!대!체! 왜 안되는 것이었을까에 대해서 많은 고민이 있었다. 그래서 하나하나 뜯어보기로 결정!
1. assertSimpleTest()
2. runException()
3. assertThat()
4. output()
1. assertSimpleTest
public class Assertions {
private static final Duration SIMPLE_TEST_TIMEOUT = Duration.ofSeconds(1L);
private Assertions() {
}
public static void assertSimpleTest(final Executable executable) {
assertTimeoutPreemptively(SIMPLE_TEST_TIMEOUT, executable);
}
}
이 코드를 보면 assertSimpleTest의 경우 SIMPLE_TEST_TIMEOUT 시간만큼, 즉 1초 동안 코드를 실행하고 종료하는 함수인 것을 알 수 있다.
2. runException("1000j")
public abstract class NsTest {
protected final void runException(final String... args) {
try {
run(args);
} catch (final NoSuchElementException ignore) {
}
}
protected final void run(final String... args) {
command(args);
runMain();
}
private void command(final String... args) {
final byte[] buf = String.join("\n", args).getBytes();
System.setIn(new ByteArrayInputStream(buf));
}
protected abstract void runMain();
}
순서대로 보면, 우선 runException에서 try를 먼저 시도하게 된다. try 안에는 run(args)가 있고 run은 기본적으로 command 함수을 통해 받아진 String을 받게 된다. (String.join("\n", args).getBytes();의 형태로 byte[] buf에 넣어주게 되는데, 이는 커맨드 창에 인풋을 System.setIn()을 통해 받게된다).
정리하면 run("1000j")을 통해 command("1000j")에서 시스템 버퍼에 "1000j"를 주게되고, 추상 클래스인 runMain을 실행하게 된다.
3. assetThat().contains는 output()과 ERROR_MESSAGE를 CharSequence로 변환하여 output() 안에 ERROR_MESSAGE가 존재하는지 검사한다. contains(), output()을 자세히 알아보자.
// contains: CharSequence 인풋으로 받고 존재 확인 검사!
public SELF contains(CharSequence... values) {
strings.assertContains(info, actual, values);
return myself;
}
// output -> captor 클래스가 변수로 있음!
protected final String output() {
return captor.toString().trim();
}
public abstract class NsTest {
private PrintStream standardOut;
private OutputStream captor;
@BeforeEach // 아니 이건!! @BeforeEach : 본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이전에 수행됩니다.
protected final void init() {
standardOut = System.out;
captor = new ByteArrayOutputStream();
System.setOut(new PrintStream(captor));
}
@AfterEach // 아니 이건!! @AfterEach : 본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이후에 수행됩니다.
protected final void printOutput() {
System.setOut(standardOut);
System.out.println(output());
}
protected final String output() {
return captor.toString().trim();
}
}
더 분석이 필요한 부분들이 남았지만..... 우선 이 부분은 나중에 더 더해보는 걸로 하자......
'각종 프로그램 참여 > 우아한 테크코스, 우테코 이야기' 카테고리의 다른 글
| [3 주차 코드 회고] 천외천, 산 넘어 산 (0) | 2022.11.18 |
|---|---|
| MVC 체계에 대해 알아보자 (0) | 2022.11.17 |
| 코틀린 컨벤션 정리, 한글 번역 (0) | 2022.11.16 |
| [3 주차 학습 회고] 완벽주의에 대해서, 멘탈을 잡자 (0) | 2022.11.16 |
| [3 주차 중간 회고] 이제 진짜 현업 코딩하는 느낌......?!?! (2) | 2022.11.14 |