Web/Java

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

🚀 기능 요구사항

기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.

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

 

 


 

일단 코드를 작성하기 전에 기능 목록부터 작성해보기로 했습니다.

 

아래는 "맨 처음" 작성한 기능 목록입니다. (지금은 바뀌었습니다.)

🚀 기능 목록

  • 랜덤으로 1~9짜리 3개의 수를 생성한다.
  • 사용자에게 수를 입력받는다.
  • 입력받은 3자리 수에서 볼, 스트라이크 개수를 구해서 반환한다.
  • 구해진 볼, 스트라이크를 통해 출력값을 결정한다.
    • 스트라이크 0개 : "낫싱"
    • 스트라이크 1개 ~ 2개 : "n볼 n스트라이크"
    • 스트라이크 3개 : "3스트라이크"
      • 정답문구 출력 : "3개의 숫자를 모두 맞히셨습니다! 게임 종료"
      • 계속 진행할 것인지 묻기 : "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."
        • 1 입력 : 처음으로 돌아간다.
        • 2 입력 : 프로그램을 종료한다.

 

 

기능 목록을 작성하면서 커밋메세지 컨벤션도 한번 정리하면 좋을 것 같았습니다.

그래서 AngularJS Commit Message Conventions 를 참고해서 커밋메세지 컨벤션을 정리했습니다.

 

커밋메세지 컨벤션

  • "태그: 제목"의 형태이며, 태그는 영어로 쓰되 첫 문자는 대문자로 합니다.
  • : 뒤에만 space가 있습니다.
  • ex) feat(...): add score compute

태그이름설명

feat 새로운 기능을 추가할 경우
fix 버그를 고친 경우
style 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우
refactor 프로덕션 코드 리팩토링
comment 필요한 주석 추가 및 변경
docs 문서를 수정한 경우
test 테스트 추가, 테스트 리팩토링(프로덕션 코드 변경 X)
chore 빌드 테스트 업데이트, 패키지 매니저를 설정하는 경우(프로덕션 코드 변경 X)
rename 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우
remove 파일을 삭제하는 작업만 수행한 경우

 

 

 

우테코에서 제공한 자바컨벤션도 먼저 쭉 읽어보고, 이미 알고 있던 것 빼고 새로 알게된 것들과 잘 틀릴만한 것들을 따로 정리했습니다.

Java 컨벤션 (내가) 주의할 사항

import

import 선언의 순서와   삽입 [import-grouping]

import 구절은 아래와 같은 순서로 그룹을 묶어서 선언한다.

  • static imports
  • java.
  • javax.
  • org.
  • net.
  • 8~10을 제외한 com.*
  • 1~6, 8~10을 제외한 패키지에 있는 클래스
  • com.nhncorp.
  • com.navercorp.
  • com.naver.

 그룹 사이에는 빈줄을 삽입한다. 같은 그룹 내에서는 알파벳 순으로 정렬한다.

좋은 

import java.util.Date;
import java.util.List;

import javax.naming.NamingException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;

import com.google.common.base.Function;

import com.naver.lucy.util.AnnotationUtils;

 규칙은 대부분 IDE에서 자동으로 정리해주는 대로 쓰기 때문에 IDE 설정을 일치시키는데 신경을 써야 한다.

줄바꿈 위치

줄바꿈 허용 위치 [line-wrapping-position]

