[Clean Code] 함수의 길이 줄이기(1): if/else 혹은 switch 문 ➡️ object mapping 으로 변경해보자

Quartz 쿼츠·2022년 11월 21일
1

Better Code & Design

목록 보기
1/4

#0 Introduction

어느덧 우테코 프리코스 마지막 주차에 들어섰다. 4 주라는 시간이 짧게 느껴질 정도로 정말 많은 것을 공부했고 코드를 짜는 방식도 발전했다고 느낀다. 혼자 개발을 할 때는 제약 사항없이 떠오르는대로 타이핑했다면 최근에는 리팩토링에 조금 더 신경을 써서 가독성이 좋고 유지보수가 편한 코드를 만드려고 노력하고 있다.

이 글에서는 함수의 역할을 단순화하고 길이를 줄이는 방법에 대해 설명하려 한다. 우테코 프리코스 4 주차 미션에서는 아래와 같은 제약 사항이 있었다.

⚠️ 제약 사항

  • 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
  • 함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.

함수의 길이가 길어지는 예시 중 하나인 분기 처리문을 그동안 사용했던 if/else 외의 다른 방법을 사용하여 구현해보자.

#0.1 제약사항의 ESLint 설정

#0에서 제시한 제약 사항에 대해 ESLint 규칙을 설정하면 아래와 같다.

rules: {
  "max-depth": ["error", 2],
  "max-lines-per-function": ["error", 10],
}

#1. String 조건문의 분기 처리


아래와 같은 INPUT_TYPE에 따라 각각 다른 함수를 호출하는 분기 처리를 한다고 가정해보자.

const INPUT_TYPE = {
  SIZE: "size",
  MOVING: "moving",
  GAME_COMMAND: "gameCommand",
};

#1.1 If/else 문 사용하기

사용자로부터 받은 input 값을 처리하는 handleInput 함수는 아래와 같이 조건문으로 나타낼 수 있다. INPUT_TYPE 의 문자열을 조건으로 받아 각각 다른 함수를 직접 호출한다. 이 때 Validation 함수에서 에러가 발생할 경우 try-catch 문을 사용해 에러를 출력하고 각각의 폴백 함수를 호출한다.

안타깝게도 이 코드는 길이도 길고, 분기 처리 이후 try-catch 로직이 반복된다는 문제가 있다.

반복되는 로직
inputType 분기처리 > try Validation & 다음 함수 호출 / catch error 메시지 출력 & 폴백 함수 호출

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();
    }
  }
}

#1.2 Switch 문 사용하기

If/else 대신 switch 문을 사용해도 분기 처리 이후 try-catch 로직이 반복될 것을 예상할 수 있다. 또한 케이스가 많아질수록 함수의 길이는 점점 더 길어질 것이다..

handleInput(input, inputType) {
  switch (inputType) {
    case 'size':  
      ...
      [break]
    case 'moving':  
      ...
      [break]
    case 'gameCommand':  
      ...
      [break]
    default:
      ...
      [break]
}

#1.3 Object mapping 사용하기

그렇다면 어떤 방법을 사용해야 handleInput 함수의 분기 처리를 10 줄 이내로 만들 수 있을까? 나는 다음의 [프롱트] 유튜브 참고 영상을 보고 문자열이 아니라 함수도 객체로 맵핑할 수 있다는 것을 배웠다. Object mapping을 사용하면 분기 처리를 간결하게 만들어 메인 로직을 간단하게 정리할 수 있다.

#1.3.1 메인 로직

각 분기 처리에서 중복되는 함수를 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);
  }
}

#1.3.2 Object mapping을 사용한 함수 분기 실행

함수를 key로 object mapping

중복되는 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();가 실행된다.

Class의 static method 사용

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 값일 때에는 어떻게 처리해야 할까? 이 내용은 다음 글에 설명하려 한다.

참고 자료

if만 제거했을뿐인데.. 클린코드라니

profile
Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.

0개의 댓글