우아한테크코스 - 프리코스 2주차

주노·2022년 11월 4일
11
post-thumbnail

서론

1주차 온보딩 미션을 통해 어느정도 프로젝트 작성 및 제출방식에 익숙해졌다고 생각한다.

2주차가 시작됨과 동시에 github discussion도 열렸다.

슬랙 채널로 운영되고있던 주간 회고록 채널도 새로운 카테고리로 개설되었다.

2주차의 시작은 피어리뷰와 다른 사람들의 회고록을 읽어보는 시간과 함께했다.
1주차 온보딩 미션을 진행하면서 내가 미처 신경쓰지 못한 부분을 다른 사람의 고민으로부터 배울 수 있었다.

수요일, 목요일 약 2일가량 discussion을 통해 지난 주차를 되돌아보는 시간을 가지고 2주차 미션을 진행하기 시작했다.

🚀 미션 - 사전 확인

1주차와 비교했을 때 고려해야할 내용이 많아졌다.

🎯 프로그래밍 요구 사항

  • JDK 11 버전에서 실행 가능해야 한다. JDK 11에서 정상적으로 동작하지 않을 경우 0점 처리한다.
  • 프로그램 실행의 시작점은 Applicationmain()이다.
  • build.gradle 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
  • Java 코드 컨벤션 가이드를 준수하며 프로그래밍한다.
  • 프로그램 종료 시 System.exit()를 호출하지 않는다.
  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.

Java 코드 컨벤션 가이드가 추가되었다.

추가된 요구 사항

Convention

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
  • 3항 연산자를 쓰지 않는다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
    • 테스트 도구 사용법이 익숙하지 않다면 test/java/study를 참고하여 학습한 후 테스트를 구현한다.

최근 읽고있는 클린코드의 내용 중 일부를 언급하고있다.
추가로 테스트를 구현한다는 내용도 추가되었다.

라이브러리

  • camp.nextstep.edu.missionutils에서 제공하는 RandomsConsole API를 사용하여 구현해야 한다.
    • Random 값 추출은 camp.nextstep.edu.missionutils.RandomspickNumberInRange()를 활용한다.
    • 사용자가 입력하는 값은 camp.nextstep.edu.missionutils.ConsolereadLine()을 활용한다.

제공된 API를 사용하여 구현해야한다.
친절하게 사용 예시까지 알려주었다.😀

Git

  • 미션은 java-baseball 저장소를 Fork & Clone해 시작한다.
  • 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리해 추가한다.
  • Git의 커밋 단위는 앞 단계에서 docs/README.md에 정리한 기능 목록 단위로 추가한다.
  • 과제 진행 및 제출 방법은 프리코스 과제 제출 문서를 참고한다.

1주차에서 다들 고민하던 커밋 메시지 컨벤션, 기능 목록을 정리할 문서도 지정해줬다.
이를 통해 구현하기 전 생각해보는것을 한번 더 강조하는 것 같다.

예외처리

우아한테크코스 5기 프리코스 코치와 수다 타임에서 이야기한 내용 중 실제 사용자의 요구사항은 이렇게 친절하지 않다는 언급이 있었다.
상대방이 어떤 미상의 동작을 수행할지 예측하고 이로 인해 일어날 문제를 사전에 방지할 것을 차근차근 쌓아 올려야한다.

🚀 미션 - 기능목록 정리

구현에 손대기 전에 요구사항을 먼저 분석해보자.

중,고등학생 때 학교에서 친구들과 심심할때마다 하던 게임 중 하나인 숫자야구게임 구현을 목표로한다.

요구사항 살펴보기

java-baseball 레포지토리 바로가기

  • 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
    • 예) 상대방(컴퓨터)의 수가 425일 때
      • 123을 제시한 경우 : 1스트라이크
      • 456을 제시한 경우 : 1볼 1스트라이크
      • 789를 제시한 경우 : 낫싱
  • 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 서로 다른 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
  • 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
  • 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.

간략하게 정리하면 다음과 같다.

  • 컴퓨터가 서로다른 임의의 수 3개를 선택해서 준다.
  • 이 값을 이용해 야구게임의 룰을 이행하는 프로그램을 작성하라.
  • 컴퓨터가 선택한 3개의 수를 모두 맞히면 게임이 종료된다.
  • 종료 후 게임을 다시시작 하거나 완전히 종료할 수 있다.
  • 잘못된 값은 IllegalArgumentException으로 반환.

예외사항 생각해보기

  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.

위 항목에 대한 예외사항도 충분히 생각해봐야겠다.

사용자가 잘못된 값을 입력할 경우를 생각해보자.

사용자가 입력한 값에 대한 예외

  • 숫자가 아닌 값을 입력할 때
  • 글자수가 맞지 않을 때
  • 중복된 수가 존재할 때

위와 동일하게 컴퓨터가 생성한 랜덤한 값도 예외처리를 해주면 좋겠다.

