본문 바로가기

🎒 Activity/우아한 테크코스 프리코스

[우아한 테크코스] 프리코스 3주차 회고

📘 3주차 미션을 시작하면서

3주차 미션을 받은 순간... 추가 요구사항이 더욱 디테일해졌다는 것을 느꼈고, 할 수 있겠지라는 걱정부터 시작되기 시작했었다.
자바를 스스로 공부하기 시작하면서 기본적인 자료형, 문법 등에 대해서는 사용도 해보고 했지만 이번 미션에서는 enum(열거형)을 사용하라는 요구사항이 있었다. 2주차 미션을 하면서 '상수'를 적절하게 사용하면 가독성을 높일 수 있구나라는 것을 느꼈고, 다음 미션에는 꼭 사용해봐야지라는 생각을 하고 있었다. 우테코는 정말 신기한게 마치 내가 하고 있는 생각을 알고 있는 것처럼 새로운 미션에 귀신같이 '열거형'을 사용하라는 요구사항이 있었다. 😦

 

 

 

📖 열거형이 무엇인가?

열거형은 자바에 대해서 공부하면서 들어본적은 많았지만 실제로 코드 개발에서 사용해본적은 없다. 그래서 이번에 미션에서 사용하기 위해서는 혼자 학습을 해봐야겠다고 생각했다. '자바의 정석'이라는 책과 구글링을 통해서 열거형에 대해서 학습하고, 블로그에 정리해놓았다. 간단하게 말하면 '열거형'이란 비슷한 역할을 하는 상수들을 모아서 관리할 수 있도록 도와주는 역할을 한다. static final을 사용하지 않고 비슷한 상수는 모아서 사용한다면 훨씬 가독성을 높일 수 있을 거라고 생각했다.

 

 

 

📖 이번 미션에서 힘들었던 점

솔직히 말하면 다 힘들었다. 클래스 분리도 정말 어려웠고, 함수(메소드)의 길이를 15자 이하로 작성하라는 요구 사항을 의식하면서 코드 작업을 했다. 모든 코드를 작성할 때, 자연스럽게 평소보다 많은 생각을 하게되었다. 그리고 처음에 코드를 작성하면서 이건 엄청난 리펙토링 작업이 필요하겠다는 것을 느끼면서 작업했다. 그 중에서 가장 많이 시간을 쏟았던 삽질을 적어보려고 한다. 한풀이 느낌으로 글을 작성할 것이기 때문에 길어질 수 있다는 것을...ㅠ

 

이번 로또 문제 중에 이런 요구 사항이 있었다.

사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.

예를 들어 처음에 로또 구입 금액을 입력 받는데, 이때 입력 값이 “1000j”라고 해보자.

  • “1000j” 는 ‘j’라는 문자가 포함되어 있기 때문에 금액이라고 할 수 없다.
  • 잘못된 값을 입력했기 때문에 IllegalArgumentException을 발생한다.
try {
        Integer.valueOf("1000j");
} catch (Exception e) {
        System.out.println("[ERROR] 숫자가 아닌 값을 입력했습니다.");
        throw new IllegalArgumentException();
}

처음에는 이렇게 코드를 작성했다.

내가 생각했던 프로그램 진행 방향은 다음과 같다.

  1. “1000j”가 입력 값으로 들어옴.
  2. Interger.valueOf(”1000j”)에서 NumberFormatException이 발생
  3. catch 블럭으로 Exception을 잡아내고, IllegalArgumentException을 발생 시킴.
  4. 프로그램이 종료됨.

생각한대로라면 사용자가 잘못된 값을 입력했고, IllegalArgumentException을 발생했고, “[ERROR]”로 시작하는 에러 메시지도 출력했고, throw new를 통해서 프로그램도 종료되었다.

하지만 ApplicationTest에서 예외_테스트()가 자꾸 실패했다.

@Test
void 예외_테스트() {
    assertSimpleTest(() -> {
        runException("1000j");
        assertThat(output()).contains(ERROR_MESSAGE);
    });
}

