[Java] 우테코 프리코스 1주차 - 숫자야구 (1)
시작하며…
드디어 프리코스 1주차가 시작됐다. 걱정도 되면서 재밌을 것 같다는 생각에 몹시 설렌다!
취준을 하면서 즐거울 수 있는건 정말 개발자를 꿈꾸는 사람들의 특권인 것 같다. 열심히 준비해서 붙은 1차를 헛되이 하지 않도록
무조건 최종에 붙자. 불합을 의식하지 않고 이 과정을 즐거워하다 보면 어느새 합격해 있을 것이란 생각이 든다.
첫 미션은 어릴때 누구나 한번 쯤은 해봤을 숫자야구 게임이다. 기능적으로 보면 어렵지 않은 프로젝트라 구현 자체는
전혀 어려워 보이지 않지만, 우테코에서 원하는건 기능자체의 구현이 아닌 읽기 쉬운 코드, 아름다운 코드에 대해서 고민하고 연습하며 이로 인한 능력의 향상이다.
그동안 사실 방구석에서 혼자 코딩을 하다보니 의식적으로 예쁘게 코드를 짜야지! 하다가도 촉박한 시간이나 귀찮음에 밀려
다시 기능적으로만 작동하는 코드 작성에 그쳤는데 이제부터는 의식적인 노력과 연습을 통해 코드의 기능뿐만 아니라 가독성과 재사용성에 대해서도 고민해보며 코딩을 해야겠다는 생각이 들었다.
준비
프리코스가 시작하기 전에 프리코스를 잘해내기위한 준비로 어떤 걸 해야 할지 고민했다. 사실 프리코스 문제들은 모두 온라인에 공개가 되어있는 상태라 미리 대략 구현을 해볼 수는 있었지만,
과제가 주는 기간은 1주일 가량으로 구현하기에 전혀 부족한 시간이 아닌지라 굳이 먼저 풀어보고 싶지는 않았다. 그래서 그것보다는 어떤 코드가 깔끔한 코드인지, 아름다운 코드인지 미리 공부하고 과제를 시작해서 과제를 시작하다가 부랴 부랴 찾아 보기보다는 미리 알고있는 상태로 처음부터 코드를 작성하고 싶었다.
그래서 유튜브에 있는 우테코 캡틴이신 박재성님의 강의를 정독 했다.
확실히 영상으로 보니 이래서 클린 코드가 필요하고, 클린 코드는 이렇게 작성하는 거구나! 싶은 생각이 들었다.
영상을 보기전에는 왜 else예약어를 쓰지말라고 하는지, 왜 indent를 2로 제약하고 심지어는 되도록이면 1의 indent로 작성하라고 하는지 이유를 알 수 있었다.
지금까지는 코드의 재사용성이나 가독성을 생각하며 코딩해본적이 없어서 너무나 신선했다. 새로운 방식으로 코딩을 하고 싶은 생각에 의욕이 샘솟았다. 빨리 코딩하러 가고싶었다.
다음은 TDD에 대한 강의를 봤다.
사실 테스트코드에 대해서는 얼마전에 넥스트스텝 강의에서 학습해서 대략 알았는데, 테스트코드랑 TDD는 다르다는 말이 무슨 뜻인지 잘 몰랐는데, 이 영상을 보고 확실히 알게 되었다.
TDD는 코딩을 하는 방법론이라고 한다면 테스트코드는 코드를 테스트하기 위한 기능을 부르는 말로, 다시말해서 TDD를 하기 위해서 필요한 기능이 테스트코드였다.
프로덕션 코드보다 테스트 코드를 먼저 작성함으로써 여러가지의 버그 발생을 낮추고, 확실하게 구현할 코드만 작성하게 됨으로써 시간과 과정을 낭비하지 않을 수 있는 방법이었다.
물론 많은시간이 필요하고, 개녕이 어려우며, 매우 귀찮고 지루할 수 있다.
사실 이번 과제에서 TDD를 하면서 하라고는 명시 되있지 않았지만, 이왕 연습하고 발전하려면 처음부터 습관을 길러가는게 편할 것같아서 어려운 과정이지만 처음부터 차근차근 진행해보기로 했다.
개발 환경 세팅
드디어 수요일이 되었고 3시가 되기전까지 다른 기수 참가자들이 어떻게 프리코스를 진행했는지를 보고 있었다.
3시가 땡치자마자 메일이 왔고 프리코스 진행방식 깃허브를 보면서 먼저 개발환경 세팅을 하기로 했다.
원래는 인텔리제이를 깔고 같이 깔렸던 JDK를 그냥 사용하고 있었고, 애초에 자바 버젼에 대한 생각을 해본 적이 없었는데,
이 과제에서는 자바8 환경으로 세팅 할 것을 요구했다.
java -version 으로 내 자바의 버전을 보니까 자바 16이 깔려있었다. 그래서 엥?? 왜 16버젼이나 나왔는데 8버젼으로 세팅하라고 하는지 궁금해서 찾아봤더니,
대부분의 교육기관에서는 1.8, 즉 자바 8버젼을 통해서 사용하고 있는 것 같았다. 나온지 좀 된 버젼이라 안정적이기도 하고, 자바 1.8 이후로는 유료일뿐더러 정말 대대적이고 필수적인 기능 발전이 있는 것도 아니다 보니
아직 많은 기업이나 공공기관에서 아직 자바 8을 많이 사용하기 있고, 떄문에 대부분의 교육기관에서는 자바 8을 사용해서 보통 교육한다고 한다.
그래서 기존에 깔려있던 최신버젼의 자바를 지우고 자바 8을 까는데, 별 생각 없이 자바 8을 깔았더니 기본 환경 변수가 자꾸 최신버젼을 가르켜서 다시 삭제하고 세팅하고 뭐하느라 시간을 많이 낭비했다.
그래도 앞으로 항상 어떤 프로젝트를 할 때 jdk의 버젼까지 생각해봐야 한다는 걸 깨달았다. 최근에 Swift로 앱개발을 할때도 크게 안알아보고 swiftUI로 개발했다가 나중에 UIKit으로 할걸… 하며 후회를 많이 했었는데,
항상 어떤 새로운 프로젝트를 할 때는 개발 환경 부터 고민하고 세팅해야 겠다는 생각이 들었다.
자바 8로 세팅을 완료한 후에도 기존 인텔리제이가 이미 최신 자바 버젼으로 설정되어서 그런가 자꾸
./gradlew clean test가 fail이 나서 거의 두세시간을 이거 고친다고 고생하다가 인텔리제이 설정 초기화 한번으로 해결했다…
그렇게 수요일 첫날을 개발 환경을 세팅하는데 하루를 다 보냈다.
Readme.md 작성
과제 요구사항에 맞춰서 우선 프로젝트 시작전에 Readme에 기능 구현 목록을 미리 정리하기로 했다.
근데 이게 처음 해보다보니까 기준도 없고 어떤 방식으로 기능을 작성해야 할지 몰라서 시간이 많이 들었다. 다른 참가자들 것도 여러개를 보면서 어떤식으로 작성 했는지 봤는데,
사실 사람마다 나눈 기준도 다르고 작성 방식이 달라서, 어떤 정답이 있다기보다는 누가봐도 기능적으로 분리되어있어서 가독성이 편하게 읽을 수 있고, 내가 개발하면서 기능단위 커밋을 하기 용이 하면 어떨게 작성해도 괜찮다는 생각이 들었다.
그래서 우선 나는 기능단위로 커밋할 수 있도록 크게 한번 나누고, 그 사이에 예외처리해야할 항목이나 큰 기능에 포함되어있는 세부 기능을 작성하기로 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 🛠 구현 기능 목록
* 1-9사이의 숫자중 3개를 뽑아 3자리 수를 정한다.
* 3자리 수를 입력받는다.
* 받은 값이 3자리가 아닌 경우 예외 출력
* 받은 값이 숫자가 아닌 경우 예외 출력
* 받은 값에 중복된 숫자가 있을 시 예외 출력
* 입력값에 따라 힌트를 구분한다.
* 자리와 숫자 둘다 맞춘 경우 => 스트라이크
* 숫자는 같지만 자리가 다른 경우 => 볼
* 자리와 숫자 모두 틀린 경우 => 낫싱
* 게임의 승패 유무를 판단한다.
* 3스트라이크인 경우, 게임이 종료된다.
* 3스트라이크가 아니면 다음 라운드로 자동 진행된다.
* 게임 종료가 되면, 다시 게임을 진행할지 완전히 프로그램을 종료할 지를 출력한다.
* 1 입력 시 게임 재시작
* 2 입력 시 프로그램 종료
그 동안 readme를 작성해야 한다는건 알면서도 쓰다 말기를 반복했었는데, 확실히 프로그램 기능을 구현하기전에 먼저 readme에 구상하니 다음 기능을 구현하기위해 계속 readme를 업데이트하고 들락날락하게 될 뿐만아니라,
프로그램을 만들다보면 오늘 어떤기능을 만들지 앉아서 무작정 보이는거 부터 코딩하고는 했는데, 이렇게 작성을 하면 하나하나 완료해가면서 커밋하게 됨으로 매우 체계적인 단계가 생기겠다는 생각이 들었다.
그럼 기능 구현을 시작해보자.
패키지 나누기
먼저 각 클래스들의 기능을 어떤 기준으로 나누고 기능적으로 어떻게 분배 해주어야 할지 정하고 시작하려 했다.
앱개발을 할때도 이러한 문제로 인해 고민을 했었는데, 그때는 mvvm모델을 사용해서 얼추 비슷하게 따라하려 노력해봤지만
이미 개발이 한참 진행 된 뒤라서 갑자기 새로운 패턴을 적용하기에는 너무 어려워 완벽하게 구현하지 못했었다. 이번에는 기획 단계에서 부터 하나의 모델을 정하고 거기에 최대한 맞춰서 개발해보도록 하기로 했다.
이번에 내가 적용해볼 패턴은 MVC모델이다. MVC 모델은 Model, View, Controller로 나누어진 패턴으로
가장 대표적으로 많이 사용되는 패턴이다. M,V,C는 다음과 같은 역할을 담당한다.
- Model - 데이터와 관련된 부분
- View - 사용자에게 보여지는 부분
- Controller - Model과 View를 이어주는 부분
MVC모델은 아래와 같은 규칙을 따른다.
-
Model은 Controller와 View에 의존하지 않아야 한다.
Model 내부에 Controller와 View에 관련된 코드가 있으면 안된다. -
View는 Model에만 의존해야 하고, Controller에는 의존하면 안된다.
View 내부에는 Model에 대한 코드는 있을 수 있지만, Controller에 대한 코드는 있어선 안된다. -
View가 Model로부터 데이터를 받을 때는, 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야한다.
-
Controller는 Model과 View에 의존해도 된다.
Controller 내부에는 Model과 View에 관한 코드가 존재할 수 있다. -
View가 Model로 부터 데이터를 받을 때, 반드시 Controller를 통해서 받아야 한다.
이 내용은 우아한 테크 코스에서 진행했던 10분 테코톡 ‘제리’님의 영상을 참고했다.
기능 구현
1-9 사이의 숫자를 3개를 뽑아 3자리수를 정한다.
먼저, 게임이 시작되면 컴퓨터가 정답으로 지정할 1-9 사이의 3개의 숫자를 난수로 출력해서 저장해야 한다.
Model에 AnsewerNumberGenerater라는 클래스를 만들어 주었다.
TDD를 적용해서 연습하기로 했으므로 테스트코드 먼저 작성 해보도록 하겠다.
테스트코드
AnswerNumberGenerator 클래스는 말그대로 정답 번호 생성기이므로, 정답 번호를 생성하는 메서드가 필요하다.
따라서 GetAnswerNumber() 라는 메서드를 호출하면 정답 번호를 배열로 반환하는 메서드가 필요하다.
다음과 같이 테스트 코드를 작성해 보았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AnswerNumberGeneratorTest {
AnswerNumberGenerator answerNumberGenerator;
@BeforeEach
void setUp() {
answerNumberGenerator = new AnswerNumberGenerator();
}
@Test
void answerNumberGenerate() {
assertThat(answerNumberGenerator.GetAnswerNumber())
.size()
.isEqualTo(3);
for (int i = 0; i < 3; i++) {
assertThat(answerNumberGenerator.GetAnswerNumber()
.get(i))
.isBetween(1, 9);
}
}
}
먼저 생성한 숫자의 개수가 3개인지 확인 해주고, 이 3가지 숫자가 모두 다른 숫자인지 확인하도록 했다.
이를 기반으로 테스트 코드가 통과 할 때까지 프로덕션 코드를 작성해 보도록 하자.
프로덕션 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnswerNumberGenerator {
private final static int MIN_RANGE_NUM = 1;
private final static int MAX_RANGE_NUM = 9;
private final static int MAX_NUMBER_SIZE = 3;
ArrayList<Integer> answerNumberList = new ArrayList<Integer>();
public ArrayList<Integer> GetAnswerNumber() {
while (answerNumberList.size() < MAX_NUMBER_SIZE) {
isNumberInAnswerNumberList(Randoms.pickNumberInRange(MIN_RANGE_NUM, MAX_RANGE_NUM));
}
return answerNumberList;
}
public void isNumberInAnswerNumberList(int number) {
if (answerNumberList.contains(number)) {
}
answerNumberList.add(number);
}
}
answerNumberList라는 arrayList를 사용해서 3개의 숫자가 각각 다른 개수로 골라질 때 까지
while문이 돌아가게 했다.
GetAnswerNumber의 indent를 1로 줄이기 위해서 리스트에 같은 숫자가 있는지 확인하는 메서드를 따로 빼서 isNumberInAnswerNumberList 메서드로 구현했다.
테스트 코드를 돌려보니 알맞게 프로덕션 코드를 잘 작성 한 것같다.
리팩토링
애초에 의식하고 작성해서 크게 리팩토링 할 부분은 없는 듯 하다.
근데 저 isNumberInAnserNumberList를 매개변수를 두개로 하고 isNumberinList로 이름을 바꿔서
재사용성을 높이는게 나을지 고민이됐다. 매개변수를 늘리는게 좋을까 재사용성이 높아지는게 더 좋을까…
고민 끝에 재사용성을 높이는게 매개변수 하나 늘어나는 것 보다 더 효율적일거라는 생각이 들었다.
그래서 프로덕션 코드를 다음과 같이 수정했다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnswerNumberGenerator {
private final static int MIN_RANGE_NUM = 1;
private final static int MAX_RANGE_NUM = 9;
private final static int MAX_NUMBER_SIZE = 3;
ArrayList<Integer> answerNumberList = new ArrayList<Integer>();
public ArrayList<Integer> GetAnswerNumber() {
while (answerNumberList.size() < MAX_NUMBER_SIZE) {
isNumberInList(Randoms.pickNumberInRange(MIN_RANGE_NUM, MAX_RANGE_NUM), answerNumberList);
}
return answerNumberList;
}
public void isNumberInList(int number, ArrayList<Integer> list) {
if (list.contains(number)) {
}
list.add(number);
}
}
이제 isNumberInList는 answerNumberList 말고도 다른 배열에 있어서도 해당 숫자가 포함되어있지 않을때만 배열에 숫자를 추가해줄 수 있게 되었다.
다음으로는 isNumberInList를
1
2
3
4
5
6
public void isNumberInList(int number, ArrayList<Integer> list) {
if (list.contains(number)) {
}
list.add(number);
}
1
2
3
4
5
public void isNumberInList(int number, ArrayList<Integer> list) {
if (!list.contains(number)) {
list.add(number);
}
}
두가지 중에 어떤걸로 할지 고민되었다. 깔끔한건 아래 코드인것 같은데, !가 잘 안보여 위에 코드보다 덜 명확한 코드라는 생각이 들어서 그냥 그대로 1번으로 진행하기로 했다.
테스트 코드를 돌려보니 전과 동일하게 테스트를 통과 할 수 있었다.
커밋
기능 구현이 하나 완료된 것 같아서 커밋을 진행해 보려한다.
그동안 커밋을 할 때 별생각없이 커밋 메시지로 그냥 날짜를 표시한다거나 했는데, 이제부터는 명확하게 규칙에 따라 커밋로그를 남겨야 한다.
태그 이름 | 설명 |
---|---|
Feat | 새로운 기능을 추가할 경우 |
Fix | 버그를 고친 경우 |
Design | CSS 등 사용자 UI 디자인 변경 |
!BREAKING CHANGE | 커다란 API 변경의 경우 |
!HOTFIX | 급하게 치명적인 버그를 고쳐야하는 경우 |
Style | 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우 |
Refactor | 프로덕션 코드 리팩토링 |
Comment | 필요한 주석 추가 및 변경 Docs 문서를 수정한 경우 |
Test | 테스트 추가, 테스트 리팩토링(프로덕션 코드 변경 X) |
Chore | 빌드 태스트 업데이트, 패키지 매니저를 설정하는 경우(프로덕션 코드 변경 X) |
Rename | 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우 |
Remove | 파일을 삭제하는 작업만 수행한 경우 |
위의 태그를 붙여서
1
2
3
4
5
type(옵션): [#issueNumber - ]Subject // -> 제목
(한 줄을 띄워 분리합니다.)
body(옵션) // -> 본문
(한 줄을 띄워 분리합니다.)
footer(옵션) // -> 꼬리말
다음과 같이 기능을 추가 한다.
이번엔 기능을 추가 했으니,
[feat] 1-9사이의 숫자중 3개를 뽑아 3자리 수를 정한다.
이렇게 커밋 로그를 작성하면 된다.
3자리 수를 입력 받는다.
사용자로부터 3자리 수를 받아야한다. 3자리 숫자가 아닌경우는 모두 예외처리되어 프로그램이 종료되어야 하는데, 따라서 숫자를 UserNumber라는 클래스로 포장해서 사용해서 사용하려 한다.
테스트 코드
이래서 TDD가 어렵다고 했구나…
테스트 코드를 먼저 작성하려면 이미 머릿속에서 어느정도 구현에 대한 감이 와야하는데, 자바가 익숙치 않아서 어렵다. 그래서 이번에는 프로덕션 코드를 먼저 구현해보고 테스트 코드를 작성해 보았다.
프로덕션 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class UserNumber {
private final static int MAX_NUMBER_SIZE = 3;
private String userNumber;
public String getUserNumber() {
return userNumber;
}
public UserNumber(String userNumber) {
if (isStringLengthThree(userNumber) && isUserNumberDigit(userNumber) && isSameAllNumber(userNumber)) {
this.userNumber = userNumber;
}
}
public boolean isUserNumberDigit(String word) {
for (int i = 0; i < word.length(); i++) {
isStringCharAtDigit(word, i);
}
return true;
}
public boolean isStringCharAtDigit(String word, int index) throws IllegalArgumentException {
if (!Character.isDigit(word.charAt(index))) {
throw new IllegalArgumentException();
}
return true;
}
public boolean isStringLengthThree(String word) throws IllegalArgumentException {
if (word.length() != USER_NUMBER_LENGTH) {
throw new IllegalArgumentException();
}
return true;
}
public boolean isSameAllNumber(String word) throws IllegalArgumentException {
Set<Character> set = new HashSet<>();
for (int i = 0; i < word.length(); i++) {
set.add(word.charAt(i));
}
if (set.size() != word.length()) {
throw new IllegalArgumentException();
}
return true;
}
}
UserNumber 클래스는 입력받은 문자열이 3자리인지 확인하고, 모든 문자열이 숫자로 들어왔는지 확인한다. 그리고 마지막으로 중복된 문자열이 있는지
세가지 유효성검사를 하는 자료형이다.
이제 프로덕션 코드를 모두 작성하고나니, 테스트 코드를 어떻게 작성해야 할지가 보였다.
중요한건 예외처리가 잘되는지 이므로 이 클래스에 값이 들어왔을때 제대로 예외처리가 진행되는지 코드를 작성 해주면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
class UserNumberTest {
UserNumber userNumber;
@ParameterizedTest
@ValueSource(strings = {"31","311","3io","i",""," "})
public void ExceptionTest(String input) {
assertThatThrownBy(() -> {
userNumber = new UserNumber(input);
})
.isInstanceOf(IllegalArgumentException.class);
}
ParameterizedTest 어노테이션을 사용해서 모든 예외에서 올바르게 IllegalArgumentException을 출력하는걸 확인 했다.
리팩토링
우선 기능구현부터 해놓고 생각하자는 생각으로 가장 기본적인 인덴트 지키기나 메소드 분리는 잘한 것 같은데,
맘에 안드는 부분이 몇개 있어서 리팩토링이 필요할 것 같다. 우선 기능하나를 완료했으니 커밋부터 한 뒤에
리팩토링을 시작했다.
- 일일히 클래스마다 상수를 선언해주는 것보다 Constant 클래스를 만들어서 상수를 따로 모아 관리하는게 좋을 것같다.
-
우선 첫번째로 MVC패턴을 사용하기로 했는데, 세가지로 기능을 나누는게 좀 감이 잘 안와서 제대로 역할 분배가 안된 것같다.
다시 코드를 보면서 MVC 세가지 패키지로 클래스를 더 세분화 해보자. - 유효성 검사가 너무 복잡한 것 같다. 더 간단하게 예외를 처리 할 수 있을것 같다.
먼저 1번에 대한 문제를 해결해보자.
상수들을 따로 클래스에 모아서 저장하면 한번만 선언해도 이 상수를 다른곳에서 모두 사용할 수 있고,
상수 변경이 필요할때도 일일히 찾지 않아도 된다. 따로 패키지를 만들어서 Constant 라는 클래스를 만들어주었다.
1
2
3
4
5
public class Constant {
public final static int MIN_RANGE_NUM = 1;
public final static int MAX_RANGE_NUM = 9;
public final static int USER_NUMBER_LENGTH = 3;
}
그 다음 2번 문제를 해결해보자.
우선 AnswerNumberGenerator이름을 PlayerNumber로 바꾸고, PlayerNumber가 좀 더 모델의 클래스에 가깝게 AnswerNumber 객체를 만들면
생성자가 세자리 난수를 부여하고 이를 getNewPlayerNumber() 메서드를 따로 만들어서 만약 다음게임이 시작되어 난수를 새로 부여받아야 한다면
이 메소드로 모델의 값을 변경 하는게 좋을 것 같다.
이렇게 하면 좀 더 기능적으로 구분이 잘 될 것이다.
추가로 반환하는 값이 지금 ArrayList로 되어있는데, PlayerNumber와 동일하게 String으로 통일하는게 맞는 것 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class AnswerNumber {
private String answerNumber;
public AnswerNumber() {
getNewAnswerNumber();
}
public String getAnswerNumber() {
return answerNumber;
}
public void getNewAnswerNumber() {
ArrayList<String> answerNumberList = new ArrayList<String>();
while (answerNumberList.size() < Constant.USER_NUMBER_LENGTH) {
isNumberInList(getStringRandomNumber(), answerNumberList);
}
this.answerNumber = String.join("", answerNumberList);
}
public static void isNumberInList(String number, ArrayList<String> list) {
if (list.contains(number)) {
}
list.add(number);
}
public static String getStringRandomNumber() {
return Integer.toString(Randoms.pickNumberInRange(Constant.MIN_RANGE_NUM, Constant.MAX_RANGE_NUM));
}
}
이렇게 하면 AnswerNumber라는 객체가 모델 기능에 더 맞는 느낌으로 바뀐것 같다.
밖에서 접근 할 수 없도록 클래스 멤버 변수는 private로 선언하고 getAnswerNumber로 값을 받아올 수 만 있게 했다.
새로운 난수가 필요하다면, getNewAnswerNumber() 메서드를 사용해서 모델의 데이터를 변경해주면 된다.
UserNumber도 마찬가지로 좀 더 모델의 역할에 충실하게 리팩토링 해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class PlayerNumber {
private String playerNumber;
public String getUserNumber() {
return playerNumber;
}
public void setPlayerNumber(String playerNumber) {
if (isStringLengthCorrect(playerNumber) && isDigitPlayerNumber(playerNumber) && isDifferentPlayerNumber(playerNumber)) {
this.playerNumber = playerNumber;
}
}
public static boolean isStringLengthCorrect(String word) throws IllegalArgumentException {
if (word.length() != Constant.USER_NUMBER_LENGTH) {
throw new IllegalArgumentException(SystemMessage.NUMBER_LENGTH_NOT_CORRECT);
}
return true;
}
public boolean isDigitPlayerNumber(String word) {
for (int i = 0; i < word.length(); i++) {
isDigitCharInString(word, i);
}
return true;
}
public static boolean isDifferentPlayerNumber(String word) throws IllegalArgumentException {
Set<Character> set = new HashSet<>();
for (int i = 0; i < word.length(); i++) {
set.add(word.charAt(i));
}
if (set.size() != word.length()) {
throw new IllegalArgumentException(SystemMessage.HAS_SAME_NUMBER);
}
return true;
}
public static boolean isDigitCharInString(String word, int index) throws IllegalArgumentException {
if (!Character.isDigit(word.charAt(index))) {
throw new IllegalArgumentException(SystemMessage.STRING_IS_NOT_NUMBER);
}
return true;
}
}
커밋
[refactor] mvc모델에 맞게 리팩토링
입력값에 따라 힌트를 구분한다.
이제 본격적으로 앞에서 구현한 클래스들을 사용하는 객체를 만들어 보려고 한다.
게임에 참가하는 Player와 상대방인 Computer 객체를 만들어 Player는 숫자를 고르고,
Computer는 이 숫자를 받으면 힌트를 주도록 할 것이다.
Player, Computer라는 모델 두개를 만들어 준다.
먼저 Computer 클래스를 만들어보자.
Computer 테스트 코드
Computer 객체는 getHint() 메서드를 호출하면 userNumber에 맞는 힌트를 출력해주어야 한다.
따라서 다음과 같이 테스트 코드를 작성 할 수 있다.
1
2
3
4
5
@ParameterizedTest
@CsvSource(value = {"123: 3스트라이크", "132:1볼 2스트라이크","456 : 낫싱"},delimiter = ':')
public static void getHintTest(String input, String output){
assertThat(computer.getHint(input).isEqualTo(output);
}
Computer 프로덕션 코드
테스트 코드를 만족하는 프로덕션 코드를 작성 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Computer {
private AnswerNumber computerAnswerNumber;
public Computer() {
computerAnswerNumber = new AnswerNumber();
}
public String getAnswerNumber() {
return computerAnswerNumber.getAnswerNumber();
}
public static int CountingBall(String userNumber, String answerNumber) {
int ball = Constant.BALL_INITIAL_VALUE;
for (int i = 0; i < userNumber.length(); i++) {
if (answerNumber.contains(Character.toString(userNumber.charAt(i)))) {
ball += 1;
}
}
return ball;
}
public static int CountingStrike(String userNumber, String answerNumber) {
int strike = Constant.STRIKE_INITIAL_VALUE;
for (char userNum : userNumber.toCharArray()) {
if (answerNumber.indexOf(userNum) == userNumber.indexOf(userNum)) {
strike += 1;
}
}
return strike;
}
public int[] getHint(String userNumber) {
int ballCount = CountingBall(userNumber, computerAnswerNumber.getAnswerNumber());
int strikeCount = CountingStrike(userNumber, computerAnswerNumber.getAnswerNumber());
return new int[]{ballCount - strikeCount, strikeCount};
}
}
Computer는 인스턴스를 만들면 랜덤으로 수를 하나 정한 뒤, 플레이어에 답에 따라 줄 힌트를 골라준다.
Player 테스트 코드
Player 클래스가 하는 일이 받은 수 를 저장하는 것뿐이라 따로 테스트 코드를 작성할 필요가 없을 것 같다.
그럼에도 굳이 Player 클래스를 만들어야 하는 이유는 후에 만약 Player에게 기능이 추가 될때 유지보수가 더 용이하기 때문이다.
Player 프로덕션 코드
1
2
3
4
5
6
7
public class Player {
private TryNumber playerNumber = new TryNumber();
public void setUserNumber(String userNumber) {
playerNumber.setUserNumber(userNumber);
}
}
그럼 이제 이 기능들을 출력하는 View를 따로 만들어주자.
readme 수정
생각해보니 readme 파일에 입출력도 따로 기능 구현 목록으로 추가 해주고 분리해서 커밋하는게 맞을 것 같아서 readme 파일을 수정했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 🛠 기능 구현 목록
* 1-9사이의 숫자중 3개를 뽑아 3자리 수를 정한다. v
* "숫자를 입력하세요" 문구를 출력한다.
* 3자리 수를 입력받는다. v
* 받은 값이 3자리가 아닌 경우 예외 출력 v
* 받은 값이 숫자가 아닌 경우 예외 출력 v
* 받은 값에 중복된 숫자가 있을 시 예외 출력 v
* 입력값에 따라 힌트를 구분한다. v
* 자리와 숫자 둘다 맞춘 경우 => 스트라이크 v
* 숫자는 같지만 자리가 다른 경우 => 볼 v
* 자리와 숫자 모두 틀린 경우 => 낫싱 v
* 답에따른 힌트 문구를 출력한다.
* 게임의 승패 유무를 판단한다.
* 3스트라이크인 경우, 게임이 종료된다.
* 3스트라이크가 아니면 다음 라운드로 자동 진행된다.
* 게임 종료가 되면, 다시 게임을 진행할지 완전히 프로그램을 종료할 지를 출력한다.
* 1 입력 시 게임 재시작
* 2 입력 시 프로그램 종료
InputView 기능 구현
1
2
3
4
5
6
public class InputView {
public static String setUserNumber() {
System.out.printf(SystemMessage.SET_USER_NUMBER_MESSAGE);
return Console.readLine();
}
}
InputView 클래스를 따로 만들어서 입력을 받는 View를 구성해준다.
입력을 받을때마다 “숫자를 입력하세요”를 함께 출력한다.
OutputView 기능 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OutputView {
public static void printHintMessage(int[] computerHint) {
if (computerHint[0] != 0) {
System.out.print(computerHint[0] + SystemMessage.BALL_MESSAGE + " ");
}
if (computerHint[1] != 0) {
System.out.print(computerHint[1] + SystemMessage.STRIKE_MESSAGE + " ");
}
if (computerHint[0] == 0 && computerHint[1] == 0) {
System.out.print(SystemMessage.NOTHING_MESSAGE + " ");
}
System.out.println();
}
}
마찬가지로 출력값을 담당하는 Output 클래스를 만들어서 출력에 관한 View는 여기서 담당하도록 해준다.
Computer에서 힌트를 받아서 그에 맞는 메시지를 출력한다.
종료
오늘은 여기까지…
솔직히 기능 구현만 하면 한시간이면 다 할 것 같은데 진짜로 코드하나 쓰기전에 생각을 한시간씩 하게 된다.
어떤게 올바른 코드인지 한 한시간을 검색하고 고민한뒤에 코드를 적으니 진도가 너무 더디지만, 뭔가 성장해가는 기분이 들어 뿌듯하다. 그동안 코딩해보면서 이렇게 고민하고 코드 적었던 적이 없었던것 같다…
아직도 너무너무 불만족스러운 코드라서, 내일 마저 열심히 더 예쁜코드를 위해 노력해보겠다!
댓글남기기