본 포스팅은 조금 긴 이야기가 될 것 같다. 꾸준히 공부를 해왔음에도 불구하고 부족한 부분들이 많이 보였기에, 1주차 프리코스를 하며 느꼈던 점들을 모두 적어보려 한다. 새롭게 알게 된 부분들은 따로 포스팅 해 분리했지만 그래도 길다,, 난 투머치토커인가?🤣
✔️ 제시된 요구사항 목록
🚀 기능 요구 사항
기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.
- 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
- 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게임 플레이어는 컴퓨터가 생각하고 있는 서로 다른 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
- 이와 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
- 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
🎯 프로그래밍 요구 사항
- JDK 17 버전에서 실행 가능해야 한다. JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.
- 프로그램 실행의 시작점은 Application의 main()이다.
- build.gradle 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
- Java 코드 컨벤션 가이드를 준수하며 프로그래밍한다.
- 프로그램 종료 시 System.exit()를 호출하지 않는다.
- 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
라이브러리
- camp.nextstep.edu.missionutils에서 제공하는 Randoms 및 Console API를 사용하여 구현해야 한다.
- Random 값 추출은 camp.nextstep.edu.missionutils.Randoms의 pickNumberInRange()를 활용한다.
- 사용자가 입력하는 값은 camp.nextstep.edu.missionutils.Console의 readLine()을 활용한다.
사용 예시
List<Integer> computer = new ArrayList<>();
while (computer.size() < 3) {
int randomNumber = Randoms.pickNumberInRange(1, 9);
if (!computer.contains(randomNumber)) {
computer.add(randomNumber);
}
}
✏️ 과제 진행 요구 사항
- 미션은 java-baseball-6 저장소를 Fork & Clone해 시작한다.
- 기능을 구현하기 전 docs/README.md에 구현할 기능 목록을 정리해 추가한다.
- 과제 진행 및 제출 방법은 프리코스 과제 제출 문서를 참고한다.
✔️ 이전 코드
아래 코드는 갈아엎기 전 구현했던 코드이다. 머릿속이 복잡했던 터라 일단 대략적인 기능 구현부터 해보자!! 라는 생각으로 구현에만 집중했다.(1주차 시작하기 전 했던 다짐들은 다 어디로 갔지,,) 어느 정도 구현을 하고보니 아래와 같은 문제점들이 있었다.
1. 게임 재시작이 1번밖에 안된다.
2. 3스트라이크일 경우 3볼 3스트라이크로 출력된다.
이외의 사이드 이펙트가 다양하게 있을 것으로 예상된다.(제대로 기능 구현이 되지 않았다.)
내가 직접 구현한 코드이지만 보면서 "이건 clean code가 아니라 dirty code인데,,,?" 라는 생각이 들어 헛웃음이 났다. 불필요하게 반복되는 코드가 많이 보였고 객체지향적인 설계라는 생각이 도무지 들지 않는다. 너 도대체 뭘한거니?
package baseball;
import camp.nextstep.edu.missionutils.Console;
import camp.nextstep.edu.missionutils.Randoms;
import java.util.ArrayList;
import java.util.List;
public class Application {
static String player;
static String computer;
public static void main(String[] args) {
System.out.println("숫자 야구 게임을 시작합니다.");
gameProcess();
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
String gameStatus = Console.readLine();
switch (gameStatus) {
case "1":
gameProcess();
case "2":
break;
}
}
private static void gameProcess() {
computer = randomComputerNumber();
System.out.println(computer);
while (!computer.equals(player)) {
System.out.print("숫자를 입력해주세요 : ");
player = Console.readLine();
if (player.length() != 3) {
throw new IllegalArgumentException();
}
String result = getResultToString(computer, player);
System.out.println(result);
}
}
public static String randomComputerNumber() {
StringBuilder computer = new StringBuilder();
while (computer.length() < 3) {
int randomNumber = Randoms.pickNumberInRange(1, 9);
String randomNumberToString = String.valueOf(randomNumber);
if (!computer.toString().contains(randomNumberToString)) {
computer.append((randomNumber));
}
}
return computer.toString();
}
public static int checkBallCount(String computer, String player) {
String number;
int ball = 0;
for (int i = 1; i < computer.length() + 1; i++) {
number = player.substring(i - 1, i);
if (computer.contains(number)) {
ball++;
}
}
return ball;
}
public static int checkStrikeCount(String computer, String player) {
String number, computerNumber;
int strike = 0;
for (int i = 1; i < computer.length() + 1; i++) {
number = player.substring(i - 1, i);
computerNumber = computer.substring(i - 1, i);
if (computerNumber.equals(number)) {
strike++;
}
}
return strike;
}
public static String getResultToString(String computer, String player) {
String result = "";
int ball = checkBallCount(computer, player);
int strike = checkStrikeCount(computer, player);
if (ball == 0 && strike == 0) {
result = "낫싱";
if (ball != 0) {
result += ball + "볼";
}
if (strike != 0 && strike < 3) {
result += strike + "스트라이크";
}
if (strike == 3) {
result = "3스트라이크";
}
return result;
}
return result;
}
}
✔️ 그래서 어떻게 할건데? 🤔
코드를 갈아엎기로 결정했다. 커뮤니티에서 많은 실력자분들이 양질의 정보를 많이 공유해주셨다. 클린 코드, MVC 패턴, TDD 등 기존에 알고 있던 부분들과 새롭게 알게 된 부분들이 머릿속에 복잡하게 엉켜버리기 시작했다. 아마 마음은 조급하고 "이건 뭐지? 저건 뭐지? 다 알고싶다!!!!!!!"라는 생각때문이였던 것 같다.
그래서 처음부터 다시 천천히 설계해보며 머릿속의 실마리들을 하나씩 풀어보기로 결정했다. 우선 게임 프로세스를 다시 정리하고, 기능과 클래스를 설계했다. 이렇게 자세해도 되나 싶을 정도로 자세히 정리했다. 내가 생각했을 때 발생할 수 있는 모든 예외상황도 함께 정리해보았다. 설계에 맞게 기능을 먼저 구현해 테스트 코드를 통과하고, 리팩토링은 이후에 해도 충분하다는 생각이 들었다. 하지만 위 코드처럼 너무 중구난방으로 구현하는 것보다는 그동안 Spring MVC를 공부하면서 배웠던 MVC 패턴을 적용해 구현하기로 결정했다.
MVC 패턴은 다음 영상을 통해 한번 더 머릿속에 정리했다.
https://www.youtube.com/watch?v=ogaXW6KPc8I
(1) Model은 Controller와 View에 의존하지 않아야 한다.
(Model 내부에 Controller와 View에 관련된 코드가 있으면 안된다.)
(2) View는 Model에만 의존해야 하고, Controller에는 의존하면 안된다.
(View는 Model에만 의존해야 하고, Controller에는 의존하면 안된다.)
(3) View가 Model로부터 데이터를 받을 때는, 사용자마다 다르게 보여줘야 하는 데이터에 대해서만 받아야 한다.
(4) Controller는 Model과 View에 의존해도 된다.
(5) View가 Model로부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다.
✔️ 게임 프로세스 요약 📝
1. 컴퓨터가 1과 9 사이의 랜덤 숫자를 생성한다.
🚫 예외상황 1-1) 숫자가 중복되지 않아야 한다.
2. 사용자가 1과 9 사이에 있는 길이가 3인 숫자를 입력한다.
🚫 예외상황 2-1) 숫자가 중복되지 않아야 한다.
🚫 예외상황 2-2) 숫자의 길이는 3이어야 한다.
🚫 예외상황 2-3) 숫자는 1과 9 사이의 수이어야 한다.
3. 컴퓨터의 숫자와 사용자의 숫자를 비교한다.
- 3-1. 같은 숫자가 있고 자리가 다르면 볼 카운팅
🚫 예외상황 3-1-1) "볼은 스트라이크다. 스트라이크는 볼이다."는 둘 다 성립되지 않는다.
-> 숫자가 볼에 해당할 때 스트라이크는 카운팅되지 않아야 한다. 따라서 숫자는 포함되어 있지만 자리가 다른 것을 확인해야 한다.
- 3-2. 같은 숫자가 있고 자리도 같으면 스트라이크 카운팅
🚫 예외상황 3-2-1) "볼은 스트라이크다. 스트라이크는 볼이다."는 둘 다 성립되지 않는다.
-> 숫자가 스트라이크에 해당할 때 볼은 카운팅되지 않아야 한다. 따라서 자리와 숫자 모두 같은 것을 확인해야 한다.
- 3-3. 같은 숫자가 아예 없으면 낫싱(스트라이크 카운트와 볼 카운트 모두 0일 때)
4. 3 스트라이크면 게임이 클리어된다.
5. 사용자에게 새로 시작할지, 종료할지에 대한 여부를 묻는다.
🚫 예외상황 5-1) 1과 2만 입력할 수 있다.
- 5-1. 새로 시작하면 처음부터 다시 시작
- 5-2. 종료하면 그대로 종료
✔️ 기능 목록 🚀
1. 컴퓨터 숫자 랜덤 생성 기능
2. 카운트 계산 기능
- 볼 카운트 계산
- 스트라이크 카운트 계산
3. 결과 메시지 반환 기능
- 볼, 스트라이크, 낫싱
4. 프로그램 종료 조건 검증 기능
- 재시작, 종료
✔️ 클래스 설계 📚
조금 헷갈렸던 점은 사용자로부터 입력받는 부분을 Controller에서 처리할지 View에서 처리할지였다. Spring MVC를 공부하며 사용자로부터 입력받은 데이터는 Controller에서 PostMapping을 통해 받아와 DTO를 이용해 데이터를 전달하였다. 이 점을 자꾸 생각하다보니 헷갈렸지만 결론적으로는 InputView 클래스를 따로 만들었다. 얼른 다른 분들과의 코드 리뷰를 통해 다양한 피드백을 받고싶다
- constant
- Constant : 상수를 관리하는 객체
- message
- CountMessage : 카운트 메시지와 관련된 객체
- getMessage() : 메시지를 반환하는 함수
- toString() : 메시지를 연결해 반환하는 함수
- CountMessage : 카운트 메시지와 관련된 객체
- validation
- NumberValidation : 주어진 입력이 올바른지 검증하는 객체
- validationAll() : 플레이어가 입력한 값을 전체 검증하는 함수
- validateDuplicate() : 중복을 검증하는 함수
- validateLength() : 길이를 검증하는 함수
- validateRangeAndType() : 1과 9 사이의 수인지 검증하는 함수
- validateIsInArray() : 해당 숫자가 배열 안에 있는지 검증하는 함수
- validateComputerDuplicate() : 컴퓨터 랜덤 숫자가 중복되었는지 검증하는 함수
- validateGameStatus() : 주어진 입력값이 1 또는 2인지 검증하는 함수
- NumberValidation : 주어진 입력이 올바른지 검증하는 객체
- service
- ProgressService : 게임 진행을 돕는 객체
- resetGame() : 게임을 초기화하는 함수
- getComputerNumber() : 컴퓨터의 3개의 숫자 모두 중복되지 않을 때까지 생성을 반복하는 함수
- createComputerNumber() : 컴퓨터의 랜덤 숫자를 1자리씩 생성하는 함수
- getResultMessage() : 결과 메시지를 반환하는 함수
- isGameClear() : 게임 클리어인지 판별하는 함수(3스트라이크인지 확인)
- getResultGameStatus() : 게임 재시작 또는 종료 여부를 반환하는 함수
- checkGameStatus() : 게임 종료 조건을 판별하는 함수
- CountService : 사용자가 입력한 숫자에 대한 볼/스트라이크 카운트를 계산하는 객체
- resetCount : 저장된 카운트를 초기화하는 함수
- getResultCount() : 결과 카운트를 반환하는 함수
- getBallCount() : 볼 카운트를 반환하는 함수
- getStrikeCount() : 스트라이크 카운트를 반환하는 함수
- isNothing() : 낫싱인지 판별하는 함수
- resetCount : 저장된 카운트를 초기화하는 함수
- ProgressService : 게임 진행을 돕는 객체
- controller
- GameProgress : 게임 진행을 위한 객체
- getInput() : 플레이어로부터 숫자를 입력받고 검증하는 함수
- startGame() : 게임을 시작하는 함수
- restartGame() : 게임을 재시작하는 함수
- getInput() : 플레이어로부터 숫자를 입력받고 검증하는 함수
- GameProgress : 게임 진행을 위한 객체
- view
- InputView : 플레이이로부터 입력을 받기위한 객체
✔️ 구현 🛠️
디버깅을 몇 번이나 했던가. 날이 밖아올 때쯤 드디어 테스트 코드를 통과했다!!! 모두가 자고있던 아침 6시, 고요한 침묵 속에서 조용히 주먹을 불끈 쥐며 마음속으로 "아싸!!"를 외쳤다.
이 맛에 코딩하지. 역시 문제를 해결했을 때 오는 희열감은 힘들었던 시간들을 모두 잊게 해준다ㅎㅎ
구현하는 내내 도메인 분리에 대한 미련을 놓지 못했다. Spring MVC를 할 때는 DTO를 만들어 엔티티를 Builder를 이용해 DTO화 해준 이후 데이터를 전달했는데, 이번 과제에 적용하기에는 너무 불필요하게 많은 클래스가 생기는 것 같다는 생각이 들었다. getter/setter를 사용하지 않고 외부 라이브러리 사용없이 어떻게 해결하지? 자료를 열심히 찾아보던 중 Tecoble에 올라와 있는 글을 발견하였다. 발견하자마자 바로 '함께 나누기'에 공유하였다.
"getter를 사용하는 대신 객체에 메시지를 보내자."
https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/
getter를 사용하는 대신 객체에 메시지를 보내자
getter는 멤버변수의 값을 호출하는 메소드이고, setter는 멤버변수의 값을 변경시키는 메소드이다. 자바 빈 설계 규약에 따르면 자바 빈 클래스 설계 시, 클래스의 멤버변수의 접근제어자는 private
tecoble.techcourse.co.kr
https://www.slipp.net/questions/565
getter 메소드를 사용하지 않도록 리팩토링한다.
오늘 코드 리뷰 주제는 지난 번에 이어 다음 그림과 같은 볼링 게임 점수판을 구현하는 프로그래밍의 일부이다. 볼링 게임을 구현하면서 쓰러진 볼링 핀을 관리하는 클래스를 Pins 클래스로 다음
www.slipp.net
"위 블로그들을 참고해 구현하면 되겠다!" 라고 생각했지만 솔직히 꽤나 어려워서 5번 넘게 정독했다. 최대한 이해하고 참고하려 노력했다. 하지만 도메인을 만들 때 Computer 객체에서 컴퓨터 랜덤 숫자를 생성하고, Player 객체에서 사용자로부터 입력을 받도록 구현해 실행하니 시간 초과가 떴다. 결론적으로 도메인 분리에는 실패했다. 이유가 무엇일까 생각해보았는데 생성자 시점에서 저장을 한 후 따로 메소드를 만드는 방향으로 가야했던 것 같다. 위 부분들 모두 조금 더 공부해 다음 미션 때는 꼭 도메인 분리에 성공하고 싶다.
최대한 각 클래스의 책임이 분리되어 있고 역할이 명확히 보일 수 있도록 구현하려고 노력했다. 주요 비즈니스 로직은 service 계층에서 구현하였다. Constant 클래스나 View 클래스와 같은 간단한 부분들을 먼저 구현하고 이후 service, controller 계층에만 집중하니 훨씬 몰입이 잘 되었다.
PR 주소
✔️ 어려웠던 점, 새롭게 알게 된 점 💡
이번 우아한테크코스를 참여하며 가장 어려웠던 점은 객체지향적인 설계와 커밋 주기였다. 새롭게 알게 된 점은 일급컬렉션, 람다 스트림, 커밋 컨벤션이다. 커밋 주기는 아직 정답을 찾지 못한 것 같다. 커뮤니티의 토론하기에 올라온 글을 계속해서 봤지만 답이라는게 정해져 있지 않은 것 같다. 커밋 컨벤션에 대해 공부해 조금은 감이 잡혔지만, 이 부분은 조금 더 고민해봐야 할 것 같다. 이외의 사항들은 이미 포스팅이 너무 길어 따로 정리해두었다.
1. 일급컬렉션
2. 람다 스트림
3. 커밋 컨벤션
✔️ 1주차를 마치며
1주차를 끝내고 돌이켜보니 아직 많이 부족하다는 것을 깨달았다. Java에 대한 이해도를 높이는 것이 중요하다고 생각했고, Java의 버전이 업그레이드 될 때마다 각 문서들을 살펴보며 새롭게 생겨난 기술이 무엇인지 공부해야겠다고 다짐했다. 또한, 무작정 구현을 시작하기보다는 기능을 상세히 명시하는 것이 가장 중요하다는 생각이 들었다. 난이도가 많이 높은 편이 아님에도 불구하고, 맨 처음 구현했던 코드는 내가 봐도 알아보기 어렵고 비효율적이였기 때문이다. 얼른 다른 분들과 함께 코드 리뷰를 주고받으며 피드백을 받고싶다!!!🤩
코딩을 하다보면 글쓰기와 많이 비슷하다는 생각이 든다. 나는 글을 쓸 때 단락별로 대략적인 주 내용을 정리하고, 마음에 들 때까지 수십 번을 보며 퇴고를 하는 편이다. 사실 이 글도 최소 5번은 퇴고를 한다ㅎㅎ 이쯤되면 퇴고를 좋아하는 것일지도 모른다.🫢 여튼 코딩도 마찬가지이지 않을까? 메서드별로 주요 기능을 정리하고, 코드를 수십 번을 보며 디버깅으로 에러를 찾고 리팩토링하는 과정도 글을 퇴고하는 것과 같다고 생각한다.
앞으로 남은 3주도 포기하지 않고 끝까지 완수하고 싶다. 내 힘으로 문제를 해결하는 과정, 작은 성공을 맛보고나니 자신감이 생기기 시작한다. 우아한테크코스 프리코스의 본디 목적은 바로 이런 거겠지? 꼭 본 과정이 아니더라도 프리코스를 통해 충분히 성장할 수 있다는 걸 깨닫기 시작했다. 뿐만 아니라 다른 분들과 같은 목표를 바라보며 공유하고 소통하고 공감하는 과정이 무척 즐겁다.😆 본 과정은 얼마나 더 재미있을까? 남은 시간 최선을 다해 열심히 임해야겠다. 모두 파이팅!!!👊
소감문은 위 내용들을 요약해 제출했다! 학습과정을 잘 녹여내려 하다보니 약 1000자 정도,,,😂
'우아한테크코스 > 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기 프리코스] 2주차 코드 리뷰 종합해보기! (0) | 2023.11.06 |
---|---|
[우아한테크코스 6기 프리코스] 2주차 회고 (0) | 2023.11.01 |
[우아한테크코스 6기 프리코스] 1주차 코드 리뷰 종합해보기! (2) | 2023.10.27 |
[우아한테크코스 6기 프리코스] 1주차를 시작하기에 앞서 (2) | 2023.10.19 |
[우아한테크코스] 이전 기수 프리코스 1주차 문제 풀어보기 (0) | 2023.09.23 |