본문 바로가기

우아한테크코스 6기/프리코스

우아한테크코스 6기 프리코스 1주차

728x90

1. GitHub (진행 가이드 정리)

 

처음에 미션 저장소 GitHub 링크에 있는 READ.md 파일을 보고 살짝 당황했다. 어디선가 본 적이 있는 문제였기 때문이었다. 하지만 안내문에 나와있던 문구를 보고는 금방 이해가 됐다.

 

1주 차 미션은 개발 환경을 세팅하고, GitHub에 과제를 제출하는 등 미션 외에도 추가로 익혀야 하는 부분들이 있어 가벼운 미션으로 준비했어요.
저희는 가볍다고 생각하는데 여러분은 어떻게 느낄지 모르겠네요. 앞으로 갈수록 조금씩 난도가 높아지도록 설계했어요.

 

우선 안내문 "프리코스 진행 방식 - 미션 제출 방법 - GitHub을 활용한 제출 방법은 문서를 참고해 제출한다."에 있는 문서 링크 중 진행 가이드 부분을 차근히 읽어보았다. GitHub에 대해 잘 알고 있었다고 생각했는데 아니었다. 진행 가이드와 검색을 통해 GitHub에 대해 알게 된 부분을 요약해 보겠다.

 

1) fork

fork는 우리가 생각하는 그 포크가 맞다. 포크의 모양을 보면 손잡이에서 끝 부분으로 갈수록 여러 뾰족한 갈래로 나눠진다. 즉, 하나의 큰 줄기에서 여러 갈래로 나누어지는 나만의 저장소이다. 쉽게 말해 fork를 하면 대상 저장소를 나의 계정으로 복사하는 것이다.

 

A fork is a new repository that shares code and visibility settings with the original “upstream” repository.

출처 - GitHub Docs

 

2) clone

clone의 뜻은 말 그대로 복제이다. 앞서 fork한 저장소는 github에 있기 때문에 자유롭게 수정이 불가능하다. 따라서 이 fork된 저장소를 나의 컴퓨터(local)로 복사하는 것이 clone이다. 

 

When you clone a repository, you copy the repository from GitHub.com to your local machine.   

출처 - GitHub Docs

 

3) branch

맞는 비유인지는 잘 모르겠지만 내가 생각한 branch는 fork된 저장소의 또 다른 버전이다.

 

You always create a branch from an existing branch. Typically, you might create a new branch from the default branch of your repository.  

출처 - GitHub Docs

 

우리가 처음 fork 했을 때의 브랜치는 master (main) 하나이다. main 브랜치에서 작업을 하면서 문제가 생기면 다시 원본으로 돌아갈 수 없게 될 것이다. 그렇기 때문에 또 다른 브랜치를 만들어 이를 방지하는 것이다. 아래 그림을 보면 local에서 기능 구현 후 add, commit을 하게 되면 브랜치 javajigi의 버전이 2.0이 된 것을 볼 수 있다. 

[그림 1]
[그림 2]

 

4) add, commit vs. push

 

구현한 기능들을 반영한다는 점에 있어서는 위 세 명령이 같지만 어디에 반영하냐는 점에 있어서 차이가 있다. add, commit은 local(내 PC)에 변경을 반영하고나서 push는 remote(GitHub branch)에 해당 내용을 반영하는 것이다. 

 

5) Pull Request

 

Pull Request는 original 저장소를 내 branch와 비교하고 그 차이점을 반영해 준다. 

 

[그림 3]

 

위 그림으로 큰 흐름을 다시 보면 ① Fork ② Clone ③ Push ④ Pull Request이다. 

 

6) git 명령

 

git 명령은

이외에도 진행 가이드 터미널에서 쓴 git 명령어도 아래 사이트에서 다운로드하였다.

 

https://git-scm.com/download/win

 

Git - Downloading Package

Download for Windows Click here to download the latest (2.42.0) 32-bit version of Git for Windows. This is the most recent maintained build. It was released about 2 months ago, on 2023-08-30. Other Git for Windows downloads Standalone Installer 32-bit Git

git-scm.com

 

.exe 파일을 실행하고 선택할 옵션들이 많은데 그냥 기본적으로 선택되어 있는 옵션으로 진행하면 된다.


2. 미션 요구 사항 파악

 

진행 가이드 "5. 미션 요구"를 보면 미션 요구사항을 파악해 기능을 구현하라고 적혀있다. 요구 사항에는 크게 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항이 있다. 아래 순서대로 읽어보면서 중요하다고 생각한 부분과 결과들을 다음과 같이 정리했다.

 

1) 과제 진행 요구 사항

 