🚀 미션 - 기능목록 작성

아래 기능목록이 나올때까지 많은 착오과정이 있었지만 일단 최종본만 보도록하자

  • 숫자야구게임을 진행하는 객체 - BullsAndCows
    • 컴퓨터의 랜덤한 값(정답)을 생성한다.
    • 점수를 구한다.
    • 결과를 반환한다.
      • 볼, 스트라이크, 낫싱
    • 프로그램 종료조건을 검증한다.
      • 재시작
      • 종료
  • 숫자야구게임 결과 값 - ResultMessage
      • STRIKE("스트라이크")
      • BALL("볼")
      • NOTHING("낫싱")
    • 숫자를 넣었을 때 결과 문구로 반환해주는 기능
      • of(int number)
        • ex) BALL.of(1) return "1볼"
        • ex) STRIKE.of(2) return "2스트라이크"
        • ex) BALL.of(0) return ""

삽질과정을 보고싶다면 커밋내역의 docs 부분을 보면 된다... 안보는걸 추천한다

🚀 미션(?) - 테스트 통과하기

기능 목록대로 함수를 쭉 작성하다보니 문득 테스트에 대한 생각이 났다.
테스트가 어떤 과정을 통해 구동되는지를 알고 시작해야한다.

⚠️ 주의 ⚠️
이 앞은 생각의 흐름대로 지식의 층이 꼬리물기로 이어집니다.
최대한 정리해보려고 노력했지만 정신없을 수 있습니다.
가볍게 보고 가실분은 #과정정리 부터 봐주시면 감사하겠습니다.
1F, 2F, 3F ... 형태로 꼬리물기의 depth를 표현했습니다.
참고바랍니다.

[1F] 테스트.. 어떻게 적용하는거지?

우리는 랜덤한 값을 다루는 기능에 대해서 테스트를 진행해야한다.
제시된 테스트케이스를 참고하며 이해를 시도해보자.

@Test
void 게임종료_후_재시작() {
    assertRandomNumberInRangeTest(
            () -> {
                run("246", "135", "1", "597", "589", "2");
                assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
            },
            1, 3, 5, 5, 8, 9
    );
}

랜덤한 값을 받아오는데 246, 135 ... 과 같은 고정된 입력값이 의미가 있을까 싶다,,,

추가로 아래 저 1, 3, 5, 5, 8, 9는 뭐지?

[2F] 한층 내려가기

테스트 내부로 한층 들어왔다.

public static void assertRandomNumberInRangeTest(
        final Executable executable,
        final Integer value,
        final Integer... values
) {
    assertRandomTest(
        () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
      	executable,
       	value,
       	values
    );
}

[3F] Executable이 뭔데?

인터페이스에 작성되어있는 Javadocs 혹은 참고문서, 참고 블로그를 확인하여 알 수 있다.

Executable은 JUnit5에 정의되어 있는 함수형 인터페이스이다.
Runnable을 예외를 던질 수 있도록 재정의한 클래스라고 생각하면 된다.

[4F] 함수형 인터페이스?

함수형 인터페이스(Functional interface)는 1개의 추상 메소드를 갖고 있는 인터페이스를 말한다.
Single Abstract Method(SAM)라고 불리기도 한다.

람다식은 함수형 인터페이스로만 접근이 되기 때문에 함수형 인터페이스를 사용한다.

참고 블로그

[2F] 테스트 다시 살펴보기

public static void assertRandomNumberInRangeTest(
        final Executable executable,
        final Integer value,
        final Integer... values
) {
    assertRandomTest(
        () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
      	executable,
       	value,
       	values
    );
}

자 이제 함수를 실행하되 예외를 던질 수 있게 하도록 Executable을 사용했단 것을 알았다.

[3F] 한층 내려가기

private static <T> void assertRandomTest(
        final Verification verification,
        final Executable executable,
        final T value,
        final T... values
) {
    assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
        try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
            mock.when(verification)
            .thenReturn(value, Arrays.stream(values).toArray());
            executable.execute();
        }
    });
}
  • 특정 메소드를 호출했는지에 대해서 검증하기 위해 verification을 사용한다.
    참고 블로그

  • asssertTimeoutPreemptively 는 해당 시간내로 테스트가 수행되는지 테스트하는 내용으로 짐작할 수 있겠다.

내가 집중하는 부분은 그 안에있는 MockedStatic 이놈이다.
MockedStatic이라는 녀석이 Randoms를 감싸고 뭔가를 하고있다.
한번 알아보자.

[3.5F] MockStatic

이전에 정리했던 Mockito에 대한 내용을 리마인드하며 다시 살펴보자.

Mockito.mockstatic method의 설명을 보면 다음과 같다.

Creates a thread-local mock controller for all static methods of the given class or interface.
(번역) 지정된 클래스 또는 인터페이스의 모든 정적 메서드에 대한 스레드 로컬 모의 컨트롤러를 만듭니다.

