🎯 주제
우아한테크코스 6기의 최종 코딩테스트 미션인 온콜이다.
비상 근무를 배정할 월과 요일을 입력하고 평일 근무자와 휴일(법정 공휴일, 주말) 근무자들을 모두 입력받으면 순번대로 근무자를 배치해주는 미션이다.
최종 코딩 테스트에서는 5시간이라는 시간 제한으로 인해 완벽하게 기능을 구현하지 못했으나, 부족했던 부분과 개선할 수 있는 부분들을 다시 고민하고 리팩토링하려고 한다.
🤔 해결하지 못한 부분
📝 기능 요구 사항
평일이면서 법정공휴일의 경우에만 휴일을 표기를 해야 한다.
평일이면서 법정공휴일의 경우에만 요일 뒤에 (휴일) 표기를 해야 한다.
최종 코딩 테스트 때 해당 요구 사항을 꼼꼼하게 파악하지 못한 채 법정공휴일의 경우에만 (휴일)을 표기하도록 로직을 작성했었다.
const HOLIDAYS = [
[1, 1], // 1월 1일 (신정)
[3, 1], // 3월 1일 (삼일절)
[5, 5], // 5월 5일 (어린이날)
[6, 6], // 6월 6일 (현충일)
[8, 15], // 8월 15일 (광복절)
[10, 3], // 10월 3일 (개천절)
[10, 9], // 10월 9일 (한글날)
[12, 25], // 12월 25일 (크리스마스)
];
#isHoliday(month, day) {
return HOLIDAYS.some(([hMonth, hDay]) => hMonth === month && hDay === day);
}
법정공휴일을 [month, day] 형태로 HOLIDAYS라는 상수로 정의하고, 이 중 하나라도 해당되면 true를 반환하는 로직을 작성했었다.
하지만, 이는 평일이면서 법정공휴일이라는 요구사항을 충족시키지 못했기 때문에 수정이 필요했다.
class DateHelper {
static getStartingDayIndex(week, weekList) {
return weekList.indexOf(week);
}
static getMonthAndDay(month, day, dayOfWeek, isHoliday) {
return `${month}월 ${day}일 ${dayOfWeek}${isHoliday ? MESSAGE.print.holiday : ''}`;
}
static isWeekendOrHoliday(weekOfIndex, month, day, holidays) {
return weekOfIndex === CONSTANTS.week.sunday || weekOfIndex === CONSTANTS.week.saturday || this.isWeekdayHoliday(weekOfIndex, month, day, holidays);
}
static isWeekdayHoliday(weekOfIndex, month, day, holidays) {
return weekOfIndex >= CONSTANTS.week.monday && weekOfIndex <= CONSTANTS.week.friday && holidays.some(holiday => holiday.month === month && holiday.day === day);
}
}
export default DateHelper;
이를 위해, 날짜와 관련한 로직들은 모두 DateHelper 라는 클래스로 분리하여 관리하였다.
isWeekdayHoliday메서드에서 요일에 해당하는 index인 weekOfIndex가 월요일에서 금요일(평일)이면서 법정공휴일 중에 하나라도 속한다면 true를 반환하도록 메서드를 수정하였다.
#generateDutySchedule(month, startingDayIndex, dayOfMonth) {
return Array.from({ length: dayOfMonth }, (_, i) => {
const weekOfIndex = (i + startingDayIndex) % CONSTANTS.week.length;
const monthAndDay = DateHelper.getMonthAndDay(month, i + 1, CONSTANTS.week.list[weekOfIndex], **DateHelper.isWeekdayHoliday(weekOfIndex, month, i + 1, CONSTANTS.holidays)**);
const worker = this.#assignWorkerAndHandleNextDay(weekOfIndex, i, month, startingDayIndex);
return `${monthAndDay} ${worker}`;
});
}
DateHelper클래스 내부에 있는 getMonthAndDay 메서드는 월과 일, 휴일의 유무를 문자열로 반환하는 메서드이다.
인자로 DateHelper내부에 있는 isWeekdayHoliday메서드를 호출하여 휴일이면서 법정공휴일인지 해당하는 booelan값의 isHoliday를 보낸다.
getMonthAndDay는 isHoliday가 true이면 (휴일)을 반환하고 그렇지 않을 경우 공백을 반환하도록 처리하였다.
비상 근무자는 연속 2일은 근무할 수 없다.
근무자 보호와 비상 근무 운영의 효율을 위해, 비상 근무자는 어떤 경우에도 연속 2일은 근무할 수 없다. 순번상 특정 근무자가 연속 2일 근무하게 되는 상황에는, 다음 근무자와 순서를 바꿔 편성한다.
최종 코딩 테스트때 가장 많은 시간을 소비한 부분은 인덱스를 swap 하는 부분이었다.
변경사항이 적용되지 않아, 5시간의 절반 가량을 이 부분에 집중한 것 같다.
결국 문제를 해결하지 못하고 제출할 수밖에 없었지만, 어느 부분에서 잘못되었는지 이번에 다시 한번 확인해볼 필요가 있다고 생각하였다.
if (isHolidayToday || currentDayOfWeek === '토' || currentDayOfWeek === '일') {
if (this.#weekdayNickname[weekdayIndex] === this.#weekendNickname[weekendIndex]) {
const temp = this.#weekendNickname[weekendIndex];
this.#weekendNickname[weekendIndex] = this.#weekdayNickname[weekdayIndex];
this.#weekdayNickname[weekdayIndex] = temp;
}
dutyPerson = this.#weekendNickname[weekendIndex];
weekendIndex = (weekendIndex + 1) % this.#weekendNickname.length;
} else {
while (
this.#weekdayNickname[weekdayIndex] === lastDutyPerson ||
this.#weekdayNickname[weekdayIndex] ===
this.#weekdayNickname[(weekdayIndex + 1) % this.#weekdayNickname.length]
) {
const temp = this.#weekdayNickname[weekdayIndex];
this.#weekdayNickname[weekdayIndex] =
this.#weekdayNickname[(weekdayIndex + 1) % this.#weekdayNickname.length];
this.#weekdayNickname[(weekdayIndex + 1) % this.#weekdayNickname.length] = temp;
}
dutyPerson = this.#weekdayNickname[weekdayIndex];
weekdayIndex = (weekdayIndex + 1) % this.#weekdayNickname.length;
}
위는 기존에 작성한 코드이다.
현재 주말 혹은 법정공휴일인 것을 확인하고 평일 근무자와 휴일 근무자가 같으면 서로 swap도록 하였다. 그렇지 않은 경우도 마찬가지이다.
먼저 조건문에서부터 틀렸다는 것을 알게되었다.
isHolidayToday는 isHoliday메서드를 호출한 boolean값인데, 기존의 isHoliday메서드는 법정공휴일만 판단하므로 잘못된 조건임을 알 수 있다.
또한, swap의 대상이 잘못되었다.
요구사항에서 제시한 내용은 그 다음 순번 근무자와 swap하는 것이었으나, 기존에 작성한 코드는 연속된 근무자들끼리 서로 바꾸는 로직을 작성한 것이다.
문제점을 파악한 후 다음과 같이 리팩토링하였다.
#generateDutySchedule(month, startingDayIndex, dayOfMonth) {
return Array.from({ length: dayOfMonth }, (_, i) => {
const weekOfIndex = (i + startingDayIndex) % CONSTANTS.week.length;
const monthAndDay = DateHelper.getMonthAndDay(month, i + 1, CONSTANTS.week.list[weekOfIndex], DateHelper.isWeekdayHoliday(weekOfIndex, month, i + 1, CONSTANTS.holidays));
const worker = this.#assignWorkerAndHandleNextDay(weekOfIndex, i, month, startingDayIndex);
return `${monthAndDay} ${worker}`;
});
}
#assignWorkerAndHandleNextDay(weekOfIndex, i, month, startingDayIndex) {
let worker = '';
if (DateHelper.isWeekendOrHoliday(weekOfIndex, month, i + 1, CONSTANTS.holidays)) {
worker = WorkerAssigner.assignWorker(this.#weekendNicknames, this.#weekendIndex, this.#weekdayIndex);
WorkerAssigner.handleNextDay(worker, this.#weekdayNicknames, this.#weekdayIndex, (i + 1 + startingDayIndex) % CONSTANTS.week.length, WorkerAssigner.swapIfSameWorker);
this.#weekendIndex += 1;
return worker;
}
worker = WorkerAssigner.assignWorker(this.#weekdayNicknames, this.#weekdayIndex, this.#weekendIndex);
WorkerAssigner.handleNextDay(worker, this.#weekendNicknames, this.#weekendIndex, (i + 1 + startingDayIndex) % CONSTANTS.week.length, WorkerAssigner.swapIfSameWorker);
this.#weekdayIndex += 1;
return worker
}
EmergencyDutyScheduler 클래스의 일부 메서드를 가져온 것이다.
generateDutySchule을 통해 비상 근무표를 생성하도록 하고 처음에는 DateHelper에 있는 getMonthAndDay를 통해 날짜를 가져온다.
이후 assignWokrkerAndHandleNextDay 메서드를 호출해서 현재 날짜를 기반으로 평일과 휴일을 판단하고 다음날이 평일 ⇒ 휴일, 휴일 ⇒ 평일일 경우 근무자가 서로 같은지를 확인하여 그 다음 근무자의 순번으로 swap후 woker를 업데이트하여 반환하도록 수정하였다.
class WorkerAssigner {
static assignWorker(nicknameArray, primaryIndex) {
return nicknameArray[primaryIndex % nicknameArray.length];
}
static handleNextDay(worker, otherNicknames, otherIndex, nextDayIndex, swapIfSameWorker) {
if (nextDayIndex) swapIfSameWorker(worker, otherNicknames, otherIndex);
}
static swapIfSameWorker(worker, nicknameArray, index) {
if (worker === nicknameArray[index % nicknameArray.length]) {
const currentIndex = index % nicknameArray.length;
const nextIndex = (index + 1) % nicknameArray.length;
const temp = nicknameArray[currentIndex];
nicknameArray[currentIndex] = nicknameArray[nextIndex];
nicknameArray[nextIndex] = temp;
}
}
}
export default WorkerAssigner;
worker를 판단하는 로직들은 WorkerAssigner 클래스로 분리하여 관리하였다.
assignWorker를 통해 평일 혹은 휴일 근무자들 중에 특정 index에 해당하는 근무자를 가져오도록 한다.
이후 swapIfSameWorker를 호출하여 현재 근무자와 다음 근무자가 서로 같을 경우를판단하여 swap하도록 한다.
🛠️ 개선할 수 있는 부분
reTry.js
평일 순번 또는 휴일 순번의 입력 값이 올바르지 않은 경우, '평일 순번'부터 다시 입력 받는다.
const reTry = async (callback, onError) => {
while (true) {
try {
return await callback();
} catch ({ message }) {
Console.print(message);
}
}
};
기존에 작성한 reTry 메서드이다. callback함수를 받으면 에러가 발생하지 않을 때까지 callback을 수행하고 에러가 발생하면 에러메시지를 콘솔에 출력하도록 한다.
그러나 위와 같은 요구사항이 주어지면 에러가 발생하였을 때 에러메시지를 콘솔에 출력한 후 평일 근무자를 입력 받는 메서드를 호출하도록 해야한다.
async #inputWeekendNickname() {
while (true) {
try {
const weekendNickname = await this.#inputView.readWeekendNickname();
this.#emergencyDutyService.setWeekendNickname(weekendNickname);
return this.#printResult();
} catch ({ message }) {
Console.print(message);
return this.#inputWeekdayNickname();
}
}
}
해당 코드는 주말 근무자들을 입력받는 기능을 하는 메서드이다.
당시 시간이 부족해서 기존의 reTry메서드를 수정하지 않고 입력을 받는 메서드에 내부의 로직을 입맛대로 작성하였다.
그러나 문제는 해당 로직은 controller내부에서 수행하는데 출력을 담당하는 부분을 controller내부에서 수행하므로 MVC패턴으로서의 의미와 부적합하다.
때문에 reTry 메서드 로직을 다시 수정해야할 필요가 있다.
import { Console } from '@woowacourse/mission-utils';
const reTry = async (callback, onError) => {
while (true) {
try {
return await callback();
} catch ({ message }) {
Console.print(message);
if (onError) return await onError();
}
}
};
export default reTry;
해당 코드는 reTry메서드를 다시 개선한 코드이다.
휴일 근무자를 잘못 입력했을 경우 평일 근무자를 입력 받는 메소드를 호출하도록 해야하므로 onError라는 콜백함수를 받아 onError 콜백함수가 존재할경우 해당 콜백함수를 호출하도록 로직을 변경하였다.
async #inputWeekendNicknames() {
return reTry(
async () => {
const weekendNicknames = await this.#inputView.readWeekendNicknames();
this.#emergencyDutyService.setWeekendNicknames(weekendNicknames);
return this.#printEmergencyDury();
},
() => this.#inputWeekdayNicknames(),
);
}
이처럼 reTry메서드에 inputweekdayNicknames 메서드를 호출하는 콜백함수를 인자로 넘겨주어 에러 발생시 평일 근무자를 다시 입력 받도록 로직을 개선하였다.
💡 회고
드디어 최종 코딩테스트까지 마무리하였다.
코딩 테스트 당시 짧은 시간의 압박과 중간에 원하는 결과가 나오지 않아 당황스러운 나머지 많은 기량을 못 보여준게 아쉽기도 하지만, 이번 기회를 통해 진짜 나의 실력이 어느 정도 인지를 빠르게 파악할 수 있었던 것 같다.
회고를 하면서 느끼는 거지만, 아직 까지 배울점이 많다는 것을 느꼈다. 그리고 진정한 몰입을 통해 배우는 즐거움이 무엇인지를 깨달을 수 있었다.
한 학기 동안 몰입한 나에게 너무 수고했다는 말을 전하고 싶다.
최종 발표일에 좋은 결과가 있기를 바라며 당분간은 휴식을 좀 취해야겠다.
정말 고생 많았어! 👍
🏃♂️ 구현 코드 보러가기
[최종 코딩테스트 온콜 미션 리팩토링] 리뷰용 PR입니다. by llbllhllk · Pull Request #1 · llbllhllk/javascr
📄 기능 목록 입력 월과 시작 요일을 입력 받는 기능 평일 비상 근무 사원들을 입력 받는 기능 주말 비상 근무 사원들을 입력 받는 기능 출력 비상 근무표를 출력하는 기능 기능 비상 근무자들
github.com
👀 1차 합격과 최종 코딩테스트 후기 보러가기
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기
🎉 1차 결과 발표 12월 11일 오후 3시에 1차 합격자가 발표하는 날이었다. 그날 컴퓨터네트워크 시험이었는데 시험 시작이 마침 오후 3시였다. 시험 시작과 동시에 핸드폰으로 메일 알림이 왔고
llbllhllk.tistory.com
'🚀 우아한테크코스 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기 (4) | 2023.12.17 |
---|---|
[우아한테크코스 6기] 프리코스 4주차 - 크리스마스 프로모션 회고록 (0) | 2023.11.20 |
[우아한테크코스 6기] 프리코스 3주차 - 로또 회고록 (0) | 2023.11.10 |
[우아한테크코스 6기] 프리코스 2주차 - 자동차 경주 회고록 (0) | 2023.11.02 |
[우아한테크코스 6기] 프리코스 1주차 - 숫자 야구 회고록 (0) | 2023.10.26 |