위에서 정리한 방법을 토대로 진행했다.

 

  • 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리

모든 코드를 Application의 main 메서드 안에 집어넣을 수도 있었겠짐나 객체 지향적으로 프로그래밍하기 위해 클래스, 연관된 메서드를 먼저 구별하였다. "3) 기능 요구 사항"에서 설명할 예정이다.

 

2) 프로그래밍 요구 사항

  • JDK 17 버전에서 실행 가능해야 한다.

JDK 8 버전을 사용하고 있었기 때문에 아래 링크에서 JDK 17을 다운로드하고 해당 bin 폴더 위치를 환경 변수에 추가해 주었다. IDE에서 Java 17로 실행되는지 확인하기 위해 다음과 같이 확인했다.

 

[그림 4]

 

시간 부족으로 체크하지 못했다.

 

  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.

[그림 5] 2차 commit 후 테스트 실행했을 때 터미널의 일부

gradlew.bat clean test 명령이 [그림 5]와 같은 결과가 나와서 분명 사용자가 잘못된 입력값을 입력할 경우 throws IllegalArgumentException로 예외를 발생하게 했는데 왜 테스트가 실패했을까 했다. 다시 프로그래밍 요구 사항을 읽어보니 IllegalArgumentException만 발생시킨다고 적혀 있었고 에러를 catch 하는 것에 대한 언급이 없었다. ApplicationTest의 예외_테스트 또한 "1234"라는 예외를 실행했을 때 IllegalArgumentException의 종류인지 확인(isInstanceOf)하는 것으로 보아 Application 클래스에서 catch 하면 안될 거 같았다. try-catch 블록을 없애주니 정상적으로 테스트가 통과됐다.

 

    @Test
    void 예외_테스트() {
        assertSimpleTest(() ->
                assertThatThrownBy(() -> runException("1234"))
                        .isInstanceOf(IllegalArgumentException.class)
        );
    }

 

라이브러리는 3) 기능 요구 사항에서 설명하겠다.

 

3) 기능 요구 사항

 

우선 다음과 같이 객체의 후보에는 노란색, 기능(메서드)의 후보에는 초록색으로 표시했다.

 

[그림 6] 입력값은 주어진 Console API에서, 출력값은 println 메서드를 사용하기로 했다.

 

이를 바탕으로 구현할 기능 목록을 docs/README.md에 정리하기 시작했다. 구현할 기능들을 무작위로 쓰기보다는 클래스대로 분류하기로 했다. 위에 노란색으로 강조했듯이 Strike, Ball, Nothing 클래스를 Hint 클래스의 자손 클래스들로 만들어야 하나 생각했다. 하지만 생각해 보니 Strike, Ball, Nothing을 구분하는 방법은 하나의 메서드로 해결될 거 같아서 일단은 Hint 클래스 하나로 만들어 보려 했다.

 

(1) Hint 클래스

 

Hint (힌트) 클래스의 속성(변수)에는 볼과 스트라이크의 개수를 셀 수 있는 카운터, 플레이어와 컴퓨터의 3가지 숫자가 있다. 이때 볼과 스트라이크의 카운터가 0이면 자동으로 낫싱이기 때문에 따로 변수로 뽑아내지는 않았다. 사용 예시와  Random API를 보니 (아래 코드 참고) 컴퓨터가 선택한 3가지 숫자는 List<Integer>로 반환됐기 때문에 Hint의 컴퓨터의 3가지 숫자의 타입을 이로 설정했다. 또한 Console API를 보니 입력값을 받을 때 사용하는 static 메서드인 readLine의 반환 타입이 String이었기 때문에 (아래 코드 참고) 플레이어의 3가지 숫자를 담은 입력값을 String으로 설정했다. 그리고  플레이어와 컴퓨터의 3가지 숫자를 담는 변수에 static을 붙여야 하는 것 아닌가 하는 고민을 했지만 이렇게 한다면 플레이어의 입력값을 계속 받으면서 카운터들의 숫자가 계속 누적된다는 생각 때문에 붙이지 않았다.

 

// 라이브러리 - 사용 예시
List<Integer> computer = new ArrayList<>();
while (computer.size() < 3) {
    int randomNumber = Randoms.pickNumberInRange(1, 9);
    if (!computer.contains(randomNumber)) {
        computer.add(randomNumber);
    }
}

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

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

 

가장 중요한 볼과 스트라이크의 개수를 계산하는 메서드는 다음과 같은 방식으로 생각했다. 플레이어와 컴퓨터의 3가지 숫자를 담는 변수는 모두 배열의 형태 (String도 char 배열)를 띄고 있다. 따라서 두 배열을 비교해 다음과 같은 조건을 만족하면 볼과 스트라이크의 카운터가 1씩 증가한다. 

 