가독성을 위해 줄을 바꾸는 위치는 다음 중의 하나로 한다.

  • extends 선언 후
  • implements 선언 후
  • throws 선언 후
  • 시작 소괄호(() 선언 후
  • 콤마(,) 후
  • . 전
  • 연산자 전
    • +, -, *, /, %
    • ==, !=, >=, >,⇐, <, &&, ||
    • &, |, ^, >>>, >>, <<, ?
    • instanceof

좋은 

public boolen isAbnormalAccess (
User user, AccessLog log) {

    String message = user.getId() + "|" | log.getPrefix()
        + "|" + SUFFIX;
}

import 와일드카드

static import에만 와일드 카드 허용 [avoid-star-import]

클래스를 import할때는 와일드카드(*) 없이 모든 클래스명을  쓴다. static import에서는 와일드카드를 허용한다.

나쁜 

import java.util.*;

좋은 

import java.util.List;
import java.util.ArrayList;

탑레벨 클래스

소스파일당 1개의 탑레벨 클래스를 담기 [1-top-level-class]

탑레벨 클래스(Top level class) 소스 파일에 1개만 존재해야 한다. ( 탑레벨 클래스 선언의 컴파일타임 에러 체크에 대해서는 Java Language Specification 7.6 참조 )

나쁜 

public class LogParser {
}
class LogType {
}

좋은 

public class LogParser {
// 굳이 한 파일안에 선언해야 한다면 내부 클래스로 선언
class LogType {
}
}

접근제한자

클래스  멤버 수정자가 있는 경우 Java 언어 사양에서 권장하는 순서로 나타냄

public protected private abstract default static final transient volatile synchronized native strictfp

예외 잡기: 생략하지 않음

아래 명시되있는 것말고 예외를 잡고 아무것도 안하는 것은 거의 있을  없습니다. (전형적인 반응은 로그를 남기는  혹은 불가능하다고 간주되면 AssertionError로 다시 던져줍니다.) 캐치 블록에서 아무것도 하지 않는 것이 정당하다면 주석을 남기는 것으로 정당화합니다.

try {
    int i = Integer.parseInt(response);
    return handleNumericResponse(i);
} catch (NumberFormatException ok) {
    // it's not numeric; that's fine, just continue
}
return handleTextResponse(response);

예외: 테스트에서 예외를 잡는 부분은 expected, 혹은 expected로 시작하는 이름을 지으면서 무시할  있습니다. 다음 예제는 테스트에서 예외가 나오는게 확실한 상황에서 사용되는 대중적인 형식으로 주석이 필요없습니다.

try {
    emptyStack.pop();
    fail();
} catch (NoSuchElementException expected) {
}

 

 

 

 

 

코딩시작

 

과제는 만약 코딩테스트같이 그냥 풀어내기만 하면 되는 문제라면 쉽게 풀어낼 수 있을만한 쉬운 문제였지만,

저는 우테코를 하기 전부터 매 과제마다 내가 할 수 있는 최대한 좋은 구조와 코드를 작성하고 싶었습니다.

 

하지만 이제까지는 좋은 구조에 대해 고민해본적도 없고 밑바닥부터 자바로만 이루어진 프로그램을 개발해본 적도 없었습니다.

 

그래도 ClassFlix를 개발하면서 경험해본 바로 애플리케이션이 대충 어떤 구조로 되어야겠다고 생각은 들었습니다.

 

일단은 Controller가 필요하다고 생각했습니다.

 

Controller

  • 게임세팅과 진행을 컨트롤러에서 제어한다.
  • 컨트롤러에서 view나 service, util class들을 부릅니다. (반대로 view, service, util에서는 controller를 못봄)

기본적으로 이 규칙을 가지고 프로그래밍을 진행했습니다.

 

이 방식이 맞는 방식인지는 모르겠으나 Controller (프로그램 구조상 가장 상위)를 만들며 필요할때 마다 다른 클래스들을 만들어 갔습니다.

전에 classflix를 제작할 때는 domain, repository, service를 거쳐 controller를 제작한 것과는 반대였습니다.

아마도 작은 프로그램이다보니 controller부터 작성하는게 가능했던 것 같았고 프로그램이 커질수록 작은 단위부터 만들어야될 것 같았습니다.

 

애플리케이션의 개발 순서에 관한 자료를 좀 더 찾아봐야겠습니다.

 

한번 시작하니까 프로그램 자체를 완성하는데 까지는 2~3시간정도밖에 걸리지 않았습니다.

그런데 우테코에서 제공하는 테스트가 다 fail이 발생했습니다.

 

 

 

 

테스트 실패원인 - 1

원인은 파악완료했습니다.

 

문제를 꼼꼼히 읽어보지않고 "제가 잘못 알고있던 숫자야구 게임"을 구현하고 있었습니다.

 

제가 잘못 알고 있던 숫자야구 게임

  • 자리 + 숫자 까지 맞으면 : 스트라이크
  • 틀리면 : 볼
  • 입력한 3개의 수가 전부 틀리면 : 낫싱

 

실제 올바른 숫자 야구 게임

  • 자리 + 숫자가 맞으면 : 스트라이크
  • 숫자만 맞으면 (다른 자리에 있으면) : 볼
  • 입력한 3개의 수가 어느 자리에도 다 없으면 : 낫싱

 

잘못된 내용을 파악한 뒤 기능목록을 다시 작성했습니다.

 

🚀 기능 목록

  1. 랜덤으로 1~9짜리 서로 다른 3개의 수를 생성한다.
  2. 사용자에게 수를 입력받는다.
  3. 입력받은 수를 검증한다.
  4. 입력받은 3자리 수에서 볼, 스트라이크 개수를 구해서 반환한다.
  5. 구해진 볼, 스트라이크를 통해 출력값을 결정한다.
    • 스트라이크, 볼 0개 : "낫싱"
    • 스트라이크 0~2개, 볼 0개 아님 : "n볼 n스트라이크"
    • 스트라이크 3개 : "3스트라이크"
      • 정답문구 출력 : "3개의 숫자를 모두 맞히셨습니다! 게임 종료"
      • 계속 진행할 것인지 묻기 : "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."
        • 1 입력 : 처음으로 돌아간다.
        • 2 입력 : 프로그램을 종료한다.
  6. 스트라이크 3개가 나올 때 까지 2~5과정을 반복한다.

 

새로운 기능목록으로 코드를 수정했더니 올바른 결과가 나왔습니다.

 

 

 

 

테스트 실패원인 - 2

그리고 또 테스트를 돌렸는데 실패했습니다.;

 

왜 안되지 계속 고민하면서 실패로그를 봤는데 출력문구가 다른 것을 발견했습니다.

 

내가 작성한 출력문구

3개의 숫자를 모두 맞히셨습니다! 게임종료
게임을 계속 시작하려면 1, 종료하려면 2를 입력하세요.

 

 

올바른 출력문구

3개의 숫자를 모두 맞히셨습니다! 게임 종료
게임을 다시 시작하려면 1, 종료하려면 2를 입력하세요.

 

 

"게임V종료", "다시" 부분이 틀렸습니다.

 

이 부분도 문제를 꼼꼼히 읽지 않았기 때문에 발생한 문제였습니다.

 

("계속"은 왜 저렇게 적은건지 모르겠네;)

 

우테코 하기 전부터 문제를 진짜 꼼꼼히 읽겠다고 결심해놓고 이런 실수를 했습니다.

이제는 정신 똑바로 차리고 다시는 실수하지 않도록 해야합니다.

 

 

 

테스트 실패원인 - 3

Exception 발생이 되지 않고 있었습니다.

잘못된 입력값이 있을때 IllegalArgumentException이 잘 발생하고 프로그램이 종료되는 것이 보입니다.

 

 

분명 중간에 throw로 Exception을 발생시키고..

상위 계층으로 넘겨주고... Controller에서도 더 상위로 넘겨주고.. 거기서 try catch를 했는데..?

 

아!

 

Exception이 발생하긴 했는데 Exception발생으로 프로그램이 종료된 것이 아니라

try catch를 해서 printTrace을 호출한 내용이 보이고 있었습니다.

 

try-catch문을 제거하고 그냥 Controller에서 Exception이 발생하도록 했습니다.

 

이제 기본 테스트는 다 통과했습니다.

 

 

 

 

 

구조 리팩토링

기존에는 컨트롤러에서 서비스로직을 실행하는 코드가 많았습니다.

이것들을 domain과 연결된 서비스클래스를 생성하여 이곳에서만 복잡한 서비스 로직을 실행하도록 했습니다.

 

또한, 클래스끼리 묶으면서 리팩토링했는데 의미없이 묶어서인지 구조상 이해가 잘 가지 않았습니다.

예를들어 지금까지는 GameNumber와 UserNumber를 묶어서 Numbers라는 클래스로 다뤘는데, 이렇게 클래스로 묶는 것 보다 Game클래스 안에 gameNumber와 strike, ball개수를 담고(게임과 관련된 데이터들), User클래스에 user가 입력한 숫자를 보관(유저와 관련된 데이터들)하는 것이 더 좋은 구조였습니다.

 

이제 GameService에서 Game클래스와 User클래스간의 서비스 로직을 만들고, 컨트롤러에서는 이를 호출만 하도록 짜서 더 깔끔한 코드를 완성했습니다.

public class Controller {
	final int SIZE = 3;
	final int START_INCLUSIVE = 1;
	final int END_INCLUSIVE = 9;
	final int RETRY = 1;
	final int GAME_OVER = 2;

	GameService gameService = new GameService();

	public void run() throws IllegalArgumentException {
		setGame();
		startGame();
		endGame();
		askRetry();
	}

	private void setGame() {
		gameService.setGame(SIZE, START_INCLUSIVE, END_INCLUSIVE);
	}

	private void startGame() throws IllegalArgumentException {
		gameService.playGame();
	}

	private void endGame() {
		SystemMessage.printGameOverMessage();
	}

	/**
	 * 유저입력이 (문자 or 0 or 3 이상)  : Exception
	 * 유저입력이 (1)                  : 재시작
	 * 유저입력이 (2)                  : 종료
	 */
	private void askRetry() throws IllegalArgumentException {
		RequestInput.printRetryMessage();
		if (getInputNum() == RETRY) {
			run();
		}
	}

	private int getInputNum() throws IllegalArgumentException {
		int inputNum = Integer.parseInt(Console.readLine());

		if (inputNum == 0 || inputNum > GAME_OVER) {
			throw new IllegalArgumentException();
		}
		return inputNum;
	}
}

 

 

 

 

 

느낀점

쉬운 난이도의 과제를 내준 이유는 분명 있을 것입니다.

깃에 적응하고 새로운 자바문법에 적응하라는 이유도 있겠지만, 저는 이미 깃과 자바에는 어느정도 익숙했습니다.

그래서 저의 성장에 도움이 되려면 프리코스기간동안 애플리케이션 구조와 읽기좋은 코드를 중점으로 과제진행을 해야겠다고 생각했습니다.

 

코드를 완성했다고 생각해도 계속 더 좋은 구조, 읽기좋게 수정하고 싶은 부분이 생각났습니다.

처음엔 컨트롤러에 모든 서비스 로직을 다 박았지만, 지금은 컨트롤러에서 서비스 로직을 호출하기만 합니다.

 

요구사항 완성은 하루도 채 걸리지 않았지만 리팩토링만 4~5일동안 진행중입니다.

처음에 짰던 코드도 그 당시에는 잘 짰다고 생각했지만 지금보면 구린코드로 보입니다.

 

한번도 밑바닥부터 혼자 설계해본적 없지만 이번 과제를 짜면서 다음 과제를 할땐 어떤 것 부터 수행해야할 지 계획이 잡혔습니다.

첫번째 과제에서 얻은 좋은 관점과 포인트들을 두번째 과제에서 적용할 생각에 기대가 되고, 또 새로 만날 어려움도 기대가 됩니다.

3주간의 프리코스에서 진심으로 임해서 꼭 성장해 떨어지더라도 얻은 것이 많았던 시간이 되도록 만들 것입니다.

 

 

 

 

 

 

 

 

 

리팩토링

지금(11/26~)부터 제출전(~11/30)까지 계속 더 좋은 코드로 리팩토링을 하고 있습니다.

 

  • 숫자야구게임이 지금은 3개이지만 혹시 숫자가 커질때의 확장성 고려
  • 사용자의 다양한 입력값에 대응
  • 채점하시는 멘토님이 더 읽기 편하도록 고치기
  • 최대한 클래스별로 구분하기
  • 1단계 미션부터 그냥 2~3단계의 최종 제약사항까지 고려하기
    • else 사용불가
    • 메소드당 15줄 이내
  • 모든 파일 UTF-8 설정, build.gradle UTF-8 설정
  • controller에서 최대한 서비스로직이 안보이고 호출하는 용도로만 리팩토링

 

 

제가 작성한 숫자야구게임 코드는 아래에서 확인하실 수 있습니다.

코드확인

 

 

 

'Web > Java' 카테고리의 다른 글

[우테코 프리코스] 3주차: 자판기  (2) 2021.12.11
[우테코 프리코스] 2주차: 자동차 경주 게임  (0) 2021.12.01
[Java] 람다식  (2) 2021.09.01
[Java] 제네릭  (1) 2021.08.30
[Java] I/O  (2) 2021.08.24