2주차 시작 전
2주차 시작 전에 먼저 코수타(코치와 수다 타임)를 보면서 맘을 다잡게 되었다.
2주차 코수타(코치와 수다 타임) 중에서 하신 말씀 중에 크게 와닿았던 부분이 있었다.
어떤 분이 1주차 끝나고 번아웃이 왔다고 어떻게 해야하나요? 라는 질문에 포비가 답변해준 부분이었다.
이 짧은 시간안에 번아웃이 왔다는 것은,
내가 걸어가는 방향대로 가지 않고,
다른 사람들이 걸어가는 방향을 보며 그것을 따라가려고, 앞지르려고 하다보니 온 경우가 많을 것이다.
모든 것을 다 잘하려고 할 때 번아웃이 오는 경우가 많다.
따라서, 다른 사람과 비교하지 말고 오로지 나를 보며 내가 걸어가는 방향에 집중하고
내 속도에 맞게 묵묵히 걸어가자.
솔직히 나도 번아웃까지는 아니지만 1주차가 끝나고 나보다 훨씬 뛰어난 다른 크루의 코드들을 보면서
동기부여가 아니라 좌절을 하루동안 경험했었다.
그 후에 포비의 답변을 들으니 정말 마음을 다잡게 되었다.
앞으로는 어제의 나를 이기자!를 목표로 잡고, 다른 분들의 코드가 내 코드보다 뛰어나다면
그 코드에서 배울점을 찾고 동기부여의 계기로 삼아야겠다고 다짐했다.
또, 코수타에서 2주차에서 과제 해결 방향을 제시해주셔서 머릿속에 새겨넣고 과제를 진행하게 되었다.
1. 기능 목록 작성 부분
1. 기능목록을 최대한 작게 쪼개기 -> 테스트하기 쉽기 때문
2. 작게 쪼갠 것끼리 비교해보고 묶을 수 있는 것 묶기
3. 작게 쪼갠 것 중에 핵심적인 로직을 먼저 빠르게 구현하기
2. 요구 사항 부분
최대한 내가 현장 프로그램을 만든다고 생각하고, 내가 스스로 판단하고 처리하기
1주차 미션의 불명확한 요구 사항에 대해 말들이 많았는데, 이는 우테코에서 설계한 방향이라고 했다.
현장에서도 요구 사항이 불명확한 경우가 많기 때문에 이를 해결하는 방향으로 설계했다고 한다.
그러므로 스스로 요구사항을 알맞게 판단하여 제외할 것은 제외하고 구현할 것은 구현하기
3. 2주차 미션에서 얻어가야 할 점
1. 1주차 미션에서는 하나의 함수를 잘 구현하는 것에 초점을 맞췄다면,
2주차부터는 그 하나의 함수를 더 많이 구현해야 한다.
따라서, 함수를 하나의 기능만 가지도록 분리하는 것에 초점을 두자!
2. 함수를 테스트하는 테스트 도구에 익숙해지자! (테스트 코드 작성)
TDD(테스트 주도 개발)을 진행해보자!
2주차 미션 설계
1주차는 하나의 프로그램 형식이 아닌 알고리즘 코딩테스트 형식이어서
설계 후 구현을 하긴 했지만 그 필요성이 크게 와닿진 않았다.
하지만, 이번 2주차 미션은 하나의 숫자 야구 프로그램을 만드는 것이었기 때문에
거의 1주차 7문제를 하나로 합치는 것과 양이 비슷하다고 느껴졌기 때문에
구현 전의 설계가 무척 중요하다고 느껴져서 거의 하루 이틀을 다 썼다.
구현 전의 나의 설계 과정들을 기록해보려고 한다.
물론 최종 프로덕션 코드가 설계대로 가진 않아서 최종 코드와는 좀 다르지만,
설계 과정에 초점을 맞춰서 이러한 과정으로 3주차도 설계를 진행해보려고 한다.
1. 기능 목록 초안 작성
[게임 로직 기능]
1. 컴퓨터 랜덤 수 생성 기능
2. 각 자릿수의 숫자를 리스트에 추가하는 기능
3. 사용자의 각 자릿수 숫자 리스트와 컴퓨터의 각 자릿수 숫자 리스트 비교하는 기능(볼, 스트라이크 판정)
4. 볼 개수를 증가시키는 기능
5. 스트라이크 개수를 증가시키는 기능
[입력 기능]
1. 사용자가 입력하는 수 입력 받는 기능
2. 게임 끝난 후 재시작/종료 수를 입력받는 기능
[출력 기능]
1. 게임 시작 문구 출력 기능
2. 입력한 수에 대한 볼, 스트라이크 개수를 출력하는 기능
3. 입력한 수에 대한 볼, 스트라이크가 없는 경우 낫싱을 출력하는 기능
4. 입력한 수가 컴퓨터의 랜덤 수일 경우(3개의 숫자를 모두 맞혔을 경우) 스트라이크 개수와 함께 게임 종료 문구 출력하는 기능
[검증 기능]
1. 사용자가 숫자 형식 이외의 값을 입력했는지 검증하는 기능
2. 사용자가 정해진 숫자 길이를 지켰는지 검증하는 기능
3. 게임이 끝났을 때 재시작, 종료 숫자 이외의 값을 입력했는지 검증하는 기능
2. 필요한 클래스(객체) 나열
필요한 클래스 나열해보자! -> 객체 중심으로 클래스 설계해보자! -> 필요한 객체가 무엇일까?
* 상대방 컴퓨터 객체
* 플레이하는 플레이어 객체
* 게임 관련 로직을 처리하는 숫자 야구 게임 객체
* 입력을 처리하는 입력 객체
* 출력을 처리하는 출력 객체
* 검증을 처리하는 검증 객체
이렇게 필요한 객체들을 나열해놓고 나니, 객체들이 많아져서 패키지 없이 작성하기가 어색했다.
따라서, 이전에 알고 있었던 MVC 패턴을 적용해보았다.
적용 전에, MVC 패턴을 수박 겉핥기 식으로 알고 있었기 때문에 먼저 더 지식을 쌓은 뒤 적용하기로 했다.
💡 MVC 패턴
✅ Model : 데이터와 관련된 로직을 처리하는 계층이다.
- View와 Controller에 의존하면 안된다.
✅ View : 사용자에게 보이는 화면, 즉 UI 계층이다.
- Controller로부터 Model이 가진 데이터를 전달받아 출력할 뿐, Model이 가진 데이터를 따로 저장하면 안된다.
- Controller로부터 Model이 가진 데이터를 전달받아 출력할 뿐, 모든 행동은 Controller에게 맡긴다.
✅ Controller : 애플리케이션의 메인 로직, 즉 비즈니스 로직이 구현되어 있는 계층이다.
- 데이터(Model)와 UI(View)를 잇는 다리 역할
- 사용자의 요청을 처리하고, 모델을 조작하며, 최종 UI로 출력될 뷰를 결정한다.
- 사용자의 요청을 받아 Model에게 요청에 맞는 데이터 변경을 지시한다.
- Model에게 응답에 필요한 데이터를 가져와서 응답을 구성한다.
- 사용자의 요청으로 특정 View와 종속 관계가 발생할 수 밖에 없다.
또, MVC 패턴을 안전하게 활용할 수 있도록 돕는 MVC 패턴 원칙들을 참고하여 설계를 진행했다.
💡 MVC 패턴을 안전하게 활용할 수 있도록 돕는 MVC 패턴 원칙들
✅ Model은 Controller와 View에 의존해서는 안된다.
- Model이 Controller, View의 책임을 가지면 안된다는 것이다.
- 오로지 데이터에 대한 순수 로직만 존재하는 것이 좋다.
✅ View는 Model의 데이터에만 의존한다.
- View에서 사용되는 값은 Model에서 전달된 데이터여야한다.
- 이때, Model의 데이터는 Controller가 View로 넘겨준다.
✅ View는 Model로부터 사용자마다 다르게 변경되는 데이터만 받아야한다.
- 사용자마다 다른 생애주기를 가진 데이터만을 Model에게 받아야한다.
- 즉, 공통으로 처리되는 부분은 View 자체에서 처리되어야 한다.
- EX) Player라는 Model이 있고, OutputView라는 View가 있을 때 프로그램 실행 시 항상 "Player의 결과"라는 문자열이 출력되게 설계한다면?
👉 Player라는 Model에게 "Player의 결과"라는 문자열을 받는 것이 아니라,
OutputView에서 자체적으로 문자열을 출력하도록 처리해야한다.
✅ View가 Model로부터 데이터를 받을 때는 Controller에서 받아야한다.
- Controller는 요청에 맞게 Model에게 데이터 요청을 하여 Model에서 처리 후 Controller에게 다시 반환하여 그 결과를 View에 전달해야한다.
3. 클래스를 패턴에 맵핑
* 상대방 컴퓨터 객체 -> 랜덤 수 데이터와 볼, 스트라이크 데이터를 가진다 -> Model
* 플레이하는 플레이어 객체 -> 입력한 수 데이터를 가진다 -> Model
* 게임 관련 로직을 처리하는 숫자 야구 게임 객체 -> 게임의 메인 로직을 처리한다 -> Controller
* 입력을 처리하는 입력 객체 -> 사용자가 입력하는 화면을 처리 -> View
* 출력을 처리하는 출력 객체 -> 사용자에게 보이는 출력을 처리 -> View
* 검증을 처리하는 검증 객체 -> 어떤 형식의 검증인가에 따라 Model, View, Controller가 나뉨
* Model : 데이터에 대한 검증
* Controller : 들어오는 데이터가 있는지 존재 유무 검증
* View : 사용자의 입력 값에 대해 검증
-> 현재 검증 기능은 모두 입력 값에 대한 검증이므로 View에서 처리
4. 클래스 네이밍
* 상대방 컴퓨터 객체 / Model -> OpponentComputer
* 플레이하는 플레이어 객체 / Model -> Player
* 게임 관련 로직을 처리하는 숫자 야구 게임 객체 / Controller -> NumberBaseballGameController
* 입력을 처리하는 입력 객체 / View -> InputView
* 출력을 처리하는 출력⌞객체 / View -> OutputView
5. 패키지 구조 설계 -> MVC 패턴에 맞게 패키지 별로 정리
model
⎿ OpponentComputer
⎿ Player
view
⎿ InputView
⎿ OutputView
controller
⎿ NumberBaseballGameController
6. 기능 목록에서 각 클래스(객체)가 가지는 기능 맵핑
[게임 로직 기능]
1. 컴퓨터 랜덤 수 생성 기능 -> OpponentComputer
2. 각 자릿수의 숫자를 리스트에 추가하는 기능 -> NumberBaseballGameController
3. 사용자의 각 자릿수 숫자 리스트와 컴퓨터의 각 자릿수 숫자 리스트 비교하는 기능(볼, 스트라이크 판정)
-> NumberBaseballGameController
4. 볼 개수를 증가시키는 기능 -> Player
5. 스트라이크 개수를 증가시키는 기능 -> Player
[입력 기능]
1. 사용자가 입력하는 수 입력 받는 기능 -> InputView
2. 게임 끝난 후 재시작/종료 수를 입력받는 기능 -> InputView
[출력 기능]
1. 게임 시작 문구 출력 기능 -> OutputView
2. 입력한 수에 대한 볼, 스트라이크 개수를 출력하는 기능 -> OutputView
3. 입력한 수에 대한 볼, 스트라이크가 없는 경우 낫싱을 출력하는 기능 -> OutputView
4. 입력한 수가 컴퓨터의 랜덤 수일 경우(3개의 숫자를 모두 맞혔을 경우) 스트라이크 개수와 함께 게임 종료 문구 출력하는 기능 -> OutputView
[검증 기능]
1. 사용자가 숫자 형식 이외의 값을 입력했는지 검증하는 기능 -> InputView
2. 사용자가 정해진 숫자 길이를 지켰는지 검증하는 기능 -> InputView
3. 게임이 끝났을 때 재시작, 종료 숫자 이외의 값을 입력했는지 검증하는 기능 -> InputView
7. 각 기능 처리 과정 정리
* 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개 선택
-> 임의의 수 데이터 OpponentComputer(Model)에 저장
* 게임 플레이어는 컴퓨터가 생각하고 있는 서로 다른 3개의 숫자를 입력
-> InputView(View)에서 데이터 입력 후 NumberBaseballController(Controller)로 전달
-> NumberBaseballController(Controller)에서 Player(Model)로 전달
-> Player(Model)에 저장
* 컴퓨터는 입력한 숫자에 대한 결과를 출력
-> NumberBaseballController(Controller)에서 Player(Model)에 저장된 입력 수 조회
-> 조회 한 입력 수를 OpponentComputer(Model)에 전달
-> 입력 수에 대한 스트라이크, 볼 개수를 계산하는 로직을 거친 후 NumberBaseballController(Controller)에게 전달
-> NumberBaseballController(Controller)에서 OutputView(View)로 입력 수에 대한 스트라이크, 볼 개수 전달
-> OutputView(View)에서 전달받은 스트라이크, 볼 개수 출력
* 3개의 숫자를 맞추지 못하면 숫자 입력 반복
-> NumberBaseballController(Controller)에서
OpponentComputer(Model)에서 받는 스트라이크 개수가 3개가 될때까지 입력한 숫자에 대한 결과 출력 과정 반복
마지막 과정으로는 각 클래스별 기능을 정리하여 그에 맞게 테스트 코드 메소드를 짰는데,
테스트를 짜면서 많이 달라져서 기록은 하지 않으려고 한다.
테스트가 처음이라 테스트 코드를 어떻게 짜야할지 모르는 기능이 많았어서 그랬던 것 같다.
3주차부터는 설계 시에 테스트 코드를 어떻게 짤지 제대로 설계해야겠다.
TDD 개발
코수타에서 TDD로 개발을 진행해보라고 해서 TDD로 진행하게 되었다.
TDD는 들어보기만 하고 직접 해본 적은 한번도 없어서 역시 먼저 TDD에 대한 지식을 쌓고 개발을 시작했다.
💡 TDD(Test Driven Development)
실제 애플리케이션 개발 시 프로덕션 코드를 먼저 짜는 것이 아닌,
테스트 코드를 먼저 작성하여 프로그램이 잘못되었음을 증명하고,
그 잘못된 부분을 수정해가며 최종 목표에 부합할만큼 참으로 만드는 개발 방법!
내가 설계한대로 0부터 시작하여 프로그램이 올바르지 않음을 증명해가다가,
최종적으로 프로그램이 올바르지 않음을 증명하는게 실패하게 되면(내가 설계한 모든 잘못된 점을 수정하면)
비로소 프로그램의 허점을 찾을 수 없는 올바름이 된다는 것이 TDD의 사상!
※ TDD 과정
여러 TDD 관련 참고 문서들이 많이 있었지만, 공통적인 과정은 다음과 같았다.
1. 기능에 따른 실패하는 테스트 케이스 작성
2. 1번에서 만든 실패하는 테스트 케이스를 통과할 정도로만 production 코드 작성
3. 테스트 코드 리팩토링
이 과정을 나의 TDD 예시로 살펴보자.
OpponentComputerTest
1. 기능에 따른 실패하는 테스트 케이스 작성 - 상대방 생성과 동시에 랜덤 수 저장 기능
@Test
void 상대방_생성과_동시에_랜덤_수_저장() {
OpponentComputer opponentComputer = new OpponentComputer(Randoms.pickNumberInRange(111,999));
int opponentComputerNumber = opponentComputer.getNumber();
assertThat(opponentComputerNumber).isNotNull();
}
구현 코드 없이 해당 테스트 케이스를 작성하면 당연히 컴파일 오류가 발생한다.
발생하는 이유는 다음과 같다.
* OpponentComputer 클래스 존재 X
* 생성과 동시에 랜덤 수를 저장하는 OpponentComputer 생성자 존재 X
* OpponentComputer의 랜덤 수 필드를 반환하는 getNumber() 존재 X
2. 1번에서 만든 실패하는 테스트 케이스를 통과할 정도로만 production 코드 작성
위의 실패 이유를 해결해가며 실패하는 테스트 케이스를 통과할 정도로만 production 코드 작성해간다.
실패하는 테스트 케이스를 통과할 정도로만 production 코드 작성
* OpponentComputer 클래스 존재 X : OpponentComputer 클래스 생성
* 생성과 동시에 랜덤 수를 저장하는 OpponentComputer 생성자 존재 X
public OpponentComputer(int randomNumber) {
this.number = randomNumber;
}
* OpponentComputer의 랜덤 수 필드를 반환하는 getNumber() 존재 X
public int getNumber() {
return number;
}
3. 테스트 코드 리팩토링
OpponentComputer opponentComputer = new OpponentComputer(Randoms.pickNumberInRange(111,999));
이 부분에서 Randoms.pickNumberInRange(111,999) 부분을 따로 Util 클래스로 리팩토링하여 처리하고 싶었다.
따라서, 아래와 같이 리팩토링하게 되었다.
OpponentComputer opponentComputer = new OpponentComputer(RandomUtil.createRandomNumber());
공부한 부분
1. System.out.println() 테스트 코드 작성하기
private final ByteArrayOutputStream output = new ByteArrayOutputStream();
@BeforeEach
public void setUpStreams() {
System.setOut(new PrintStream(output));
}
@AfterEach
public void restoreStreams() {
System.setOut(System.out);
}
@Test
void 게임_시작_문구_출력() {
String gameStartMessage = "숫자 야구 게임을 시작합니다.\n";
OutputView.printGameStartMessage();
assertThat(output.toString()).isEqualTo(gameStartMessage);
}
@BeforeEach와 @AfterEach를 통해 System.out에 대한 테스트를 위한 초기 설정을 진행한다.
👉 System.out.println()은 마지막에 줄바꿈 개행문자가 들어가서 테스트 문자열에 \n을 추가해줘야 한다.
2. System.in() 테스트 코드 작성하기
@ParameterizedTest
@ValueSource(strings = {"234"})
void 사용자가_입력한_수_입력_받기(String input) {
InputStream in = new ByteArrayInputStream(input.getBytes());
System.setIn(in);
String playerInputNumber = InputView.inputNumber();
assertThat(playerInputNumber).isEqualTo(input);
}
👉 InputStream in = new ByteArrayInputStream(input.getBytes()); 과
System.setIn(in) 을 통해 @ValueSource 로 입력한 값이 사용자 입력으로 들어가게 된다.
사용자 입력을 받는 InputView.inputNumber() 가 실행되면,
@ValueSource 로 입력한 값이 사용자 입력으로 입력되어 입력 테스트가 성공한다.
3. @ParameterizedTest - 여러 값을 한 번에 테스트 가능
@ParameterizedTest
@ValueSource(strings = {"23", "1", "2345", "12345235"})
void 사용자가_입력한_수_입력_받기_3자리가_아니면_예외_처리(String input) {
InputStream in = new ByteArrayInputStream(input.getBytes());
System.setIn(in);
assertThatThrownBy(InputView::inputNumber)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("[ERROR] : 입력하는 수는 3자리여야합니다.");
}
👉 @ValueSource 의 값인 “23”, “1”, “2345”, “12345235” 값을 하나의 테스트로 테스트하는 코드이다.
@ParameterizedTest 를 쓰지 않았다면, 파라미터들이 코드로 들어가서 코드가 길어졌을 것이다.
기능 검증 시 여러 값을 검증하고 싶을 때 쓰면 유용할 것 같다!
4. 클래스가 너무 많은 역할을 한다면, 설계와 달라지더라도 클래스를 분리하자.
이번 미션에서 MVC 패턴을 사용하여 Model-View-Controller 구조로 패키지를 구성하고
프로그램을 최종 완성하였는데, 컨트롤러가 담당하는 역할이
게임 진행 + 게임 로직 역할 이렇게 2가지로 있어서 리팩토링하기가 쉽지 않았다.
따라서, 구글링을 하던 중 현대에는 MVC 패턴에서 Service 계층을 추가하여 사용한다고 했는데,
이렇게 컨트롤러가 너무 많은 역할을 담당하기 때문에 로직 부분은 Service 계층에서 관리한다고 했다.
컨트롤러가 많은 역할을 담당하는 것을 느껴본 입장에서 서비스 계층이 왜 나왔는지 바로 이해하게 되었다.
따라서, 내 코드에도 서비스 계층을 추가하게 되었다.
* 서비스 계층 - Model 로직 처리해서 컨트롤러에게 전달
* 컨트롤러 계층 - 서비스 계층에게 받은 Model 데이터를 바탕으로 View에게 전달
* Model → 서비스 → 컨트롤러 → View → 사용자
참고 링크
MVC 패턴
우아한 테크 -MVC 패턴 리뷰- [레이어, MVC 패턴, 5레이어]
TDD 개발
JUnit, AssertJ 사용법, 메소드 정리
[Java] JUnit, AssertJ의 개념 및 기초적인 사용법 (단정문, 어노테이션)
System.out.println() 테스트 코드 작성
'우아한테크코스 5기 프리코스' 카테고리의 다른 글
우아한테크코스 5기 1차 합격 & 최종 코딩테스트 후기 (7) | 2022.12.18 |
---|---|
우아한테크코스 프리코스 4주차 후기 (0) | 2022.11.23 |
우아한테크코스 프리코스 3주차 후기 & 공부한 부분 (0) | 2022.11.16 |
우아한테크코스 프리코스 1주차 후기 & 공통 피드백 및 공부한 부분 (4) | 2022.11.02 |
우아한테크코스 5기 프리코스 시작! 프리코스 시 지킬 나만의 규칙 (0) | 2022.11.02 |