왜 실패하지…?;; 이때 부터 삽이 다 닳아서 나무 막대기만 남을 정도로 삽질을 시작했다.

  1. 첫 번째 삽질
    • runException()에 뭔가가 숨어있나보다. 찾아보러 가자.
    • 그렇게 처음으로 woowacourse-projects에 있는 NsTest.java 파일에 가봤다.
    • protected final void runException(final String... args) { try { run(args); } catch (final NoSuchElementException ignore) { } }
    • 코드를 보니 뭔가 저 catch (final NoSuchElementException ignore)이 문제일 거 같다는 생각이 들었다.
    • 그래서 내가 작성한 코드에서 NoSuchElementException을 throw 해보기로 했다.
    • try { Integer.valueOf("1000j"); } catch (Exception e) { System.out.println("[ERROR] 숫자가 아닌 값을 입력했습니다."); throw new NoSuchElementException(); }
    • 이렇게 코드를 작성하고, 다시 ApplicationTest를 돌려봤다. 오…초록불이 들어왔다! 해결한건가?
    • 하지만 문제 요구 조건에서 잘못된 입력이 들어오면 IllegalAegumentException을 발생시키고 라는 요구 조건에 만족하지 못했다.
    • 그래서 혹시나 하는 마음에 미리 만들어놓았던 IllegalArgumentException 발생 여부를 테스트하는 코드를 돌려봤다.
    • @DisplayName("입력된 금액 예외 발생 테스트") @ParameterizedTest @ValueSource(strings = {"3500", "4650", "9080", "10", "100", "1000j"}) void validatePurchaseAmountTest(String money) { assertThatThrownBy(() -> InputException.validatePurchaseAmount(money)) .isInstanceOf(IllegalArgumentException.class); }
    • 역시나 “1000j” 부분에서 빨간불이 들어왔다. ApplicationTest만 통과하면 되는거 아닌가? 라는 생각이 순식간에 지나갔지만 절대…용납할 수 없다. 이런 찝찝한 문제가 발생하면 절대…잠을 잘 수가 없다.
  2. 두 번째 삽질
    • 첫 번째 삽질이 끝나고 ApplicationTest가 통과하려면 일단 NoSuchElementException이 발생되어야 한다는 결론을 무작정 내버렸다.
    • NoSuchElementException 은 발생되어야 하고, IllegalArgumentException도 발생되어야 하고 이게 무슨 혼돈의 카오스인가….
    • “두 개의 예외를 동시에 발생시킬 수 있는가?” 부터 시작해서 정말 모든 키워드는 다 넣어서 구글링을 해봤다.
    • 하지만 어떠한 방법도 ApplicationTest와 내가 만든 테스트가 동시에 통과하는 방법도 없었다.
  3. 결론에 도달
    • 검정색 화면에 있는 코드만 계속 보고 있으니 점점 눈이 흐려질 때쯤 문제를 다시 한번 읽어봐야겠다라는 생각이 들어서 README.md 파일을 다시 천천히 읽어봤다.
    • 잘못된 입력 → IllegalArgumentException 발생 → [ERROR] 출력 → 프로그램 종료….?
    • 갑자기 문득 main 함수가 종료돼도 프로그램이 종료되는 거 아닌가…? 라는 생각이 들었고, 코드를 작성해봤다. 
    • // 프로그램 실행 코드 public void playLottoProgram() { try { savePurchaseAmount(); saveLottoIssueCount(); issueLottoSeveralTimes(lottoIssueCount, myLotto); OutputView.printLottoPurchaseInformation(myLotto, lottoIssueCount); saveWinningNumber(); saveBonusNumber(); setResult(myLotto, winningNumber, bonusNumber); Calculator calculator = new Calculator(); calculator.calculateTotalEarnings(result); calculator.calculateEarningsRate(purchaseAmount); OutputView.printLottoResult(result, calculator.getEarningsRate()); } catch (IllegalArgumentException exception) {} }
    • // 예외 처리 코드 try { Integer.valueOf("1000j"); } catch (Exception e) { System.out.println("[ERROR] 숫자가 아닌 값을 입력했습니다."); throw new IllegalArgumentException(); }
    • 프로그램 실행 코드 전체를 try-catch로 감싸서 IllegalArgumentException이 발생되면 프로그램이 실행이 멈추도록 해봤다.
    • 와…우…. ApplicationTest와 내가 만든 테스트 코드에서 모두 초록색 불이 들어왔다.
    • 정말 간단했던 해결방법인데, 거의 6시간 정도를 빙빙 돌았다. 허무하면서도 이제 잘 수 있다는 안도감에 너무 좋았다. (지금 시간은 오전 2시 24분)
    • 결론은 문제를 잘못 이해했고, woowacourse-projects를 의심했던 벌을 받았다고 생각한다. 틀려도 내가 틀렸지 우테코가 틀렸을리가…없지 역시 😂

 

 

 

