2주 차 때 읽은 Classes mdn 문서에 이어 JavaScript 객체 기본 mdn 문서를 읽었다.
class 구현에 조금은 익숙해진 기분
숫자 야구 게임 강의에서 캡틴 제이슨이 작성한 기능 목록을 참고하여 작성하였다.
2주 차에 기능 목록과 3주 차 기능 목록을 비교하면 다음과 같은 변화가 생겼다.
기능 목록을 너무 상세하게 작성하지 말라는 2주 차 공통 피드백을 염두에 두고 작성하였다. 확실히, 사용할 라이브러리와 출력할 문자열까지 포함되어 있던 2주 차 기능 목록에 비해 구현해야할 기능 위주로 간결하게 정리된 모습을 볼 수 있다.
다음과 같이 문자열, 숫자, 정규 표현식 등을 별도의 상수 파일을 만들어 관리하였다.
const MESSAGE = Object.freeze({
ENTER_PURCHASE_AMOUNT: '구입금액을 입력해 주세요.',
ISSUED_QUANTITY: '개를 구매했습니다.',
ENTER_WINNING_NUMBER: '당첨 번호를 입력해 주세요.',
ENTER_BONUS_NUMBER: '보너스 번호를 입력해 주세요.',
STATISTICS: '당첨 통계\n---',
HISTORY_FIFTH_PLACE: '3개 일치 (5,000원) - ',
HISTORY_FOURTH_PLACE: '4개 일치 (50,000원) - ',
HISTORY_THIRD_PLACE: '5개 일치 (1,500,000원) - ',
HISTORY_SECOND_PLACE: '5개 일치, 보너스 볼 일치 (30,000,000원) - ',
HISTORY_FIRST_PLACE: '6개 일치 (2,000,000,000원) - ',
HISTORY_COUNT: '개',
RETURN_TOTAL: '총 수익률은 ',
RETURN_PERCENT: '%입니다.',
});
const LOTTO = Object.freeze({
PRICE: 1000,
NUMBER_MIN: 1,
NUMBER_MAX: 45,
NUMBER_OF_NUMBERS: 6,
});
const PRIZE = Object.freeze({
FIFTH_PLACE: 5000,
FOURTH_PLACE: 50000,
THIRD_PLACE: 1500000,
SECOND_PLACE: 30000000,
FIRST_PLACE: 2000000000,
});
const REGEX = Object.freeze({
PURCHASE_AMOUNT: /^[1-9]{1}[0-9]*0{3}$/,
WINNING_NUMBER:
/^(([1-9]{1}|[1-3]{1}[0-9]{1}|4{1}[0-5]{1}),){5}([1-9]{1}|[1-3]{1}[0-9]{1}|4{1}[0-5]{1}){1}$/,
BONUS_NUMBER: /^([1-9]{1}|[1-3]{1}[0-9]{1}|4{1}[0-5]{1}){1}$/,
});
const FORMAT = Object.freeze({
COMMA: ',',
POINT: 1,
TO_PERCENT: 100,
});
const ERROR = Object.freeze({
ENTER_VALID_PURCHASE_AMOUNT: '[ERROR] 구입 금액은 1,000원 단위 입니다.',
ENTER_VALID_WINNING_NUMBER:
'[ERROR] 로또 번호는 1부터 45 사이의 쉼표로 구분된 숫자 6개여야 합니다.',
ENTER_VALID_BONUS_NUMBER:
'[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.',
ENTER_WITHOUT_REPETITION: '[ERROR] 중복되지 않는 숫자를 입력해 주세요.',
});
module.exports = { MESSAGE, LOTTO, PRIZE, REGEX, FORMAT, ERROR };
한 함수에서 안내 문구 출력, 사용자 입력, 유효값 검증 등 여러 일을 하고 있다면 이를 적절하게 분리하라는 피드백을 염두에 두고 코딩하였다.
기능 목록을 작성할 때 각 기능에 번호를 매기고, 커밋할 때 scope란에도 구현한 기능의 번호를 기입하였다. 그런데, 이 방식은 기능 목록에 변화가 생길 경우 각 기능의 번호가 하나씩 밀린다는 치명적인 문제가 생긴다는 사실을 인지하였다.
커밋 메시지 컨벤션에서 추천하는 대로 scope란에 파일명을 작성하는 방식을 택하였다.
파일명, 즉 클래스명을 지을 때 클래스의 역할에 대한 의도를 드러내야 하는 이유가 여기에도 있구나 하는 사실을 깨달았다. 클래스명을 잘 지어놓으니 scope란에 파일명을 기입하는 것만으로도 어떤 기능에대한 커밋인지가 명확히 드러난다.
제공된 Lotto
클래스를 활용해 구현해야 한다는 프로그래밍 요구사항이 있었다.
class Lotto {
#numbers;
constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}
validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
}
// TODO: 추가 기능 구현
}
module.exports = Lotto;
numbers
의 #
prefix를 변경할 수 없다는 요구사항을 당첨 번호를 private하게 관리할 수 있도록 구현하라는 의미로 해석했고, 마찬가지로 보너스 번호를 private하게 관리할 수 있는 Bonus
객체를 만들었다.
다음과 같은 시간 흐름으로 실행될 수 있도록 클래스와 매서드를 설계했다.
가로축의 제목행은 클래스명, 세로축은 시간 흐름(위 → 아래), 각 셀은 메서드명과 기능을 의미한다.
도메인 로직?
생소한 개념이다. 프리코스 커뮤니티에서 좋은 글을 추천받았다.
도메인? 도메인 주소?
비즈니스? 내 앱은 사업이 아니라 그냥 사이드 프로젝트인데..?소프트웨어 공학에서 도메인, 비즈니스라는 말은, '소프트웨어가 풀고자하는 현실 세상의 문제'를 가리킨다.
다시 말해 소프트웨어가 존재하는 이유, 목적이다.
은행 앱이라면, 금융 및 은행 업무가 도메인이다. 은행 앱이 해결하고자 하는 문제가 금융 업무를 스마트폰에서 처리할 수 있게 해주는 것이니까. 틱톡 같은 SNS라면 동영상 촬영, 감상, 댓글 및 공유일 것이다.
조금 더 이해를 돕자면, 반대로 공학/기술적인 문제에 속하는 것들은 대개 '도메인'과는 구별된다. 수많은 은행 사용자 데이터를 어떻게 효율적으로 저장할 것인가. 어떻게 고화질 동영상을 빠르게 로딩할 것인가? 같은 것들.
따라서 '도메인 로직'이나 '비즈니스 로직' 이라고 말할 때는, 그 '현실 세상의 문제'를 해결하는 코드를 의미한다. 도메인에 대한 해결책이나 솔루션이라고 할 수 있다.
소프트웨어는 다 현실 문제를 해결하는 거 아닌가?
그렇게 생각할 수도 있겠지만, 코드가 하는 일을 잘 생각해보자.
우리는 특정한 문제 영역에 대한 솔루션을 제공하는 코드 외에도 많은 코드를 써야한다. 그 코드를 가능하게 만들고, 입력과 출력을 처리하기 위한 로직들이다.
대표적으로 데이터베이스에 연결하고, 백엔드 서버와 통신하고, 사용자와 인터랙션하는 코드들이 필요하다. 이런 것들은 도메인 로직과 구분지어 어플리케이션 서비스 로직이라고 부른다.
도메인 로직과 어플리케이션 서비스 로직을 구분하는 하나의 척도는, 바로 '비즈니스 의사결정'이다.
이 코드가 현실 문제, 즉 비즈니스에 대한 의사결정을 하고 있는가?
이거 하나만 기억하면 된다.
도메인 로직은 현실 문제에 대한 의사결정을 하는 코드다. 나머지 코드는 그 결정을 위한 입력값을 만들어주거나, 그 결정의 결과물을 해석하고 보여주고 전파하는 코드다.
아하!
Eddy님께서 들어주신 예시를 바탕으로 내가 작성한 메서드 가운데 도메인 로직에 해당하는 메서드를 추려보았다. (도메인 로직인 이유와 함께 빨간색으로 표시하였다.)
2주 차 공통 피드백 中
함수를 열심히 분리한 덕에, 작은 단위의 테스트부터 만드는 일은 거져먹을 수 있었다.
문제를 작게 나누고, 작은 일을 하는 메서드의 테스트부터 만들어 나갔다.
예를 들어 Result 클래스의 compare 메서드는 큰 단위의 테스트를 필요로한다.
Lotto 클래스의 compare 메서드, Bonus 클래스의 compare 메서드를 호출해 다음과 같은 기능을 수행한다.
compare(winningNumber, bonusNumber) {
this.lottos.forEach((lotto) => {
const numberOfMatches = winningNumber.compare(lotto);
const hasBonus = bonusNumber.compare(lotto);
this.updateHistory(numberOfMatches, hasBonus);
});
this.printStatistics();
}
Lotto 클래스의 compare 메서드와 Bonus 클래스의 compare 메서드는 작은 단위의 테스트를 필요로한다.
Lotto 클래스의 compare 메서드
compare(lotto) {
let count = 0;
lotto.forEach((number) => {
if (this.#numbers.includes(number)) count += 1;
});
return count;
}
Bonus 클래스의 compare 메서드
compare(lotto) {
return lotto.includes(this.#number);
}
Lotto 클래스의 compare 메서드, Bonus 클래스의 compare 메서드 → Result 클래스의 compare 메서드 순으로 테스트를 작성하였다.
등 지난 주차에는 낯설었던 것들을 힘들이지 않고 하는 스스로를 발견하였다.