많이 늦어버린 1주차 회고이다. 😅
1주차 때는 회고라는 것을 쓸 정신도 없이 미션에 적응하기 바빴고, 프리코스가 끝나고 다시 1주차를 회고하는 것도 괜찮겠다는 생각이 들어 미루고 미루다 이제서야 쓰게 되었다. 최근에는 프리코스가 끝나고 미뤄왔던 업무를 쳐내고 있고, 혹시라도 코딩 테스트를 응시하게 될 경우를 대비해 우아한 테크코스 코딩 테스트 스터디를 진행하는 중이다.
1주차 회고를 작성하기 위해 1주차 때 기록헀던 것들을 보고 있자니, 했던 고민들이 굉장히 귀엽다 ,,? 는 느낌이 든다. 이 시기에는 자바 컨벤션도 모르고, 어떻게 커밋 메세지를 남겨야할 지, 어떻게 함수 분리를 할 지 몰랐기에 했던 고민들이 굉장히 기초적일 수밖에 없었던 것 같다. 지금 생각하면 당연한 것들도 이때는 하나의 고민 거리였고, 그때 열심히 고민하고 나만의 정답을 내린 후 이를 체화했기 때문에 지금 와서 당연한 것이 되었다고 할 수 있겠다.
내가 어떤 공부를 했었는 지 되돌아보자.
💪 프로젝트 개요
- 게임이 시작되면, 컴퓨터는 3자리의 수를 생성한다.
- 한 번의 게임에서 사용자는 3자리의 수를 입력한다.
- 컴퓨터는 입력된 수와 생성한 수를 비교하여 힌트를 제공한다.
- 같은 수가 같은 자리에 있다면 스트라이크
- 같은 수가 다른 자리에 있다면 볼
- 같은 수가 전혀 없다면 낫싱
- 입력된 수와 생성한 수가 일치하다면, 컴퓨터는 재시작 혹은 종료 여부를 사용자에게 묻고, 사용자는 선택한다.
📝 구현할 기능 목록
게임 시작 문구 출력 기능
- 게임 시작 문구를 상수화
- 게임을 시작하는 함수에서 출력
컴퓨터의 숫자 생성 기능
- 컴퓨터의 숫자를 생성하는 함수(generateRandomNumbers)를 구현
- camp.nextstep.edu.missionutils.Randoms 클래스를 사용
- 1부터 9까지의 수 중에서 중복 없이 3자리의 수가 만들어지도록 설계
- 3자리의 수가 담긴 List를 반환
- 생성한 숫자를 저장하는 함수 (saveNumbers)를 구현
- 3자리의 수를 저장하는 Numbers 클래스 구현
- Numbers 멤버 변수를 가지는 Computer 클래스 구현
- Computer 클래스의 멤버 변수를 수정하는 함수 구현
- ComputerController에서 생성한 숫자를 저장하는 함수 (saveRandomNumbers) 를 구현
- generateRandomNumbers를 호출해서 3자리의 수를 생성
- saveNumbers를 호출해서 Computer 객체의 값을 수정
게임 플레이어의 숫자 입력 기능
- 사용자로부터 숫자를 입력 받는 함수(readInputNumbers)를 구현
- camp.nextstep.edu.missionutils.Console 클래스를 사용
- 입력한 문자열을 반환
- PlayerController에서 입력값 유효성 검사 함수 구현
- 입력에 숫자 (1 ~ 9)가 아닌 다른 문자가 포함된 경우
- 입력의 길이가 3이 아닌 경우
- 같은 수를 중복해서 입력한 경우
- 3자리의 수가 담긴 List를 반환
힌트 출력 기능
- computer와 player의 숫자를 비교하는 함수 (compareNumbers) 구현
- 같은 수가 같은 자리에 있다면 스트라이크
- 같은 수가 다른 자리에 있다면 볼
- 스트라이크, 볼 개수에 맞추어 힌트를 출력하는 함수 (generateHint) 구현
- 볼과 스트라이크의 개수가 둘 다 1 이상이라면 "1볼 2스트라이크"와 같이 출력
- 볼의 개수가 1 이상이라면 "1볼"과 같이 출력
- 스트라이크의 개수가 1 이상이라면 "2스트라이크"와 같이 출력
- 볼과 스트라이크의 개수가 둘 다 0이라면 "낫싱"을 출력
- 컴퓨터에 저장된 숫자와 입력된 숫자를 비교하여 힌트를 생성하는 함수 구현
- Computer 객체에서 입력된 숫자를 받아 savedNumbers와 비교하여 힌트를 생성
- ComputerController에서 OutputView에 생성한 힌트를 제공
게임 재시작 혹은 종료 선택 기능
- 3개의 숫자를 모두 맞췄을 때, 게임의 재시작 혹은 종료 여부를 결정하는 함수 (askIfContinue) 구현
- 3개의 숫자를 모두 맞췄음을 검증하기 위한 함수 (checkCorrectAnswer) 구현
- 사용자에게 게임의 재시작 혹은 종료 여부를 물어보는 구문을 출력
- 사용자에게 1 혹은 2의 입력을 받고, 게임의 상태 변수를 변경한다.
- 유효성 검사
- 입력에 숫자 (1, 2)가 아닌 다른 문자가 포함된 경우
- 입력의 길이가 1이 아닌 경우
🛠 시스템 흐름도
🧐 새롭게 고민한 내용들
자바 컨벤션 ... 클린 코드 ...
개인적으로 1주차에서 가장 어려움을 많이 겪은 부분이다. 한 번도 코드 컨벤션을 지키면서 개발한 적 없었고, 클린 코드를 염두에 두면서 코드를 작성한 적이 없었기 때문이다. 함수 이름을 짓는 것도 사소한 개행도 모두 컨벤션에 맞추어 개발하려다보니 신경쓸 부분이 너무 많다고 느꼈다. 다행히 이 게시물에서 IDE 자체에서 컨벤션을 적용하는 것을 알려주어 많은 수고를 덜 수 있었는데, (감사합니다) 많은 노력에도 불구하고 코드 리뷰에서는 코드 스타일과 관련한 리뷰를 많이 받게 되었다. (감사합니다)
예를 들어, 이전에는 아래와 같이 for문을 사용해 리스트에 접근하고 처리하는 것으로 개발하였다.
이 코드에는 들여쓰기 레벨이 높고, 조건식이 어떤 기준을 의미하는 지 파악하기 어렵다는 문제가 있었다.
그래서 자바 스트림을 사용해 의미 있는 변수로 부울 식을 저장하고, 이를 통해 분기하는 것으로 수정하였다.
지금 생각해보니 함수의 분리가 더 세분화될 수 있을 것 같은데, 이 점은 아쉽긴 하다.
객체지향적인 설계로 책임 분리하기 (feat. MVC 패턴)
1주차 미션에서는 MVC 패턴을 처음 적용해보았다.
항상 스프링 부트 프로젝트를 진행하며 컨트롤러 - 서비스 - 도메인 - DB 구조의 레이어드 아키텍처만 경험해왔는데, 간단한 요구사항에 맞는 프로그램을 구현하기 위해 MVC 아키텍처 패턴을 사용하기로 결정하고 구현하였다. 이후로 아키텍처 패턴의 원칙에 준수하며 개발하기 위해 기능을 구현하기 위한 클래스의 역할 분리를 고민하였다.
예를 들어서 게임을 시작하는 컨트롤러 클래스의 생성자에서 게임이 시작된다는 메시지를 출력하고 싶다고 하자.
처음에는 다음과 같이 생성자에서 바로 출력 구문을 작성하는 방식으로 구현하였는데, 이 역할을 뷰에서 수행해야한다고 느껴 아래와 같이 뷰 클래스에 함수를 생성하였다.
그리고 생성자에서 이 함수를 호출하는 식으로 구현하여, 컨트롤러에서 출력에 대한 직접적인 역할을 수행하지 않도록 역할을 분리해주었다.
입력값 검증은 어디에서?
입력값 검증에 대한 부분은 아마 3주차까지 이어나간 고민이었던 것 같다. 처음에는 모든 검증 로직을 컨트롤러 레이어에서 수행하였다. 😮 숫자가 아닌 다른 입력이 들어왔을 때나, 중복된 숫자가 들어왔을 때의 검증 로직을 모두 NumberVaildator이라는 전역 패키지 안에 있는 클래스에서 작성하고, 이를 컨트롤러에서 입력값을 문자열로 받아왔을 때 함수를 호출하는 방식으로 구현하였다.
나중에 코드 리뷰를 통해 다시 생각해 본 결과, 그 검증 로직의 성격에 맞는 레이어에서 검증을 수행하는 것이 적합하다고 판단하게 되었다.
만약 단순히 입력값이 비어 있거나 요청된 입력과 다른 터무니 없는 입력인 경우, 입력 값에서 검증하는 것으로 처리한다. 그리고, 도메인 레이어에서 값을 저장하는 지점에서 검증을 수행할 필요가 있는 경우 도메인에서 검증한다. 예를 들어, 사람이 무작위 숫자를 입력하면 이를 도메인에서 저장하는데, 이 숫자가 주어진 범위 이상의 숫자라면 예외 처리하기 위해 숫자를 저장하는 래퍼 클래스를 생성한다. 그리고 클래스의 인스턴스 변수를 초기화 하는 시점에서 숫자의 범위를 검증하는 것이다. 또한, 그 검증 로직이 일회성이고 도메인에서 저장되는 값이 아닌 기타 로직이라면 컨트롤러 레이어에서 관리한다.
이렇게 나의 검증 로직에 대한 기준을 만들게 되었다. 하나 떠오르는 다른 문제는, 검증 로직이 여기저기 생기면서 중복된 로직이 여러 번 작성되거나 도메인에 대한 검증 역할이 커지게 되면서 무거워지는 현상이 생길 수 있다는 것이다. 이에 대해서도 고민을 많이 해본 결과 대응책은 아래 두가지이다.
- 도메인에서 처리할 검증은 도메인이 처리하되, 최대한 래퍼 클래스를 생성하여 그 로직을 분리한다. 예를 들어, 숫자를 단순히 원시값으로 저장하는 것이 아니라 래퍼 클래스로 생성하여, 숫자에 대한 검증 로직을 그 클래스 하위에서 수행한다.
- 중복되는 검증 로직은 글로벌한 패키지에 위치시켜 필요할 때마다 가져다 쓴다. 나는 이 방법을 적용하여 많은 코드 중복을 개선하였다. 만약 2번 이상의 로직이 발생하는 부분이라면 이를 클래스로 분리하고, 가져다 쓰는 방식을 사용하였다.
뷰는 어디까지 파싱을 해줘야 하나?
궁극적으로 뷰에서 입력 받은 문자열을 어디에선가는 객체로 변환해주어야 한다.
일반적으로 MVC 아키텍처 패턴에 의해 컨트롤러 레이어에서 도메인 객체를 생성하는데, 이 객체를 생성하기 위한 정보를 뷰에서 어디까지 가공해주어야 하는 지에 대한 고민을 하게 되었다. 처음에는 문자열 그대로 반환하였다가, 컨트롤러 레이어의 파싱 로직이 너무 거대해지는 듯한 느낌이 들어 도메인 객체를 생성해보기도 하였다.
그러다가 결론을 내리게 된 것은 뷰 레이어에서 정보를 나타내는 자료형의 가장 원시적인 값까지만 파싱을 하고 넘기는 것이었다. 예를 들어, 숫자에 대한 입력이 들어오면 int형 변수까지만 파싱을 하는 것이고, String, int가 섞인 입력이 주어진다면 String, int를 DTO에 담아서 보내는 것이다.
이렇게 하게 되면 컨트롤러에서는 딱 요청한 값의 형태로만 전달을 받을 수 있고, 바로 원시값을 꺼내서 도메인 객체를 생성하면 되는 것이다.
매직 넘버를 상수화하기 위한 방안
프로그램 내에서 사용되는 매직 넘버를 어떻게 확장성 있게 관리할 수 있을지에 대한 고민을 하게 되었다.
클래스 내에서 static final 키워드를 사용해 상수화하는 방법이 있었고, Enum 클래스에서 한번에 관리하는 방법이 있었다. 이 때는 Enum 클래스를 활용하는 것이 최선이라고 생각하여 시스템에서 사용되는 모든 숫자를 하나의 클래스에서 관리하였는데, 코드 리뷰를 거친 결과 그것 또한 안티 패턴일 수 있다는 결론을 내리게 되었다.
결과적으로 위와 같이 해당 클래스에서 사용하는 상수들을 상단에 정의하고, 이를 사용하는 방식을 채택하였다. 대신, 클래스를 잘게 분리하여 시스템 상수가 한 클래스에 너무 몰리지 않도록 관리하고 명확한 네이밍을 사용하여 클래스 자체의 코드가 길어지는 현상을 방지하기로 하였다.
이외에도 크고 작게 코드를 개선한 경험이 많은 1주차였는데, 평소 스프링 프레임워크를 사용하고 편리하게 개발하다가 순수 자바로 된 프로그램을 설계하려다 보니 고초를 많이 겪었던 미션이었다.
미션을 마주하고 스파게티 코드를 일부로 짜보기도 하고, 이를 개선해나가면서 불편해보이는 부분을 최적화하기 위해 고민하고 적용하는 연습을 통해 코드를 작성하는 기술을 많이 늘릴 수 있었다.
미션 구현 PR
https://github.com/woowacourse-precourse/java-baseball-6/pull/2634
'우아한테크코스 > 레벨0' 카테고리의 다른 글
[회고] 우아한 테크코스 최종 코딩테스트 대비 - 다리 건너기 (0) | 2023.12.13 |
---|---|
[회고] 우아한테크코스 1차 심사 합격 + 앞으로의 계획 (0) | 2023.12.13 |
[회고] 우아한테크코스 프리코스 4주차 회고 - 크리스마스 프로모션 (0) | 2023.11.17 |
[회고] 우아한테크코스 프리코스 3주차 회고 - 로또 게임 (0) | 2023.11.11 |
[회고] 우아한테크코스 프리코스 2주차 회고 - 자동차 경주 게임 (1) | 2023.11.01 |