JUINTINATION
우테코 백엔드 프리코스 체험해보기(숫자 야구 게임) 본문
우테코 백엔드 프리코스는 어떻게 진행되는지 미리 경험해보기 위해 다른 블로그들을 보면서 관련 미션들을 해결해보기로 했다.
미션 - 숫자 야구 게임
- 미션은 기능 요구사항, 프로그래밍 요구사항, 과제 진행 요구사항 세 가지로 구성되어 있다.
- 세 개의 요구사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
여기서 나는 일단 제대로 실행이 되는지, 테스트 코드는 제대로 통과가 되는지가 궁금했어서 기능 단위로 커밋하지는 않았다.
기능 요구사항
기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.
- 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 포볼 또는 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
- 예) 상대방(컴퓨터)의 수가 425일 때
- 123을 제시한 경우 : 1스트라이크
- 456을 제시한 경우 : 1볼 1스트라이크
- 789를 제시한 경우 : 낫싱
- 예) 상대방(컴퓨터)의 수가 425일 때
- 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
- 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
- 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
- 아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.
구현할 기능 목록
- Application
- 숫자 야구 게임 시작
- IllegalArgumentException 이 발생한 경우
- IllegalArgumentException 을 발생시킨 후 애플리케이션 종료
- Game
- 게임 실행
- 3자리 수 입력에 대한 결과 출력
- 스트라이크, 볼, 낫싱
- 3자리 수 모두 맞히면 게임 종료
- 게임 종료 이후 게임 재시작 여부 확인
- Computer
- 3자리 난수 생성
- 재시작된 게임의 경우 3자리 난수 재생성
- 3자리 난수 생성
- Player
- 3자리 숫자 및 게임 재시작 여부 입력
- 입력값 유효성 확인 및 예외 처리
- 3자리 숫자 및 게임 재시작 여부 입력
요구 사항
- 프로그램을 실행하는 시작점은 Application의 main()이다.
- JDK 8 버전에서 실행 가능해야 한다. JDK 8에서 정상 동작하지 않을 경우 0점 처리한다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.
프로그래밍 요구사항 - Randoms, Console
- JDK에서 기본 제공하는 Random, Scanner API 대신 camp.nextstep.edu.missionutils에서 제공하는 Randoms, Console API를 활용해 구현해야 한다.
- Random 값 추출은 camp.nextstep.edu.missionutils.Randoms의 pickNumberInRange()를 활용한다.
- 사용자가 입력하는 값은 camp.nextstep.edu.missionutils.Console의 readLine()을 활용한다.
- 프로그램 구현을 완료했을 때 src/test/java 디렉터리의 ApplicationTest에 있는 모든 테스트 케이스가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
이외에 입출력 요구사항, 과제 진행 요구사항 등은 우테코 깃허브에서 확인이 가능하다.
WARNING: An illegal reflective access operation has occurred
먼저 JDK 8을 사용하라는 요구사항 때문에 Mac OS 자바 버전 여러 개 관리하기 글에서 작성한 것처럼 바로 JDK 변경이 가능한 맥북에서 작업을 시작했다. 그런데 프로그램을 작성한 뒤에 실행하니 다음과 같은 에러가 발생했다.
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by camp.nextstep.edu.missionutils.Console (file:/Users/deokjae/.gradle/caches/modules-2/files-2.1/com.github.woowacourse-projects/mission-utils/1.0.0/dad5230ec970560465a42a1cade24166e6a424f4/mission-utils-1.0.0.jar) to field java.util.Scanner.sourceClosed
WARNING: Please consider reporting this to the maintainers of camp.nextstep.edu.missionutils.Console
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
터미널에서 java -version
명령어를 실행해도 JDK 8 버전으로 정상적으로 출력됐지만 위와 같은 에러가 발생한 이유는 IntelliJ에서 관련 설정을 진행하지 않았기 때문이다. 기존 인텔리제이 설정으로는 JDK 11 버전을 사용하는 것으로 인식하기 때문에 이러한 문제가 발생한 것이다. 다른 블로그의 글을 보고 해당 내용을 수정하여 해결하였다.
나는 이미 openjdk 1.8 버전을 설치해뒀기 때문에 IntelliJ에서 따로 설치하지 않고, 다음과 같이 세팅을 진행했다.
먼저 Gradle JVM의 버전을 위와 같이 변경하고,
Project Structure에서 위와 같이 변경하였다.
Application.java
package baseball;
public class Application {
public static void main(String[] args) {
try {
Game.getGame().start();
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
}
사실상 삽질에 가장 많은 시간을 사용한 코드이다. 그 이유는 아래 테스트 코드를 보면 알 수 있다.
ApplicationTest.java
package baseball;
import camp.nextstep.edu.missionutils.test.NsTest;
import org.junit.jupiter.api.Test;
import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest;
import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ApplicationTest extends NsTest {
@Test
void 게임종료_후_재시작() {
assertRandomNumberInRangeTest(
() -> {
run("246", "135", "1", "597", "589", "2");
assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
},
1, 3, 5, 5, 8, 9
);
}
@Test
void 예외_테스트() {
assertSimpleTest(() ->
assertThatThrownBy(() -> runException("1234"))
.isInstanceOf(IllegalArgumentException.class)
);
}
@Override
public void runMain() {
Application.main(new String[]{});
}
}
기능 요구사항을 보면 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다고 되어있고, 이에 대한 모든 조건에 대해 예외 처리를 마쳤다. 실제로 실행해보면 예외 상황에 적절히 IllegalArgumentException이 발생하며 프로그램이 종료된다.
여기서 예외 테스트를 보면 "1234"를 입력하면 IllegalArgumentException이 발생해야 한다고 되어있지만 실제로 실행해보니 해당 테스트만 실패가 뜨는 것이다. 원인은 아래와 같다.
try {
Game.getGame().start();
} catch (IllegalArgumentException e) {
e.printStackTrace();
System.exit(1);
}
기존 Application의 main 메서드에 있던 내용이고, 이는 IllegalArgumentException에 대한 단계별 에러 내용을 출력한 뒤에 프로그램을 종료하며, 이는 기능 요구사항에 만족하는 듯한 내용이다. 하지만 테스트 코드에서는 위의 catch문에서 IllegalArgumentException이 throw 되길 원했던 것이다. 아무튼 이 부분에서 삽질을 오래 했다.
Game.java
package baseball;
import java.util.*;
public class Game {
private static Game game = null;
private Computer computer = Computer.getComputer();
private Player player = Player.getPlayer();
private Game() {
}
public static Game getGame() {
if (game == null) {
game = new Game();
}
return game;
}
public void start() {
List<Integer> computerNumbers = computer.getRandomNumbers();
while (true) {
System.out.print("숫자를 입력해주세요 : ");
List<Integer> playerNumbers = player.getInputNumbers();
if (compareNumbers(computerNumbers, playerNumbers)) {
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
String replayInput = player.getReplayInput();
if (replayInput.equals("1")) {
computer.resetRandomNumbers();
computerNumbers = computer.getRandomNumbers();
} else if (replayInput.equals("2")) {
break;
}
}
}
}
public boolean compareNumbers(List<Integer> computerNumbers, List<Integer> playerNumbers) {
int strike = 0, ball = 0;
for (int i = 0; i < computerNumbers.size(); i++) {
if (computerNumbers.get(i).equals(playerNumbers.get(i))) {
strike++;
} else if (computerNumbers.contains(playerNumbers.get(i))) {
ball++;
}
}
StringBuilder sb = new StringBuilder();
if (strike == 0 && ball == 0) {
sb.append("낫싱");
} else {
if (ball > 0) {
sb.append(ball).append("볼 ");
}
if (strike > 0) {
sb.append(strike).append("스트라이크");
}
}
System.out.println(sb);
if (strike == 3) {
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
return true;
} else {
return false;
}
}
}
게임은 여러번 실행될 수 있지만, 하나의 Application 코드 안에서 반복적으로 실행되기 때문에 하나 이상의 인스턴스가 필요하지 않다. 그래서 싱글턴(Singleton) 패턴을 적용했으며, 같은 이유로 아래에 작성할 Computer와 Player 모두 싱글턴 패턴을 적용했다.
start 메서드에서 Computer가 맞춰야할 3개의 랜덤 숫자 리스트를 만들면 Player가 이를 맞출 때까지 3자리 숫자를 입력하여 compareNumbers 메서드를 통해 대조하게 된다.
3개의 숫자를 모두 맞히게 되면 게임이 종료되며, Player는 게임의 재시작 여부를 입력하게 된다. 재시작을 하게 된다면 Computer의 랜덤 숫자 리스트를 초기화하고, 위의 로직을 반복한다.
Computer.java
package baseball;
import camp.nextstep.edu.missionutils.Randoms;
import java.util.*;
public class Computer {
private static Computer computer = null;
private List<Integer> randomNumbers;
private Computer() {
randomNumbers = generateRandomNumbers();
}
public static Computer getComputer() {
if (computer == null) {
computer = new Computer();
}
return computer;
}
private List<Integer> generateRandomNumbers() {
List<Integer> list = new ArrayList<>();
while (list.size() < 3) {
int num = Randoms.pickNumberInRange(1, 9);
if (!list.contains(num)) {
list.add(num);
}
}
return list;
}
public List<Integer> getRandomNumbers() {
return randomNumbers;
}
public void resetRandomNumbers() {
randomNumbers = generateRandomNumbers();
}
}
Player.java
package baseball;
import camp.nextstep.edu.missionutils.Console;
import java.util.*;
public class Player {
private static Player player = null;
private List<Integer> inputNumbers;
private Player() {
inputNumbers = new ArrayList<>();
}
public static Player getPlayer() {
if (player == null) {
player = new Player();
}
return player;
}
private void validateInputNumbers(String inputNumbers) {
List<String> inputNumbersList = Arrays.asList(inputNumbers.split(""));
if (inputNumbersList.size() != 3) {
throw new IllegalArgumentException("입력한 숫자의 개수가 올바르지 않습니다. 게임 종료");
}
if (inputNumbers.chars().anyMatch(ch -> !Character.isDigit(ch))) {
throw new IllegalArgumentException("숫자가 아닌 값이 포함되어 있습니다. 게임 종료");
}
if (inputNumbersList.stream().distinct().count() != 3) {
throw new IllegalArgumentException("중복된 숫자가 있습니다. 게임 종료");
}
}
public List<Integer> getInputNumbers() {
String input = Console.readLine();
validateInputNumbers(input);
inputNumbers.clear();
Arrays.stream(input.split("")).forEach(number -> inputNumbers.add(Integer.parseInt(number)));
return inputNumbers;
}
private void validateReplayInput(String replayInput) {
if (!replayInput.equals("1") && !replayInput.equals("2")) {
throw new IllegalArgumentException("1 또는 2를 입력해야 합니다. 게임 종료");
}
}
public String getReplayInput() {
String replayInput = Console.readLine();
validateReplayInput(replayInput);
return replayInput;
}
}
사용자는 3자리 숫자만 입력해야 하며, 해당 숫자는 중복될 필요가 없다. 또한, 입력한 내용에 숫자가 아닌 다른 글자가 들어간다면 위의 Game.compareNumbers 메서드에서 에러가 발생할 것이다. 이를 방지하기 위해 validateInputNumbers 메서드에서 IllegalArgumentException을 발생시킨다.
또한 게임의 재시작 여부에 대해 입력할 때 1 또는 2가 아니라면 IllegalArgumentException를 발생시키도록 validateReplayInput 메서드를 작성하였다.
결론
마지막으로 봤던 코테를 망치고 면접을 앞두고 있는데, 면접에서 코테 결과를 뒤집을 생각은 못 하고 정신없이 '우테코 프리코스는 어떻게 되는 거지?' 하면서 미루고 미루던 1번째 미션을 해결했다. 그래도 내일은 면접 준비 간단하게나마 해야지..
지금도 글을 작성하면서 보완할 부분이 눈에 보이는데.. 최대한 빠른 시일 내로 리팩터링을 진행해야겠다는 생각이 들었다. 간단하게 보완만 하고 코드를 좀 더 세분화하는 것은 미룰까 한다.
'StudyNote' 카테고리의 다른 글
우테코 백엔드 프리코스 체험해보기 2(자동차 경주 게임) (0) | 2025.01.14 |
---|---|
다사다난했던 2024년도 후기 (3) | 2024.12.31 |
SSAFY 13기 지원부터 SW 적성 진단까지 (2) | 2024.11.27 |
2024학년도 제 21회 소프트웨어융합학과 학술제 후기 (2) | 2024.11.13 |
어댑터(Adapter) 패턴 (1) | 2024.11.02 |