본문 바로가기
Memo/우테코 4기

[우테코] 프리코스 1주차 회고 - 숫자 야구 게임

by 연로그 2021. 12. 1.
반응형

드디어 도착한 1주차 미션!

 

깔끔하고 친절하게 진행 방식을 안내해주셨다.

우테코의 깃허브는 오픈되어있고 모든 사람들이 열람할 수 있으므로 따로 가리지 않았다.

 

과제를 진행하며 제일 갈팡질팡했던건 프로젝트 구조와 테스트 코드였다.

 

 프로젝트 구조가 텅 비어있으니 어떻게 구조를 짤지 한번에 생각하려니 머리가 멈춰버렸다. 😵 지금 생각해보니 당연하다... 경험 많은 사람도 프로젝트에 맞는 적절한 설계 짜는거 힘들어하는데 나같은 초보가 한번에 좋은 설계를 짜려하니 머리가 핑 돌았지. 코드는 한번에 짜는게 아니다. 차근차근 발전시키는 것임을 잊지 말자. 오히려 처음이었기에 처음 보는 규칙들을 기억하기 위해 갈팡질팡했고 두번째 과제때는 잘 해낼 수 있을거라 믿는다.👊

 

주의해야할 점

  • 코드를 한번에 짜려고 하지말자
  • 기능별로 commit 하기 (+commit log 남기기)
  • Pull Request와 플랫폼 과제 제출까지 마쳐야 제출 인정
  • 3항 연산자 금지
  • indent는 2까지 허용 (메소드 단위를 작게)
  • Java Coding Convention, Git commit message 준수

 

진행 순서

정해진 순서가 아니고 개인적으로 진행한 순서입니다

 

  1. 깃허브 사용 유의법, 코드 컨벤션 등 과제 진행 가이드 파악하기
  2. 게임 룰 파악하기
  3. 구현할 기능 정리
  4. 과제에 주어진 API 파악하기
  5. 코드 작성하기 (기능마다 commit)
  6. 과제 제출 피드백

 

1. 과제 진행 가이드 파악하기

Pull Request

 예전에 올린 Pull Request에 관한 포스트가 있으니 설명은 생략한다.

> https://yeonyeon.tistory.com/158

 

[GitHub] Pull Request와 코드리뷰

스터디를 진행하는데 pull request를 통해 내가 푸시한 내역(코드나 글)에 대한 리뷰 진행 및 질문답변을 한다. 하지만 깃허브 기능을 많이 이용해보지 못한 관계로... Pull Request가 뭔지, 왜 필요한지

yeonyeon.tistory.com

 

🔻 Java Code Convention

더보기

띄어쓰기나 괄호 같은건 내가 기존의 코드 작성하는 방법과 유사해 생략한다.
명명 규칙에 대해서만 정리했다.

 

클래스

  • 영문+숫자+언더스코어 구성
  • 대문자 카멜케이스
    (ex: camel_case X, camelCase X, CamelCase O)
  • 명사형
  • 테스트 클래스 이름은 *Test


변수

  • 영문+숫자+언더스코어 구성
  • 소문자 카멜케이스
    (ex: camel_case X, camelCase O, CamelCase X)
  • 임시 변수 외에는 1글자 이름 사용 X
  • 상수는 대문자+언더스코어 구성 (ex: MAX_NUMBER)

 

메소드

  • 영문+숫자+언더스코어 구성
  • 소문자 카멜케이스
    (ex: camel_case X, camelCase O, CamelCase X)
  • 동사/전치사로 시작

 

패키지

  • 소문자로 구성


인터페이스

  • 대문자 카멜케이스
    (ex: camel_case X, camelCase X, CamelCase O)
  • 명사, 형용사 이용


공통

  • 한국어 발음대로 표기 금지  
  • (ex: 사용자 -> Sayongja X, User O)
  • 대문자로 표기할 약어는 목록에 별도로 명시
    (ex: 대문자로 표기할 약어의 목록을 정의하지 않는 경우 : HttpApiUrl
        API만 대문자로 표기할 약어의 목록에 있을 경우 : HttpAPIUrl
        HTTP, API, URL이 대문자로 표기할 약어의 목록에 있을 경우 : HTTPAPIURL ) 

 

🔻 Commit Log 남기기

더보기

> https://gist.github.com/stephenparish/9941e89d80e2bc58a153

 

커밋 메시지는 기본적으로 아래 구조를 따른다.

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

 

