공통 피드백과 새롭게 추가된 학습 목표를 정리하고, 다른 분들의 코드를 보면서 정리한 부분을 추가해 나만의 학습 목표를 만들었다.
1. 클래스 문법 활용
2. 테스트
3. 가독성과 함수 역할 분배
이번 미션의 특징은 Lotto 클래스가 주어졌다는 것이었다. 그리고 유일한 private 멤버 변수인 numbers를 수정하지 않고 프로그램을 구성하는 것이 중요한 미션이었다.
다른 분들의 코드를 보면서 이번 미션에서는 private 선언 + 접근자 프로퍼티 등 클래스 문법을 적극적으로 사용해 캡슐화를 지켜야겠다는 목표를 세웠는데, 이에 부합하는 조건이라서 좋았다.
그래서 Lotto를 아주 심플하게 유지했다. 다만 Lotto에 필드를 추가하지 말라는 조건이 있어서 접근자 프로퍼티는 사용하지 않고 getter 함수를 따로 만들어 사용했다. 다른 클래스에는 위의 내용을 적용했다.
class Lotto {
#numbers;
constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers.sort((a, b) => a - b);
}
#validate(numbers) {
//... validator 로직
}
getNumbers() {
return this.#numbers;
}
}
🥲 아쉬운 점
3주 차 피드백에 '객체는 객체스럽게 사용한다'는 내용이 있었다. 객체에 상태 값만 넣지 않고 로직에 대한 구현이 있어야 한다는 내용이었는데, 딱 내 코드여서 매우 뜨끔했다.
로또 번호를 비교하는 로직이 WinningLotto에 있어서 Lotto는 값만을 가지도록 만들어지게 됐는데, 피드백을 보니 해당 내용을 Lotto 클래스에 넣었다면 더 좋았을 것 같다는 생각을 했다.
이번 미션 또한 MVC 패턴을 사용해서 구현했는데, 지금까지와는 다르게 Model 코드가 여러개가 필요했다. 대표적으로 Lotto 클래스, 상금을 판단하는 Prize 클래스 등이 있었는데, 위 클래스들을 컨트롤러에서 직접 관리하자니 필요 이상으로 많은 정보를 알고 있어야 한다는 생각이 들었다. 그래서 컨트롤러와 모델들을 중개해 주는 모델을 만들었고, 그것이 LottoMachine이 되었다.
class LottoMachine {
// 로또 발행 완료 여부
isIssueOver() {}
// 일반 로또 발행
issueLotto() {}
// 당첨 로또 발행
issueWinningLotto(winningNumbers, bonusNumber) {}
// 당첨 결과
async getResults() {}
// Prize 클래스를 활용해서 상금 계산
async calculatePrizeResult() {}
}
이렇게 클래스를 분리하고 나니 도메인 로직과 서비스 로직이 분리가 되어 테스트하기 더욱 쉬워졌다.
위와 같은 피드백이 도착했을 때 '도메인 로직이 뭔데?..'라는 궁금증이 생겼다. 그러다가 이 글을 발견했다.
위 블로그에서 말하는 도메인 로직이란 '비즈니스에 대한 의사결정을 하는 코드'이다. '송금'이라는 기능이 있을 때 계좌 잔액을 확인하는 코드, 수수료를 결정하는 코드, 사용자의 잔액을 DB에 저장하는 코드 등이 있을 것이다. 여기서 의사결정을 하는 코드는 계좌 잔액이 충분한지 확인하고, 수수료를 계산하는 코드로 볼 수 있다. 즉, 해당 로직이 도메인 로직이 된다.
그렇다면 도메인 로직이 아닌 로직은 뭘까? 바로 서비스 로직이다. 이 코드들은 도메인 로직이 의사결정을 할 수 있도록 입력을 제공하고, 결과를 DB/UI에 업데이트하는 역할을 맡는다.
이번 로또 미션의 도메인 로직과 서비스 로직을 분리하면 다음과 같을 수 있다.
도메인 로직
서비스 로직
나의 경우 도메인 로직은 모델에서, 서비스 로직은 Controller와 View에서 처리하도록 했다. 따라서 모델에 대해서만 테스트 코드를 작성했는데, 클래스 분리 + 도메인 로직 분리가 같이 이루어지니 더욱 작은 단위의 함수를 작성하는 것이 가능해져 수월하게 테스트할 수 있었다.
테스트를 위해 필요한 변수나 상수가 많아질 수록 코드는 뚱뚱해지고 정작 중요한 테스트 내용이 잘 보이지 않는 걸 경험했다.. 그래서 테스트 코드의 가독성을 향상시킬 수 있는 방법을 찾아봤는데, 바로 given-when-then이었다! (이것도 우테코 테코톡의 도움을 받았다. 감사해요 테코톡~)
이번 미션에서는 유효하지 않은 입력이 들어왔을 때 Error를 던지는 것에 그치지 않고 다시 입력을 받아야 하는 요구사항이 추가됐었다. 이 부분을 컨트롤러에서 try/catch 문을 사용해 처리했는데, 함수 안에 코드 블록이 겹치니 indent depth가 하나 더 늘어나고 코드가 반복되는 것이 만족스럽지 않았다.
어떻게 하면 코드의 반복을 줄일 수 있을까 고민하다가 자바스크립트에서 함수가 일급 객체이기 때문에 매개변수로 전달될 수 있다는 걸 떠올렸다! 그래서 에러가 반환됐을 때 재시도하는 로직을 handleExeption이라는 유틸리티 함수로 분리하고, 실행할 함수를 매개변수로 전달하는 구조를 만들어 컨트롤러의 코드를 간단하게 유지할 수 있었다.
// utils.js
export const handleException = async (action) => {
while (true) {
try {
await action();
break;
} catch (err) {
Console.print(err.message);
}
}
};
// LottoController.js
await handleException(async () => this.#generateLottoMachine());
개인적으로 가장 뿌듯한 부분..!
코드 리뷰를 받으면서 while(true)로 작성한 이유를 질문 받았었는데, 재귀호출보다 스택 오버플로우가 발생할 가능성이 낮고, 루프를 명시적으로 제어할 수 있다는 점에서 장점이 있다고 생각했다!
매번 새로운 미션을 받을 때 마다 추가되는 학습 목표를 보며 내 머릿속을 들여다 보고 설계한게 아닐까 의심한다..
이전 미션에서 아쉽다고 느껴진 로직을 개선할 수 있는 방법을 생각해 놓으면, 곧바로 다음 미션의 학습 목표로 제시된다. 정말이지 기획한 분들의 노하우가 응집된 좋은 프로그램이라고 다시 한번 감탄한다.
이번 미션은 이전 미션들과 다르게 모델 클래스가 여러개 생길 수 밖에 없어서 이것들을 어떻게 하면 잘 관리할 수 있을까.. 고민하며 보냈다. 또 클래스를 분리하면서 최대한 필드에 직접 접근하는 경우를 만들지 않으려고 했는데, 아직 이 부분에서 미흡해서 몇 개는 퍼블릭으로 선언했다.😅 4주차 미션을 통해서 더 많이 고민하고 발전시키려 한다.
그리고 3주 차는 정말 많은 분들이 좋은 피드백을 주셔서 이것만 따로 정리하는 글을 만들어 보려고 한다! 지금까지 딱 한 분에게 코드 리뷰를 받았던 것을 생각하면 매우 감격이었다..🥹 이 에너지로 4주 차엔 더 좋은 코드를 작성해야지!