Intro.
11/02 ~ 11/08 : 프리코스 2주차 숫자 야구 미션을 완료했습니다.
https://github.com/woowacourse-precourse/java-baseball
사실 숫자야구 게임은 너무 유명한 게임이고,
중고등학생 시절에도 친구들과 즐겨 했었기에 룰은 아주 잘 알고 있었습니다.
언뜻 보니 기능 구현 자체가 어렵다기 보다는, 클래스들을 잘 분리하여 각 객체의 책임에 맞는 역할을 부여하는 것이 가장 중요할 것 같다고 생각했습니다.
저는 이번 2주차의 목표를 다음과 같이 정하고 시작하였습니다.
1. 클래스를 잘 분리하고 적절한 책임과 명확한 역할을 부여하도록 노력하자.
2. 가독성 좋은 코드와, 보다 더 직관적인 변수와 함수 이름을 짓도록 노력하자.
3. 메서드의 indentation을 1로 유지하도록 노력하자.
- domain : 도메인 클래스를 담고 있는 패키지
- Balls : 게임에 참여하는 유저들(플레이어, 컴퓨터)의 볼 정보를 관리하는 도메인 클래스
- Computer : 게임에 참여하는 컴퓨터를 위한 도메인 클래스
- Player : 게임에 참여하는 플레이어를 위한 도메인 클래스
- Result : 게임의 결과를 관리하는 도메인 클래스
- input : 입력을 담당하는 패키지
- InputFilter : 입력된 값을 처리하는 클래스
- InputReader : 입력값을 받아 전달하는 클래스
- output : 출력을 담당하는 패키지
- OutputViewer : 출력값을 콘솔창에 출력하는 클래스
- utils : 유틸성 클래스들을 담고 있는 패키지
- Command(Enum) : 플레이어의 게임 재시작 여부를 열거형으로 관리하는 클래스
- CustomNumberGenerator : 플레이어에게 숫자를 받아 InputFilter을 통해 가공하여 List<Integer>의 형태로 넘겨주도록 하는 클래스
- RandomNumberGenerator : 랜덤으로 숫자를 생성하여 List<Integer>의 형태로 넘겨주는 클래스
- Application : 어플리케이션의 시작을 담당하는 클래스
- GameManager : 게임의 전반적인 흐름을 관리하는 매니저 클래스
최종적으로 숫자 야구 게임의 패키지 구조를 다음과 같이 설계할 수 있었습니다.
먼저 input(입력)을 받는 부분과 output(출력)을 받는 부분을 도메인 코드들과 따로 분리하였습니다.
또, input에서도 입력받은 값을 처리하는 코드는 서로 다른 역할을 하는 것이라 생각했습니다.
따라서, 입력을 받는 InputReader와 입력받은 값을 가공(처리)하는 InputFilter 클래스를
따로 두어 관리하도록 하였습니다.
개발 과정
우아한테크코스에서 최대한 기능을 작게 분리하되 핵심적인 기능부터 구현해 나가고, 테스트를 작성하여 잘 작동하는 지 확인해 보라는 피드백을 받고 나서, 가장 먼저 진행한 것이 컴퓨터와 플레이어의 볼을 만드는 것이었습니다.
package baseball.utils;
import camp.nextstep.edu.missionutils.Randoms;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RandomNumberGenerator {
public static final int MAX_SIZE_OF_NUMBERS = 3;
public static final int MIN_VALUE_OF_NUMBER = 1;
public static final int MAX_VALUE_OF_NUMBER = 9;
public List<Integer> generate() {
Set<Integer> randomNumbers = new HashSet<>();
while (isNotFull(randomNumbers)) {
int randomNumber = generateRandomNumber();
randomNumbers.add(randomNumber);
}
return new ArrayList<>(randomNumbers);
}
private boolean isNotFull(Set<Integer> randomNumbers) {
return randomNumbers.size() < MAX_SIZE_OF_NUMBERS;
}
private int generateRandomNumber() {
return Randoms.pickNumberInRange(MIN_VALUE_OF_NUMBER, MAX_VALUE_OF_NUMBER);
}
}
RandomNumber리스트를 만드는 코드는 다음과 같이 개발하였습니다.
컴퓨터와 사용자의 숫자를 정할 때, 세 가지의 예외사항을 지켜서 처리해야 합니다.
1. 세 자리 숫자여야 한다.
2. 서로 다른 숫자로 이루어진 수여야 한다.
3. 1~9 사이의 숫자로만 이루어진 수여야 한다.
해당 코드에서
isNotFull 메서드를 통해 숫자의 길이가 3보다 작은지에 대한 검증이 이루어지도록 했고
우아한테크코스의 Randoms.pickNumberInRange() 메서드를 이용하여 1~9 사이의 랜덤값을 생성하도록 하였습니다.
또, HashSet 자료구조를 이용하여 중복값은 자연스럽게 저장되지 않도록 하여 세 가지 예외사항을 지킬 수 있었습니다.
package baseball.utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class RandomNumberGeneratorTest {
final RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator();
public static final int SIZE_OF_NUMBERS = 3;
@Test
@DisplayName("랜덤으로 선택한 숫자의 길이가 적합한지 테스트")
void checkRandomNumbersSize() {
assertThat(randomNumberGenerator.generate().size()).isEqualTo(SIZE_OF_NUMBERS);
}
@Test
@DisplayName("랜덤으로 선택한 숫자들의 중복값 존재 여부 테스트")
void checkRandomNumbersDuplication() {
assertEquals(SIZE_OF_NUMBERS, randomNumberGenerator.generate()
.stream()
.distinct()
.count());
}
}
이어서 다음과 같이 테스트를 구성하였습니다. 예외사항에 저촉되지 않고 잘 작동하는지 검증하기 위한
테스트를 작성했습니다. 테스트를 작성하는 것도 처음에는 어떻게 해야 할지 막막했지만,
https://www.baeldung.com/introduction-to-assertj
https://junit.org/junit5/docs/current/user-guide/#writing-tests
AssertJ와 JUnit5의 문서를 ㅅ위주로 참고해가며 이해가 잘 가지 않는 부분은 구글링을 통해서
다양한 테스트 코드를 접하며 점차 익숙해질 수 있었습니다.
사용자의 숫자를 정할 때에는, 사용자에게 직접 숫자를 입력 받야아 합니다.
하지만 입력을 받고 처리하는 로직이 Player 도메인 클래스 내부에 위치하면 그 역할과 비중이 너무 커진다고 생각했습니다. 따라서 별도의 패키지를 만들어 입출력과 도메인 클래스를 따로 분리시키는 방식으로 설계했습니다.
또, 사용자에게 재시작 여부를 입력받는 부분을 Enum자료형을 이용하여 구현하였습니다.
package baseball.utils;
public enum Command {
CONTINUE("1"),
STOP("2");
private final String commandKey;
Command(String commandKey) {
this.commandKey = commandKey;
}
public static Command of(String input) {
for (Command command : values()) {
if (checkCommand(command, input)) {
return command;
}
}
throw new IllegalArgumentException("올바르지 않은 형식의 재시작 커맨드를 입력하셨습니다.");
}
private static boolean checkCommand(Command command, String input) {
return command.commandKey.equals(input);
}
사실 요구사항에는 두 가지 커맨드밖에 없어 if문을 이용해서 처리할 수도 있지만,
추후에 확장 가능성을 함께 고려한다면 Enum을 이용해 상수값으로 관리하는 것이
더욱 효율적인 방법이라고 생각해서 위와 같이 구현했습니다.
피드백 반영
이번 2주차 피드백에서 가장 와 닿았던 것은 무엇보다도 기능 목록에 대한 피드백이었습니다.
지난 한 주의 개발과정을 돌이켜 보면, 저는 처음부터 무턱대고 완벽한 설계만을 지향했던 것 같습니다.
하지만 요구사항을 정리하기 위한 시간을 많이 투자했을 뿐더러 이렇게 하더라도 개발하는 과정에서 리팩토링이
필요한 부분들이 끊임없이 생겨난다는 것을 깨닫게 되었습니다.
'살아있는 문서를 만들라' 라는 표현이 참 인상깊습니다. 앞으로는 기능 구현을 거듭하며 필요에 따라 문서도 유동적으로 고쳐 나가는 방향으로 개발해야겠다고 다짐하게 되었습니다.
또 변수명에 자료형이나 자료구조를 사용하지 않아야 한다는 것도 새로 배울 수 있었습니다.
https://github.com/wonjunYou/java-baseball/commit/ad751a86e463a030e0abae63c2dbe2ed5b210cfa
사용자가 커맨드 값을 입력한 스트링 값의 변수명을 commandString이라고 표현했었는데, 해당 피드백을 반영하여
commandKey라는 변수명으로 수정했습니다.
또 클래스에서 상수, 멤버변수, 생성자, 메서드 순으로 작성하는 것 또한 당연히 지켜야 하는 컨벤션이지만,
가끔 코드를 작성하다 보면 실수하는 경우가 있어 더욱 주의해야 겠습니다.
마치며
어느덧 우아한 테크코스 프리코스의 절반의 고지를 넘어 계속 달려가고 있습니다.
한 주씩 거듭할수록 점점 성장하는 것을 느낄 수 있었고, 더욱 나은 코드에 대한 고민은 꼬리에 꼬리를 물듯
생겨나고 있는 상태입니다. 3주차도 이번 주보다 한 걸음 더 나아갈 수 있는 밑거름이 되었으면 하는 바램입니다!
'Daily > 회고' 카테고리의 다른 글
[회고] 대학생 미팅 서비스 - weave 프로젝트 회고 (3) | 2024.11.17 |
---|---|
[우아한테크코스] 프리코스 1주차 온보딩 미션을 마치며 (0) | 2022.11.01 |
2022 2분기 회고, 요새 드는 생각들 (1) | 2022.07.14 |