기다리고 기다리던 프리코스의 1주차를 마쳤다. 간단하게 할 수 있을거라 생각했던 1주차인데 생각보다 신경써야 할 것들이 많았다. 일단 받았던 기능 요구사항은 다음과 같다.
숫자야구 게임을 어떻게 구현해야 하는지 방법을 알려줬다. 기능 요구사항만을 만족하기에는 어려운 것은 아니나 그 외의 요구사항들이 많았다.
자바 코드 컨벤션을 지키면서 프로그래밍한다.
https://naver.github.io/hackday-conventions-java/
위 사이트를 기준으로 컨벤션을 지키라고 하였다. 1주차에는 이름 규칙과 관련된 것들만 읽고 포맷터를 다운로드 받아 자동으로 적용시켰다. 인텔리제이의 자동 정렬 기능을 사용할 예정이었기 때문에 컨벤션을 따로 신경쓰지 않아도 될거라 생각했었으나 자동으로 정렬되는 것은 간격, 라인 등 뿐이지 실제 값을 변경시켜주지는 않는다. 한번에 다 읽고 적용하는 것에는 무리가 있으니 프리코스를 진행하면서 틈틈히 읽고 적용시켜야겠다.
글을 포스팅하면서 다시 컨벤션 규칙을 읽어봤는데 안지킨것들이 보인다..넘 슬프다.
indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
2중 반복문을 애용(ㅠ)하던 나에겐 좀 당황스러운 문장이었다. 반복이 있어야 하는 로직도 있는데 할 수 있을까?? 걱정했었는데 2중은 무슨 단 한번의 반복도 없이 구현할 수 있는 상황도 있었다. 그동안 너무 반복문을 남용했던것같아 반성했다. 이번 숫자야구 게임에서도 1. 하나의 게임을 진행하는 것과 2. 사용자의 답을 받는 총 두번의 반복이 필요하다. 아래 코드를 작성하겠지만 1번은 게임이 종료됐을 때 다시 정답을 설정하면서 해결하였고 2번은 재귀함수를 이용하였다.
3항 연산자를 쓰지 않는다.
3항 연산자는 원래 잘 쓰지 않는다. 3항 연산자에는 true로직과 false로직이 한 줄에 있는데 로직이 길어질 수록 가독성이 떨어지기 때문에 지양하는 것이 아닌가 추측해본다.
함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.
아직까지 한 가지의 일의 기준을 잘 모르겠다. 일단 최대한 작게 만들 수 있을 때까지 나누기는 했는데 그러다보니 하나의 클래스에 자잘한 함수들이 많이 선언되어 있어 가독성이 떨어지는듯 보인다. 클래스를 나누는 방법을 조금 더 고민해봐야겠다.
JDK에서 기본 제공하는 Random, Scanner API 대신 camp.nextstep.edu.missionutils에서 제공하는 Randoms, Console API를 활용해 구현해야 한다.
camp.nextstep.edu.missionutils
는 우테코에서 직접 구현한 라이브러리이다. 우테코에서 만들었기 때문에 당연히 검색해도 관련 내용이 나오지 않는다. 직접 내부적인 코드를 보고 익히라는 의미로 받아들였다.
프로그램 구현을 완료했을 때 src/test/java 디렉터리의 ApplicationTest에 있는 모든 테스트 케이스가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
이번 프리코스를 하면서 테스트 케이스를 직접 돌려보는 것은 처음이었다. 정상 작동하는 테스트 케이스 하나, 예외 테스트 케이스 하나만 주어졌기 때문에 그 외의 테스트 케이스를 더 추가하고 싶었으나 아직 자바에 대해서 모르는 것이 많아 코드를 분석하지 못했다. 다음 과제에서는 다양한 테스트 케이스를 추가해보고 싶다.
기능을 구현하기 전에 java-baseball-precourse/README.md 파일에 구현할 기능 목록을 정리해 추가한다.
코딩테스트 풀던 습관이 남아 매번 간단한 기능을 구현하더라도 어떻게 구현할지 정리한다. 하지만 큰 그림만 잡고 시작했었는데 직접 글로 기능 목록을 정의하니 그동안 너무 간단하게 생각하고 코드를 적지 않았나 싶었다. 기능 목록을 자세하게 정리하고 시작하니 기능 단위로 개발을 하면서 빠르게 진행할 수 있었다.
Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.
git은 써본지 얼마 안됐어서 커밋하는 기준, 커밋 메시지 등 신경써야 할 것이 많아 의외로 어려웠었다. 나름 기능 단위로 잘 커밋했다고는 생각하지만 자잘한 에러를 처리하느라 쓸데없이 추가된 커밋도 있다. 커밋 이력이 남는다고 생각하니 한 번 커밋할 때마다 신중하게하려 노력했으나 아직은 부족한듯싶다. 이번 1주차에는 개발을 진행하는 브런치 yukong
과 내 저장소의 코드를 가장 최신버전으로 유지시키는 main
두 가지만 사용했었는데 기능별로 브런치를 나누기도 한다고 들었다. 구현해야 할 기능이 많지 않아 기능별로 나누는 것은 효율적인것같지는 않으나 다른 브런치 구분 방법도 찾아서 써보고 싶다.
코드가 길어 클래스 별로 나누었다. 보기 좋게 나누었을 뿐 하나의 파일에 있는 클래스들이다.
처음엔 main함수에 게임을 진행하는 반복문 하나를 두고 다른 클래스 인스턴스를 생성하였으나 main함수에는 클래스를 구동시키는 코드 하나만 두는 것이 좋다는 피드백을 받고 수정하였다. Game의 인스턴스를 생성해 첫번째 게임의 정답을 설정하고 게임을 시작한다.
package baseball;
import static camp.nextstep.edu.missionutils.Randoms.pickNumberInRange;
import static camp.nextstep.edu.missionutils.Console.readLine;
public class Application {
public static void main(String[] args) {
Game rightAnswer = new Game();
Game.init(rightAnswer);
}
}
숫자 야구 게임과 관련 없는 메소드들은 별도의 클래스에 선언하여 관리하였다.
class Array {
static boolean checkArrayContains(final int[] arr, final int number) {
for (int value : arr) {
if (value == number) {
return true;
}
}
return false;
}
static boolean checkArrayContains(final char[] arr, final char number) {
for (int value : arr) {
if (value == number) {
return true;
}
}
return false;
}
static char[] getCharArrayFromString(String str) {
char[] charArray = new char[str.length()];
for (int i = 0; i < str.length(); i++) {
charArray[i] = str.charAt(i);
}
return charArray;
}
static int getIndexFromValue(final int[] arr, final int value) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == value) {
return i;
}
}
return -1;
}
}
이번 과제를 진행하면서 목표는 새로 알게된 것들은 한번씩 써보자
였다. 자바를 이번 프리코스를 진행하면서 처음 시작했기 때문에 모든게 다 새로 알게된 것들이기는 했다.(ㅋㅋ)
생성자를 호출할 때 주어진 파라미터에 따라 실행되는 생성자의 코드가 달라지는 것이 신기해 써봤다. 값 없이 생성자를 호출하면 랜덤 3자리의 숫자를 가지는 Game 인스턴스를, 문자열을 입력하여 생성자를 호출하면 입력값을 숫자로 가지는 Game 인스턴스를 생성한다.
다른 메소드들은 최대한 하나의 기능만을 실행하도록 나누고 깔끔하게 작성하려고 노력했다.
class Game {
private static final int START_RANGE = 1;
private static final int END_RANGE = 9;
static final int NUMBER_COUNT = 3;
private static final int NEW_GAME = 1;
private static final int QUIT_GAME = 2;
private static final String GET_NUMBER_MESSAGE = "숫자를 입력해주세요 : ";
private static final String SUCCESS_MESSAGE = Game.NUMBER_COUNT + "개의 숫자를 모두 맞히셨습니다! 게임 종료";
private static final String NEW_GAME_CHECK_MESSAGE = "게임을 새로 시작하려면 " + Game.NEW_GAME + ", 종료하려면 " + Game.QUIT_GAME + "를 입력하세요.";
int[] number = new int[NUMBER_COUNT];
Game() {
int nowRandomNum;
int nowNumberIndex = 0;
while (nowNumberIndex < NUMBER_COUNT) {
nowRandomNum = this.getRandomNumber();
if (!Array.checkArrayContains(number, nowRandomNum)) {
number[nowNumberIndex++] = nowRandomNum;
}
}
}
Game(String str) {
for (int i = 0; i < Game.NUMBER_COUNT; i++) {
number[i] = Integer.parseInt(str.charAt(i) + "");
}
}
private int getRandomNumber() {
return pickNumberInRange(Game.START_RANGE, Game.END_RANGE);
}
public static void init(Game rightAnswer) {
Game answer = Game.getAnswer();
Hint hint = new Hint();
hint.compareAnswer(answer, rightAnswer);
hint.showResult();
if (hint.strike != Game.NUMBER_COUNT) {
Game.init(rightAnswer);
return;
}
int newGameAnswer = Game.checkNewGameStart();
if (newGameAnswer == Game.NEW_GAME) {
rightAnswer = new Game();
Game.init(rightAnswer);
}
}
private static Game getAnswer() {
System.out.print(GET_NUMBER_MESSAGE);
String userInput = readLine();
Game.checkInputValue(userInput);
return new Game(userInput);
}
private static int checkNewGameStart() {
System.out.println(SUCCESS_MESSAGE);
System.out.println(NEW_GAME_CHECK_MESSAGE);
String newGameAnswer = readLine();
return Game.getNewGameAnswerNumber(newGameAnswer);
}
private static int getNewGameAnswerNumber(String str) {
int intValue;
try {
intValue = Integer.parseInt(str);
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
if (intValue != 1 && intValue != 2) {
throw new IllegalArgumentException();
}
return intValue;
}
public String toString() {
return "number: " + this.number[0] + this.number[1] + this.number[2];
}
static void checkInputValue(final String str) {
if (!Game.checkInputLength(str)) {
throw new IllegalArgumentException();
}
if (!Game.checkInputNumber(str)) {
throw new IllegalArgumentException();
}
if (!Game.checkEqualNumber(str)) {
throw new IllegalArgumentException();
}
}
static boolean checkInputLength(final String str) {
return str.length() == Game.NUMBER_COUNT;
}
static boolean checkInputNumber(String str) {
try {
int numberValue = Integer.parseInt(str);
} catch (NumberFormatException e) {
return false;
}
char[] numberArray = Array.getCharArrayFromString(str);
return !Array.checkArrayContains(numberArray, '0');
}
static boolean checkEqualNumber(String str) {
char[] checkEqual = new char[str.length()];
char[] word = Array.getCharArrayFromString(str);
for (int i = 0; i < str.length(); i++) {
if (Array.checkArrayContains(checkEqual, word[i])) {
return false;
}
checkEqual[i] = word[i];
}
return true;
}
}
입력한 값에 맞는 힌트를 작성할 때에는 각 상황에 따라 비슷한 결과를 출력하기 때문에 중복코드가 나올 수도 있다고 생각하여 중복코드가 나오지 않도록 노력했다.
class Hint {
int ball, strike;
private static final String BALL_WORD = "볼";
private static final String STRIKE_WORD = "스트라이크";
private static final String NOT_MATCH_WORD = "낫싱";
public String toString() {
return "ball: " + this.ball + ", strike: " + this.strike;
}
private void addBallCount() {
this.ball++;
}
private void addStrikeCount() {
this.strike++;
}
void compareAnswer(Game answer, Game rightAnswer) {
int nowNumber;
for (int i = 0; i < Game.NUMBER_COUNT; i++) {
nowNumber = answer.number[i];
if (!Array.checkArrayContains(rightAnswer.number, nowNumber)) {
continue;
}
if (i == Array.getIndexFromValue(rightAnswer.number, nowNumber)) {
addStrikeCount();
continue;
}
addBallCount();
}
}
void showResult() {
String result = "";
if (this.ball != 0) {
result += this.ball + BALL_WORD;
}
if (this.strike != 0) {
if (this.ball != 0) {
result += " ";
}
result += this.strike + STRIKE_WORD;
}
if (result.equals("")) {
result = NOT_MATCH_WORD;
}
System.out.println(result);
}
}
나름 클래스와 메소드를 깔끔하게 나눴다고 생각했는데 다른 사람들의 코드를 보니 한참 부족한게 느껴진다. 자바를 시작하기 전 python은 절차적 프로그래밍만 해봤어서 아직 그 습관이 남아있다. 자바를 하는 만큼 클래스를 잘 활용할 수 있는 코드를 작성하고 싶다.
커밋 메시지
커밋 메시지를 작성할 때에는 어떤 것
을 개발했는지가 아니라 왜
추가, 수정했는지를 작성해야 한다고 한다. 이 부분이 조금 부족했다.
클래스 구조
처음에 쓸데없는 상속으로인해 내 코드는 정상작동했으나 테스트코드를 돌려보면 타임아웃이 걸렸다. 상속이라는 개념도 한번 적용해보고 싶어서 써본건데 역효과났다. 😂 그래도 덕분에 상속을 더 파헤쳐보는 시간을 가지기는 했다.. 다음엔 이런 고생을 하지 않도록 처음 기능 목록을 정의할 때 각 클래스 간 관계를 잘 나누고 싶다. 또 메소드를 잘게 나누니 한 클래스 내에 자잘한 메소드가 많아 가독성이 떨어졌다. 클래스를 분리하거나 메소드 정의 순서를 조정하여 가독성을 높여야 겠다.
테스트 코드 활용
이번 과제에서는 상속으로인해 발생한 오류를 해결하느라 시간을 많이 지체하여 테스트 코드를 추가하지 못했다. 과제를 진행하면서 자바를 더 공부해 우테코에서 제공한 테스트코드 라이브러리를 분석해 기본으로 제공한 테스트뿐만 아니라 그 외의 테스트도 추가해보고 싶다.
첫 과제였고 간단하다 생각했으나 신경써야 할 것들이 정말 많았다. 내가 아직 처음 시작해본 것들이 많아 익숙하지 않아서 어려웠다고 생각한다. 이제 2주차 과제를 기다리고 있는데 다음 과제에서는 내가 목표하고 있는 것들을 잘 적용해보고 싶다.
20211217추가
최종 코딩테스트를 준비하면서 어떤걸 할까, 하다가 이전 과제들을 다시 짜보기로 했다. 그러면서 1주차 과제 코드들을 다시 봤는데 문제점들이 보인다는 것만으로도 많이 성장했구나 싶다.
일단 요즘 연습하고 있는 MVC 패턴대로 나누어보았다.
View는 사용자가 실제 입력하고 결과를 확인하는 코드로 구성되어있다. 현재 진행하는 게임에서 답이라고 생각하는 세자리의 수를 입력하거나 입력한 답에 따른 힌트 및 결과를 표출한다.
Model은 데이터와 관련된 코드로 구성되어있다. model은 실제 데이터 구조와 해당 값을 호출하는 메소드로만 이루어져 있으며 실제로 값을 수정하거나 관리하는 코드들은 repository에 두었다.
Controller는 숫자야구게임을 진행하는 코드로 구성되어있다. 그 중 controller는 사용자와 관련있는 코드이고 service는 게임 자체에 관련있는 코드이다. controller의 코드를 봤을 때에는 사용자 입장에서 게임이 어떻게 흘러가는지 파악할 수 있도록 작성하였고 service는 게임 자체가 어떻게 진행되는지 알 수 있도록 작성하였다.
그 외에 type은 게임 자체에서 포괄적으로 사용되는 상수들을 모아 관리하였다.
1주차 과제를 리팩토링하면서 Enum 클래스를 내가 직접 생성해서 사용해보았다. 여지껏 상수는 클래스에 속하는 일반 멤버변수로만 사용해봤는데 여러 클래스에 걸쳐서 사용되어야 하는 변수가 필요했다. 해당 변수를 선언한 클래스를 호출해서 사용하면 되지만 그럼 그 멤버변수는 어떤 클래스에 선언해야는지도 명확하지 않았고 특정 변수에 다이렉트로 접근을 막기위한 코드를 짜면서 일부 상수에 한해서만 예외를 한다는 것이 맞지 않아 다른 방법을 찾아보았다. 오히려 클래스 수를 늘리는 일이 될거라 생각했던 코드가 구조를 더 명확하게 만들어서 이전 코드보다 이해하기 쉬워졌다 생각한다.
이번 리팩토링을 진행하면서 신경쓰고 있는점 중 하나가 프로젝트 구조 및 클래스와의 관계이다. MVC 패턴을 적용하고 싶어 찾아봐도 블로그 포스팅마다 조금씩 다른 구조를 사용했어서 때에 따라 맞게 일부만 가져와서 사용해보았었다. 그러면서 하나둘씩 패키지가 생기고 더 세분화될수록 코드를 짜기가 점점 쉬워지는게 신기하고 재미있었다. 😁 고작 2~3주 지났을 뿐인데 코드를 짤 때 고려하는 것이 달라진게 뿌듯하다.