- 자동차 경주 프로그램은 MVC 패턴에 따라 설계한다.
- YAGNI 원칙에 의거해, 필요 구현 요소를 밀도있게 구현하는 것을 목표로 한다.
-
[Game] 사용자에게 자동차 이름을
,(Comma)
를 기준으로 입력 받는다- [View] 사용자에게 자동차 이름 요청 폼을 출력하고, 문자열을 입력받는다.
- [InputValidator] 유효성 검사 : Comma로 끝나거나, whiteSpace를 포함하는 요청 예외처리한다.
- [Parser] 플레이어에게 입력받은 String을 Comma를 기준으로 파싱해 List 형태로 변환한다.
- [InputValidator] 유효성 검사 : List에 동일한 이름이 포함된 경우 예외처리한다.
-
[Game] 파싱된 List 문자열로 List를 멤버변수로 갖는 Cars 객체를 생성한다.
- [Cars] stream 문법을 사용해, 모든 List 원소를 바탕으로 Car 객체를 생성한다.
- [Cars] 위에서 만든 Car 객체를 List 형태로 리턴해 Cars의 멤버변수로 생성한다.
-
[Game] 사용자에게 라운드 횟수를 입력받는다.
- [View] 사용자에게 라운드 횟수 요청 폼을 출력하고, 숫자형 문자열을 입력받는다.
- [InputValidator] 유효성 검사 : 정수형이 아닌 숫자나 문자열이 요청되면 예외처리
- [InputValidator] 유효성 검사 : 1 미만의 숫자가 요청되면 예외처리
- [Parser] 라운드 횟수를 정수형으로 파싱해 컨트롤러에 리턴한다.
-
[Game] 라운드 횟수에 따라, 각 라운드를 반복 진행한다
- [Cars] for-each 문법을 사용해, 멤버변수의 모든 car를 대상으로 move 메소드를 호출
- [(Each) Car]
MoveCondition.create
함수를 호출해 1~9 사이의 랜덤 정수를 담은 조건 객체를 생성한다. - [(Each) Car]
MoveCondition.movable
함수를 호출해 이동 가능 여부 메세지를 보내고, 이동 여부에 따라 행동한다.
-
[Game] 각 라운드 종료
(모든 Each Car가 1회전 경주를 마친 상황)
- [Cars] buildRoundResponses 함수를 호출해 일급 컬렉션 멤버변수를 아래 과정을 통해 DTO로 변환한다.
- [RoundResponses] stream 문법을 사용해 모든 List를 순회하며 List를 생성한다.
- [RoundResponse] 각 RoundResponse는 name과 score를 가지며, getResponse 편의 메소드로
이름 : --
형태로 출력한다. - [RoundResponses] 레코드의 일급 컬렉션 멤버변수인 List를 순회하며 모든 자동차의 라운드 결과를
이름 : --
형태의String
으로 리턴한다. - [View] RoundResponses.getResponses()를 통해 각 라운드 진행 현황을 사용자에게 출력한다.
-
[Game] 모든 라운드 종료
(라운드 횟수 만큼 모든 자동차가 경주를 진행한 상황)
되었을 때- [Cars] buildFinalResponse 함수를 호출해 일급 컬렉션 멤버변수를 아래 과정을 통해 DTO로 변환한다.
- [FinalResponse] FinalResponse는 List winnerNames 가지며, getResponse 편의 메소드로
최종 우승자 : 해빈, 햅, 빈빈
형태의String
으로 리턴한다. - [View] FinalResponse.getResponse()를 통해 최종 우승자를 사용자에게 출력한다.
- [Console] Console.close() 명령어를 통해
Scanner.close()
기능을 수행한다.
-
Input Layer에서 유효성 검사를 진행하는게 맞을까에 대한 고민을 꾸준히 해왔습니다.
➡️ (View) 순수하게 문자열을 입/출력하는 기능 ➡️ (Validator) 기본 유효성, 자료형 검사 ➡️ (Parser) Validator를 통해 검증된 문자열을 자료형에 맞게 변환 `ex : String ➡️ List<String>`<br> ➡️ (Domain Constructor) 요구사항 예외처리 및 검증
-
위 순서로 View Layer에서 검증하지 않고, 별도의 Validator 도메인에서 유효성/자료형 검증을 진행합니다.
-
이렇게 기본적인 유효성 검증을 마치고, 개발 요구사항에 대한 검증은 일급컬렉션 및 각 도메인의 생성자에서 검증합니다.
-
위와 같은 방식의 설계가 MVC 패턴에 입각해 좋은 설계로 구성되었는지 봐주시면 좋을 것 같아요. ⸝⸝ʚ ̯ʚ⸝⸝
-
도메인 구조를 싱글톤으로 가져가지 않고, 설계하다 보니 테스트 코드 작성 간 어려움을 겪었습니다.
-
MovementCondition
의 경우, 개별Car
가 매 라운드마다 새로운MovementCondition
객체를 생성하고, 전진 여부를 요청합니다. -
MovementCondition은 Static 객체라, 해당 조건으로 분기하는 Car 객체는 일반적인 방법으로 테스트를 진행하기 어려웠습니다.
-
그래서 우회 방법으로
Randoms
객체를 모킹해, Random Generated Value를 모킹해서, 테스트를 진행했습니다. -
해당 방법은 좋은 테스트 방법이라고 생각되지 않습니다! 이 부분에 대해서 리뷰를 꼭 받아보고 싶어요. (어쩌면 초기 설계가 부적절 했을 수도 있다고 생각합니다.)
- 1주차 숫자 야구 Pull Request 는 많은 리뷰가 남겨져 있어 로드하는데 시간이 좀 걸립니다! (유니콘 414 에러가 뜨면서 PR창이 뜨지 않는다면 새로고침 해주세요!)
- 지난 리뷰에서 아쉬웠다고 리뷰를 받은 부분을 아래와 같이 반영하고자 노력했습니다.
- 리드미에 기능 명세를 강화해서, Controller Code Flow를 따라갈 수 있도록 설계했습니다.
- 기능, 비기능 요구사항을 나누고 설계 방향에 대한 명시를 강화했습니다.
- 플로우 차트를 추가해, 직관적으로 코드 플로우를 눈으로 따라갈 수 있도록 설계했습니다.
- 예외처리 메소드에서 사용할 검증 메소드(boolean)의 포스트컨디션에 따라, 긍정문으로 작성했습니다.
- isInvalidRoundCount, isExceedLength와 같이 직관적으로 해석되도록 정의했습니다.
- private 생성자 + 정적 팩토리 메소드 조합으로 명백한 의도가 표현되도록 노력했습니다.
- 메소드 명 자체가 해당 생성자의 의미를 직관적으로 담을 수 있도록 다음 표의 컨벤션 참고했습니다.
메소드명 | 역할 |
---|---|
from | 하나의 매개변수를 받아서 인스턴스를 생성 |
of | 여러개의 매개변수를 받아서 인스턴스를 생성 |
instance | 인스턴스를 반환하지만 동일한 인스턴스임을 보장하지 않음 |
create | 매번 새로운 인스턴스를 반환 |
getXxxx | 호출하는 클래스와 다른 타입의 인스턴스를 반환할때 사용 |
newXxxx | getXxxx와 같으나 매번 새로운 인스턴스를 반환 |
출처 : Static Factory Method 네이밍 컨벤션 - Tistory, Effective java
- 모델에서 View를 호출하지 않고, View
↔️ Controller↔️ Domain(Model) 의존 구조로 설계했습니다. - Domain 에서 Controller로 출력할 데이터를 DTO를 통해 전달하고
View에서는 DTO의 Response를 단순 출력하는 방식으로 설계했습니다.
- private 메소드로 선언해야 했음에도, 꼼꼼하게 체크하지 않은 탓에, public으로 불필요하게 개방적인 함수가 일부 존재했습니다.
- 이번 자동차 경주 미션에서는, 해당 사항을 꼼꼼하게 검증해, 함수의 권한을 적절하게 관리하도록 통제했습니다.
실제로 필요할 때 무조건 구현하되, 그저 필요할 것이라고 예상할 때에는 절대 구현하지 말라.
출처 : YAGNI - Wikipedia
- 기존 1주차에 OCP를 준수하기 위해 작성했던 코드에서, 과제 요구사항과는 거리가 있다는 리뷰를 받았습니다.
- 반면 긍정적인 리뷰도 있었습니다. 확장성 있는 설계로 좋은 리뷰를 남겨주신 분도 계셨습니다.
- 이번 미션은 자동차 경주 게임의 본질을 꾸준히 되새기며 YAGNI 설계로 구성했습니다.
- 선언한 모든 변수에 대해서, 누구나 읽어도 직관적으로 해석이 가능한 변수 명으로 설정하고자 노력했습니다.
- 기존 리뷰 받았던
xxFlag
,xxOption
,xxOrNot
과 같이 해석에 혼동을 줄 수 있는 변수와 클래스명 사용을 지양했습니다.
클래스 내부 선언 순서 컨벤션
- Static Variable :
public -> protected -> private
- Member Variable :
public -> protected -> private
- Constructor
- Static Method
- Other Method : 기능 및 역할별로 분류하여 기능을 구현하는 그룹별로 작성
- Standard Method : toString, equals, hashcode 와 같은 메소드
- Getter / Setter : 클래스의 가장 하단 부분에 위치
출처 : Are there Any Java method ordering conventions? - Stack Overflow
자바에서 예외처리는 예외 복구, 예외 회피, 예외전환을 방식이 각각 있습니다.
종류 | 역할 |
---|---|
예외 복구 | try catch -> 예외를 처리하고 정상 로직 처리 |
예외 회피 | throw 로 상위 컨텐츠에게 위임 |
예외 전환 | checkedExcpetion 을 unckeckedException 으로 변경하여 리턴 |
예외는 정적 팩토리메소드 보다는 있는 그대로를 보여주는 것이 더 정확하다고 생각합니다. Exception 에 대한 처리는 정확하게 처리해야 서버의 죽음을 막을 수 있고, 에러에 대한 정보가 정확 해야지 추후 원인 판단에 빠른 해결책을 제공할 수 있을거라 봅니다.
출처 : 1주차 숫자야구 코드리뷰 - @IMWoo94
- 이번 미션에서
RacingCarException
이라는 전역 예외를 공통으로 던지도록 설계했습니다. - 에러 메세지를 정적으로 관리하고, 각 테스트 케이스에서 JUnit5의
hasMessageContaining
메소드를 활용해 해당 예외 발생 여부와 Containing ErrorMessage 여부를 이중으로 검증해, 간결하면서 신뢰도 높은 테스트를 만들고자 했습니다. IllegalArgumentException
을 원시 형태로 발생시키는 것 보다,
전역에서 에러를 공통 규격으로 관리하는 것이 더욱 확장성이 높다고 판단합니다.- 서버가 예외의 상태를 파악할 수 있도록, 추후 Logging 방식을 통해 말씀 주신 문제를 해결할 수 있다고 생각합니다.
- 기존에 상속과 다형성을 목표로 작성했던 protected 생성자에 대해 리뷰를 받았습니다.
- 이번 과제에서 상속과 다형성으로 최종 개발되지 않은 클래스는 private 생성자를 사용했습니다.
- private 생성자를 기반으로, public Static Factory 메소드로 객체를 생성합니다.
- JPA에서 사용했던 경험을 바탕으로, 무의식적으로 사용했던 행동을 반성하게 되었습니다 :)
출처 : [JPA] JPA에서 Entity에 protected 생성자를 만드는 이유 - Velog
- 일부 검증 메소드(
isExitFlag
,isRestartFlag
)와 같은 함수는 값에 대한검증
의 성격을 띄지 않는다는 리뷰를 받았습니다. - 함수의 위치와, 이름, 그리고 이를 담는 패키지 이름까지, 조금 더 함수의 역할을 담을 수 있도록 설계했습니다.