즉, MockStatic은 Static method를 mocking 하기 위해 사용한다는 것을 알 수 있다.

// Randoms.class 내부에 존재하는 static method에 대해서 Mocking한다!
try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
	
    // Randoms 클래스 내부에 있는 함수를 사용했을 때
	mock.when(verification)
    // 그러면 메소드를 호출하되 1,3,5,5,8,9 라는 가짜 값을 이용할거다!
    .thenReturn(value, Arrays.stream(values).toArray());
    
    // main 함수를 실행한다.
	executable.execute();
}

주어진 사용 예시를 참고하면 1,3,5,5,8,9135, 589라는 랜덤 값을 임의로를 의미한다는 것을 알 수 있다.

과정 정리

그래서 이 테스트가 의미하는 바가 무엇인지 정리해보면 다음과 같다.

@Test
void 게임종료_후_재시작() {
    assertRandomNumberInRangeTest(
            () -> {
            // 맨 아래층의 executeable.execute(); 가 이 부분을 실행한다.
            // 이 과정에서 Exception도 잡는다.
                run("246", "135", "1", "597", "589", "2");
                assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
            },
            // 1, 3, 5, 5, 8, 9
            // 다시말해, "135", "589"가 나왔다 치고~ 테스트 해보자고~
            1, 3, 5, 5, 8, 9
    );
}

일단은 테스트를 하는 방법을 알았으니 기분좋게 통과표시 한번 띄워주고~
다시 시작해보자

🚀 미션 - 구현

구현단계를 모두 이야기하자면 포스팅이 너무 길어지니 구조가 궁금하다면 문서를 참고하면 좋겠다.
문서 보러가기!

테스트를 통과할 때 나오는 저 통과표시가 주는 안도감과 만족감이 엄청나다..!

🛠 리팩토링

함수를 쪼개는것에만 집중하다보니 제시한 프로그래밍 요구사항 중 지키지 못한 부분이 생겼다.

프로그래밍 요구사항 중

  • Git의 커밋 단위는 앞 단계에서 docs/README.md에 정리한 기능 목록 단위로 추가한다.
  • JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

위 두 부분에 있어 프로그래밍 요구사항을 만족하지 못했다.

구현 함수를 모두 하나의 객체에 private 함수로 구현한 바람에 Reflection을 이용한 방법을 사용하지 않으면 테스트를 진행하기가 어려웠다.

어떻게 분리를 해야할지 도무지 감이오지않아 일단 덮어두고 지난 주부터 읽기 시작한 클린코드를 읽었다.
5장 형식맞추기를 읽고 세로밀집도를 신경쓰며 함수를 나열했다.

기능별로 묶어놓고 나열해보니 몇가지 큰 분류로 나눌 수 있을것 같다는 생각을 할 수 있었고, 다음과 같이 나눌 수 있었다.

저 구조가 생각 난 당시에는 신나서 시간가는줄 모르고 새벽 2시 40분까지 리팩토링했다...😇

저렇게 구조를 나누고 나니 자연스레 테스트도 작성할 수 있었다.
자세한 내용은 PR로 와서 봐주세요 🙇‍♂️

그 외에는 자잘한 네이밍 수정 및 테스트를 추가와 문서(README.md) 수정작업이 이루어졌다.

후기

머리를 싸매고 앓다가 아이디어가 번쩍하고 찾아오는 순간을 정말 오랜만에 느꼈다...

코난(미래소년코난 아님)이 추리할때 번뜩 떠올리는 효과가 참 어울리는 그런 상황이였다.

테스트도 실제로는 처음 작성해보고 적용해봤는데 알아가는 과정이 어려우면서도 재밌었다.

테스트를 통과할때마다 오는 쾌감도 생각보다 짜릿하다
그렇다고 테스트만 돌리진 말고...

내가 고쳐가며 만들어낸 결과물은 보면 객관적으로 보면 잘 짜여진 완벽한 구조는 아닐지도 모르지만 발전과정을 알고있는 나는 이 구조로부터 뿌듯함을 느낀다.

남은 2주도 화이팅이다!

profile
안녕하세요 😆

9개의 댓글

comment-user-thumbnail
2022년 11월 9일

와... 정말 애정을 가지고 임하신 게 느껴지네요! 잘 보고 갑니다!

1개의 답글
comment-user-thumbnail
2022년 11월 9일

신나서 리팩토링하는 부분 공감가고 재밌네요 ㅎㅎ 잘 읽고 갑니다!

1개의 답글
comment-user-thumbnail
2022년 11월 9일

잘 보고 갑니다 ㅎㅎ..

1개의 답글
comment-user-thumbnail
2022년 11월 13일

테스트 코드 내부로 들어가면서 살펴보는 게 재밌네요 ㅎㅎ 잘 보고 갑니다!!

1개의 답글