type

  • feat (feature)
  • fix (bug fix)
  • docs (documentation)
  • style (formatting, missing semi colons, ...)
  • refactor
  • test (when adding missing tests)
  • chore (maintain)

 

scope

  • 커밋 변경 위치를 지정하는 것
  • ex: $location, $browser, $compile, $rootScope, ngHref, ngHref, ngClick, ...

 

subject; 제목

  • 변경 사항에 대한 간결한 설명
  • 현재 시제의 명령어 사용
    (ex: changed X, changes X, change O)
  • 첫 글자 대문자 금지
  • 끝에 (.) 금지

 

body

  • 변화에 대한 동기 및 이전과 비교
  • 현재 시제의 명령어 사용

 

footer

  • breaking change 기입 (breaking change란?)
  • breaking change에 대한 설명, 정당성, 마이그레이션 참고글

 

커밋 메시지 예제들

feat($browser): onUrlChange event (popstate/hashchange/polling)

Added new event to $browser:
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available

Breaks $browser.onHashChange, which was removed (use onUrlChange instead)
fix($compile): couple of unit tests for IE9

Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #392
Breaks foo.bar api, foo.baz should be used instead
style($location): add couple of missing semi colons
docs(guide): updated fixed docs from Google Docs

Couple of typos fixed:
- indentation
- batchLogbatchLog -> batchLog
- start periodic checking
- missing brace

 

2. 게임 룰 파악하기

난 야구를 전혀 몰라서 '숫자 야구 게임'이라는 키워드로 검색을 해보았다.

하지만 이게 무슨 일인가... 과제 리드미만 읽어도 충분히 이해할 정도로 쉬워서 검색한게 민망할 정도였다.

 

🔻 간단한 규칙 설명

더보기
  1. 플레이어 A, B가 각자 숫자 3개를 고른다.
  2. B의 숫자 중, A의 숫자와 위치가 같다면 스트라이크
    B의 숫자 중, A의 숫자만 같고 위치가 다르면 볼
    B의 숫자 중 A의 숫자 어느 것도 같지 않다면 낫싱
  3. B가 A의 숫자를 전부 맞출때까지 반복한다.

 

3. 구현할 기능 정리

  • 숫자 랜덤 생성
      - 3자리 자연수
      - 1부터 9까지의 숫자로 구성
      - 각 숫자는 중복 불가능

  • 사용자 입력 
      - (e) 1부터 9까지 숫자로 구성
      - (e) 입력 값은 3자리
      - (e) 0 이하의 정수 불가능
      - (e) 각 숫자는 중복 불가능

  • 사용자 입력 정답 검사
      - 스트라이크: 같은 위치, 같은 숫자인 경우 +1
      - 볼: 다른 위치, 같은 숫자인 경우 +1
      - 낫싱: 같은 숫자가 0개인 경우

  • 정답 검사 결과 출력
      - 볼, 스트라이크 둘 다 존재하는 경우 볼 -> 스트라이크 순으로 출력
      - 정답 시 종료 문구 출력
      - 오답 시 '2. 사용자 입력' 반복

  • 종료 조건 검사
      - 1 입력 시 재시작
      - 2 입력 시 종료
      - (e) 그 외 문자 입력 시 에러 발생 후 종료

 

 

4. 과제에 주어진 API 파악하기

과제에 총 2가지 API가 주어졌다.

  • Scanner API 대신 camp.nextstep.edu.missionutils.Console
  • Random API 대신 camp.nextstep.edu.missionutils.Randoms

 

사실 이 메소드를 이용하면 됩니다~라고 사용해야하는 메소드가 리드미에 쓰여있어서 굳이 분석할 필요는 없다.

근데 왜 우테코용 API를 따로 만들었을까? 이전 기수에는 없었던 것 같은데 기존 API랑 어떤 차이일까? 라는 궁금증이 들어서 그냥 한번 분석해보았다.

 

🔻 Console

더보기

readLine()

  • 사용자 입력을 받기 위해 이용
  • 이때 getInstance()를 이용해 전역변수로 선언한 Scanner가 null이거나 닫혀 있으면 새로운 스캐너를 생성

 

isClosed()

  • 이를 이해하기 위해서는 일단 Java Reflection 기능에 대해 알아야 함
  • getDeclaredField(String name)
    : name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 반환
  • sourceClosed
    : Scanner 클래스에서 해당 자원이 종료되었는지 저장된 boolean 변수
  • setAccessible()
    : 필드나 메소드의 접근 제어 지시자에 의한 제어 변경
    위의 경우, sourceClosed 변수가 private이기 때문에 접근 불가. 해당 메소드 통해 접근 가능하게 설정
  • 최종적으로 scanner 객체에 존재하는 sourceClosed 변수의 값을 return

 