📖 다음 미션을 시작하기 전

다음 미션을 받기 전에 이번 미션 회고록을 작성하면서 공통 피드백을 보며 필요한 부분은 학습하도록 한다. 이번 공통 피드백 내용은 다음과 같습니다. (필요한 부분만 가져왔습니다.)

 

💡 비즈니스 로직과 UI로직을 철저히 분리해라.

비즈니스 로직과 UI 로직이 한 개의 클래스에 있는 것은 단일 책임(SRP)에 위배된다.

View에서 사용해야되는 데이터라면 getter를 만들어서 전달하고, 현재 객체의 상태를 로그에 나타내기 위함이 강하다면 toString()을 사용해서 정의한다.

 

💡 연관있는 상수의 경우는 static final 보다는 enum을 사용한다.

 

💡 final 키워드를 사용해서 값의 변경을 막는다.

최근에 등장하는 프로그래밍 언어들의 기본은 ‘불변’이다. 자바에서는 불변을 위해서 final 키워드를 사용할 수 있다.

final은 상수 선언할 때만 사용하는 줄 알고 있었다. 필드(인스턴스 변수)의 불변을 위해서도 사용할 수 있다는 것을 알아두자.

 

💡 객체의 상태 접근을 제한한다.

인스턴스 변수의 접근 제어자는 private를 사용한다. 객체의 캡슐화를 하기 위한 기본적인 설정.

 

💡 객체는 객체스럽게 사용한다.

객체 내부에 로직을 구현한다. 일반적으로 getter()를 많이 사용하는 경향이 있다. 하지만 객체의 모든 인스턴스 변수를 보내줘야 하는데, 이 모든걸 getter()로 구현한다면 과연 가독성이 좋아질까?

그렇기 때문에 View에서 단순 출력을 하기 위한 데이터 정도는 getter()로 전해줄 수 있지만, 그 이외의 경우에는 getter로 데이터를 보내서 로직을 처리하기 보다는 데이터를 객체로 받아와서 로직을 돌린 후 결과를 날려주는 형식으로 구현하는 것이 좋다.

getter를 사용하는 대신 객체에 메시지를 보내자

 

💡 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다.

객체에 인스턴스 변수가 많을 수록 객체의 복잡도를 높이고, 버그 발생의 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 있지는 않은지 확인해볼 필요가 있다.

 

💡 성공하는 테스트 뿐만 아니라 예외에 대한 케이스도 테스트 해야 한다.

 

💡 테스트 코드도 코드이다.

즉, 테스트 코드도 중복되는 부분이 존재한다면 줄이는 것이 바람직하다. 예를 들어 단순히 값이 바뀌는 경우라면 @ParameterizedTest를 사용해서 테스트할 수 있다.

 

💡 테스트를 위한 코드는 구현 코드에서 분리되어야 한다.

테스트를 통과하기 위해 구현 코드에 테스트를 위한 코드를 작성해서는 안된다.

  1. 테스트를 위해서 접근 제어자를 바꾸는 경우
  2. 테스트 코드에서만 사용되는 메서드

 

💡 단위 테스트하기 어려운 코드를 단위 테스트 하기

메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기

 

💡 private 함수를 테스트하고 싶다면 클래스(객체) 분리를 고려한다.

가독성을 높이기 위해서 private 함수를 구현한 경우 public으로도 검증 가능하다고 여겨질 수가 있다. 왜냐하면 public 함수가 private 함수를 사용하기 있기 때문에 자연스럽게 테스트 범위에 포함이 된다.

하지만 가독성 이상의 역할을 하는 경우에는 테스트를 쉽게 구현하기 위해서 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닌지 고민해봐야한다.

다음 단계를 진행할 때는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.

 

 

 

🔥 다음 미션을 시작하기 전 마음가짐

아직 문제를 자세하게 읽어보진 않았지만, 슬랙 채널에서 다른 분들이 말하는 걸 보면 오징어 게임에 나오는 '다리 건너기' 게임을 구현하는 미션인 것 같다. 최근에 정말 많은 일들이 한번에 몰려와서 체력적으로 딸리는 것이 느껴진다... 그래도 지금까지 해온 것이 있으니깐 포기하지 않고 마무리 잘하고 싶다.