어느덧 우테코 프리코스 마지막 주차에 들어섰다. 4 주라는 시간이 짧게 느껴질 정도로 정말 많은 것을 공부했고 코드를 짜는 방식도 발전했다고 느낀다. 혼자 개발을 할 때는 제약 사항없이 떠오르는대로 타이핑했다면 최근에는 리팩토링에 조금 더 신경을 써서 가독성이 좋고 유지보수가 편한 코드를 만드려고 노력하고 있다.
이 글에서는 함수의 역할을 단순화하고 길이를 줄이는 방법에 대해 설명하려 한다. 우테코 프리코스 4 주차 미션에서는 아래와 같은 제약 사항이 있었다.
⚠️ 제약 사항
- 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
함수의 길이가 길어지는 예시 중 하나인 분기 처리문을 그동안 사용했던 if/else
외의 다른 방법을 사용하여 구현해보자.
#0에서 제시한 제약 사항에 대해 ESLint 규칙을 설정하면 아래와 같다.
rules: {
"max-depth": ["error", 2],
"max-lines-per-function": ["error", 10],
}
아래와 같은 INPUT_TYPE
에 따라 각각 다른 함수를 호출하는 분기 처리를 한다고 가정해보자.
const INPUT_TYPE = {
SIZE: "size",
MOVING: "moving",
GAME_COMMAND: "gameCommand",
};
사용자로부터 받은 input
값을 처리하는 handleInput
함수는 아래와 같이 조건문으로 나타낼 수 있다. INPUT_TYPE
의 문자열을 조건으로 받아 각각 다른 함수를 직접 호출한다. 이 때 Validation
함수에서 에러가 발생할 경우 try-catch 문을 사용해 에러를 출력하고 각각의 폴백 함수를 호출한다.
안타깝게도 이 코드는 길이도 길고, 분기 처리 이후 try-catch 로직이 반복된다는 문제가 있다.
반복되는 로직
inputType 분기처리 > tryValidation & 다음 함수 호출
/ catcherror 메시지 출력 & 폴백 함수 호출
handleInput(input, inputType) {
if (inputType === "size") {
try {
Validation.size(input);
this.createBridge(input);
} catch (errorMsg) {
OutputView.printError(errorMsg);
this.getBridgeSize();
}
} else if (inputType === "moving") {
try {
Validation.moving(input);
this.checkmove(input);
} catch (errorMsg) {
OutputView.printError(errorMsg);
this.getPlayerMove();
}
} else if (inputType === "gameCommand") {
try {
Validation.gameCommand(input);
this.Validation.checkRetryInput(input);
} catch (errorMsg) {
OutputView.printError(errorMsg);
this.getPlayerMove();
}
}
}
If/else 대신 switch 문을 사용해도 분기 처리 이후 try-catch 로직이 반복될 것을 예상할 수 있다. 또한 케이스가 많아질수록 함수의 길이는 점점 더 길어질 것이다..
handleInput(input, inputType) {
switch (inputType) {
case 'size':
...
[break]
case 'moving':
...
[break]
case 'gameCommand':
...
[break]
default:
...
[break]
}
그렇다면 어떤 방법을 사용해야 handleInput
함수의 분기 처리를 10 줄 이내로 만들 수 있을까? 나는 다음의 [프롱트] 유튜브 참고 영상을 보고 문자열이 아니라 함수도 객체로 맵핑할 수 있다는 것을 배웠다. Object mapping을 사용하면 분기 처리를 간결하게 만들어 메인 로직을 간단하게 정리할 수 있다.
각 분기 처리에서 중복되는 함수를 Validation
, INPUT_TRY_FN
, INPUT_CATCH_FN
객체로 만들고, inputType
을 key 값으로 받아 각 함수를 실행한다.
handleInput(input, inputType) {
try {
Validation[inputType](input);
INPUT_TRY_FN[inputType](this, input);
} catch (errorMsg) {
OutputView.printError(errorMsg);
INPUT_CATCH_FN[inputType](this);
}
}
중복되는 try-catch 로직은 INPUT_TRY_FN
INPUT_CATCH_FN
객체에 각 함수를 저장한다. inputType
를 객체의 key 이름으로 하여 타입에 따라 해당 함수가 실행되도록 하는 것이다.
const INPUT_TRY_FN = {
size(gamePresenter, size) {
gamePresenter.createBridge(size);
},
moving(gamePresenter, selectedMove) {
gamePresenter.checkmove(selectedMove);
},
gameCommand(gamePresenter, retry) {
gamePresenter.checkRetryInput(retry);
},
};
const INPUT_CATCH_FN = {
size(gamePresenter) {
gamePresenter.getBridgeSize();
},
moving(gamePresenter) {
gamePresenter.getPlayerMove();
},
gameCommand(gamePresenter) {
gamePresenter.getGameCommand();
},
};
예를 들어 INPUT_CATCH_FN[size](this);
를 호출하면 gamePresenter.getPlayerMove();
가 실행된다.
Input 값의 검증을 위한 로직은 Validation 클래스를 사용하여 재사용성을 높였다. 이 경우에도 inputType
을 클래스의 메소드 이름으로 사용하면 분기 처리를 간단하게 할 수 있다.
class Validation {
static validate({ condition, message }) {
if (!condition) {
// throw new Error(message);
throw message;
}
}
static size(number) {
const isInRange = number >= BRIDGE.SIZE_MIN && number <= BRIDGE.SIZE_MAX;
this.validate({ condition: isInRange, message: INPUT_VAL.SIZE_ERROR });
}
static moving(string) {
const isMove = string === USER_INPUT.UP || string === USER_INPUT.DOWN;
this.validate({ condition: isMove, message: INPUT_VAL.MOVING_ERROR });
}
static gameCommand(string) {
const isRetry = string === USER_INPUT.RETRY || string === USER_INPUT.QUIT;
this.validate({ condition: isRetry, message: INPUT_VAL.RETRY_ERROR });
}
}
타 로직들은 함수가 하나의 일만 하도록 잘게 쪼개주면 대부분 함수의 길이를 10 줄 이내로 만족했다. 코드의 반복을 줄이기 위해 분기 처리를 할 때 다시 함수의 길이가 길어지면서 굉장히 당황했던 기억이 있다. Object mapping을 사용하여 메인 로직을 추상화하는 것에는 성공한 것 같다.
문자열의 분기 처리는 함수/메소드의 이름을 key로 사용하여 해결했다. 그러나 조건문이 string이 아닌 boolean 값일 때에는 어떻게 처리해야 할까? 이 내용은 다음 글에 설명하려 한다.