소스 코드에 대한 이해는 마쳤으나 역시나... 왜 굳이 해당 API를 이용해야하는지는 잘 모르겠다.

Scanner에는 다양한 메소드가 존재하니 이를 억제하기 위함일까?

Scanner를 .close() 해주는 작업이 생략되어 있는데 이에 대해서는 처리를 안하는걸까?

(+ Scanner가 close()를 해야하는 이유 )

 

풀 코드

public class Console {
    private static Scanner scanner;

    private Console() {
    }

    public static String readLine() {
        return getInstance().nextLine();
    }

    private static Scanner getInstance() {
        if (Objects.isNull(scanner) || isClosed()) {
            scanner = new Scanner(System.in);
        }
        return scanner;
    }

    private static boolean isClosed() {
        try {
            final Field sourceClosedField = Scanner.class.getDeclaredField("sourceClosed");
            sourceClosedField.setAccessible(true);
            return sourceClosedField.getBoolean(scanner);
        } catch (final Exception e) {
            System.out.println("unable to determine if the scanner is closed.");
        }
        return true;
    }
}

 

 🔻 Randoms

더보기

pickNumberInList(List<Integer> numbers)

  • validateNumbers()를 통해 List가 비어있지 않은지 검증
  • return 파라미터로 주어지는 List 내의 랜덤한 숫자

 

pickNumberInRange(int startInclusive, int endInclusive)

  • validateRange()를 통해 startInclusive, endInclusive가 사용 가능한 범위인지 검증
  • return startInclusive 부터 endInclusive 사이의 랜덤한 숫자

 

pickUniqueNumbersInRange(int startInclusive, int endInclusive, int count)

  • validateRange()를 통해 startInclusive, endInclusive가 사용 가능한 범위인지 검증
  • validateCount()를 통해 유효한 count 값인지 검증
  • startInclusive 부터 endInclusive 사이의 모든 숫자를 List에 추가
  • shuffle()을 통해 List에 저장된 값을 섞고 subList()를 통해 0부터 count까지의 List 반환

 

요 API는 확실히 제공해주셔서 훨씬 편할 것 같다!

과제에서는 pickNumberInRange() 메소드를 이용하라고 명시되어 있지만 pickUniqueNumbersInRange() 메소드를 이용하는 편이 더 간편할텐데 아쉽다.

본래라면 pickNumberInRange()를 Random을 이용해 직접 구현했어야 할텐데 이 일이라도 덜어져서 다행이다.

위 메소드를 이용해 내 방식대로 pickUniqueNumbersInRange() 메소드를 만들어도 재밌을 것 같다.

 

풀 코드

public class Randoms {
    private static final Random defaultRandom = ThreadLocalRandom.current();

    private Randoms() {
    }

    public static int pickNumberInList(final List<Integer> numbers) {
        validateNumbers(numbers);
        return numbers.get(pickNumberInRange(0, numbers.size() - 1));
    }

    public static int pickNumberInRange(final int startInclusive, final int endInclusive) {
        validateRange(startInclusive, endInclusive);
        return startInclusive + defaultRandom.nextInt(endInclusive - startInclusive + 1);
    }

    public static List<Integer> pickUniqueNumbersInRange(
        final int startInclusive,
        final int endInclusive,
        final int count
    ) {
        validateRange(startInclusive, endInclusive);
        validateCount(startInclusive, endInclusive, count);
        final List<Integer> numbers = new ArrayList<>();
        for (int i = startInclusive; i <= endInclusive; i++) {
            numbers.add(i);
        }
        return shuffle(numbers).subList(0, count);
    }

    public static <T> List<T> shuffle(final List<T> list) {
        final List<T> result = new ArrayList<>(list);
        Collections.shuffle(result);
        return result;
    }

    private static void validateNumbers(final List<Integer> numbers) {
        if (numbers.isEmpty()) {
            throw new IllegalArgumentException("numbers cannot be empty.");
        }
    }

    private static void validateRange(final int startInclusive, final int endInclusive) {
        if (startInclusive > endInclusive) {
            throw new IllegalArgumentException("startInclusive cannot be greater than endInclusive.");
        }
        if (endInclusive == Integer.MAX_VALUE) {
            throw new IllegalArgumentException("endInclusive cannot be greater than Integer.MAX_VALUE.");
        }
        if (endInclusive - startInclusive >= Integer.MAX_VALUE) {
            throw new IllegalArgumentException("the input range is too large.");
        }
    }