요소의 저장 값이 같을 때 인덱스 값이 서로 다름
스트라이크 인덱스 값이 서로 같음

 

그리고 힌트 자체의 이름이 될 수 있는 출력값들은 Object의 toString() 메서드를 오버라이드 하는 방식을 사용했다. Obejct는 모든 클래스의 조상이라는 사실 또한 이 부분을 통해 알게 됐다.

 

출력 문서를 보게 되면 다음과 같이 힌트로 출력 가능한 3가지 경우의 수가 있다. 

 

스트라이크
O O
O X
X O
X X

 

이를 위해 중첩 if문을 사용했다.

 

다른 클래스도 이와 같은 방식으로 생각하면서 만들었다. 이후에는 고민이 많았던 지점만 골라서 얘기해 보겠다.

 

(2) Computer 클래스가 필요한가?

 

사실 Computer 클래스가 하는 일이라곤 3가지 숫자를 만드는 일밖에 없지만 기능 요구 사항에 컴퓨터도 반복해서 나왔던 객체 후보 중 하나였기 때문에 그냥 따로 만들었다. 

 

(3) 플레이어의 입력값을 받는 메서드의 분리 (3차 commit에 반영)

 

플레이어의 입력값은 서로 다른 3가지 숫자, 1 또는 2, 이 두 가지 경우로 제한된다. 일반적으로 입력값을 받을 때는 Scanner 클래스를 통해 한 가지 경우로 받는 것이 보통이지만 이 과제에서는 Console API를 사용함과 동시에 앞서 말했던 조건들을 만족해야 했다. 처음에는 입력값을 받는 메서드를 하나로 만들었다. 하지만 이 두 조건들을 각기 다른 상황(숫자 입력, 게임 재시작 여부)에서 한 번에 처리할 수 있는 메서드를 만드는 것이 불가능하다 생각해서 ① 3가지 다른 숫자의 입력값을 받는 메서드, ② 게임 재시작 여부 메서드 두 가지로 나누게 됐다. 서로 다른 숫자라는 것을 확인하기 위한 메서드도 ①을 위해 별도로 만들었다.

 

(4) NumberFormat의 조상은 IllegalArgumentException이다.

 

(3)에서 언급했던 메서드( ① , ② )에서 IllegalArgumentException 예외를 발생시킬 조건을 따지기 위해서는 int로의 변환이 필수였다. 그렇지만 기능 요구 사항에서는 IllegalArgumentException만 발생시켜야 한다고 해서 int로의 변환을 담당하는 parseInt가 실패할 경우 어떤 예외가 발생하는지 알아보았다. NumberFormatException 예외였고 이는 IllegalArgumentException의 자손 클래스였다. ([그림 7]) 이는 ApplicationTest.java의 27번째 줄에서도 확인 가능하다.

 

[그림 7]

 

(5) Application의 단순화

 

처음에는 Game 클래스를 만들지 않고 게임의 모든 과정을 Application 클래스에 main 메서드에 집어넣었다. 이렇게 하니 가독성이 떨어지는 것은 물론 논리를 한눈에 보기 어려웠다. 그래서 두 개의 반복(숫자 입력 반복, 개임 재시작 반복)을 각각 Game 클래스의 메서드로 만들었다. 이전에는 Computer 인스턴스 생성(랜덤 숫자 3가지 만들기)을 어디에 할지 헷갈렸는데 이렇게 메서드 단위로 나누니 훨씬 파악하기 쉬웠다. 

 

[그림 8] 실행 결과 예시를 보면 두 개의 반복이 있다.

 


3. 더 고민해야 할 부분

 

① ApplicationTest의 분석

 

[그림 9]와 같이 컴퓨터의 서로 다른 3가지 수를 출력하는 코드를 임의로 집어 넣고 가능한 모든 힌트 경우의 수를 직접 입력해보는 방식을 사용했다. 이와 같은 방법은 코드를 임의로 변경해야 하고 또 다양한 입력값을 빠르게 테스트할 수 없으므로 다음에는 ApplicationTest를 분석해서 직접 만들어보아야겠다고 생각했다.

 

[그림 9]

 

Java 코드 컨벤션 가이드 준수

 

③ 각 클래스의 접근 제어자 범위 설정

 

④ static 멤버 아니면 그냥 멤버 변수의 구분

 

⑤ Player, Computer, Application 클래스 간의 관계 

 

포함 아니면 Player와 Computer를 Application의 멤버변수로 설정해야 하나 고민이었다.

반응형