[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개의 댓글

관련 채용 정보