    private static void validateCount(final int startInclusive, final int endInclusive, final int count) {
        if (count < 0) {
            throw new IllegalArgumentException("count cannot be less than zero.");
        }
        if (endInclusive - startInclusive + 1 < count) {
            throw new IllegalArgumentException("count cannot be greater than the input range.");
        }
    }
}

 

🔻 Assertions

더보기

assertRandomNumberInRangeTest()

  • Executable: Method와 Constructor의 공통 기능을 공유하기 위한 super class 
  • `...`를 이용해 Integer형 파라미터를 여러개 받기
  • anyInt(): 입력되는 인자와 무관한 특정 값을 리턴하고 싶을 때 사용
  • assertRandomTest() 호출

 

assertRandomTest()

  • RANDOM_TEST_TIMEOUT초 내에 완료되지 않으면 타임아웃 에러
  • MockedStatic: static 함수를 가짜로 사용하기 위한 Mock에서 제공하는 기능
  • when(Verification)이 사용된다면 thenReturn(파라미터로 넣었던 value들)
  • assertTimeoutPreemptively: executable이 주어진 시간 안에 실행이 완료되는지 확인

 

assertSimpleTest()

  • executable이 주어진 시간 안에 실행이 완료되는지 확인

 

public class Assertions {
    private static final Duration SIMPLE_TEST_TIMEOUT = Duration.ofSeconds(1L);
    private static final Duration RANDOM_TEST_TIMEOUT = Duration.ofSeconds(10L);

    private Assertions() {
    }

    public static void assertSimpleTest(final Executable executable) {
        assertTimeoutPreemptively(SIMPLE_TEST_TIMEOUT, executable);
    }

	...
    
    public static void assertRandomUniqueNumbersInRangeTest(
        final Executable executable,
        final List<Integer> value,
        final List<Integer>... values
    ) {
        assertRandomTest(
            () -> Randoms.pickUniqueNumbersInRange(anyInt(), anyInt(), anyInt()),
            executable,
            value,
            values
        );
    }
    
    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();
            }
        });
    }
}

assertRandomNumberInRangeTest

 

5. 코드 작성하기

> https://github.com/yeon-06/java-baseball-precourse

 

GitHub - yeon-06/java-baseball-precourse: 숫자 야구게임 미션을 진행하는 저장소

숫자 야구게임 미션을 진행하는 저장소. Contribute to yeon-06/java-baseball-precourse development by creating an account on GitHub.

github.com

 

 

6. 과제 제출

  • Git Hub에 Pull Request
  • 우테코 플랫폼에 과제 소감 제출(1000자)

 

가벼운 미션이라는 공지를 보고 편하게 있다가 스스로의 부족함을 깨닫는.. 시간이었다.

아는 것보다 배울 것이 더 많다는 점을 잊지 말자! 🐱‍👓

 

이번 미션을 통해 크게 깨달은 점 3가지.

  1. 자만하지 않기
  2. 리팩토링 하는 코드
  3. 테스트 코드 작성의 중요성

 

다음 미션 전까지 셀프 과제

  1. TDD란?
  2. Mockito란?

 

피드백 내용

참가자가 많아서 그런가 1:1 피드백은 없었지만 공통 피드백을 정리해서 보내주셨다.

(백엔드 쪽 풀리퀘만 160개가 넘는데 파악하기 힘들만도...)

 

  1. 이름을 통해 의도를 드러내기
  2. 이름 축약 금지
  3. 공백도 코딩 컨벤션 (if, for,. while문 사이의 공백)
  4. 반복 금지
  5. space와 tab 혼용하지 말기
  6. 의미 없는 주석 없애기
  7. 의미 있는 커밋 메시지
  8. 구현 중 업데이트 된 기능들이 있다면 기능 목록도 함께 업데이트 하기
  9. 기능 목록을 재검토하기
  10. README.md 상세히 작성하기
  11. IDE의 코드 자동 정렬 기능 활용
  12. 매직 넘버 사용 금지
  13. 구현 순서
    : 상수, 클래스 변수 -> 인스턴스 변수 -> 생성자 -> 메소드

참고

  1. https://github.com/woowacourse/java-baseball-precourse
  2. https://junhyunny.blogspot.com/2019/03/field-getdeclaredfields-setaccessible.html
반응형