Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

저자가 신경 예제 코드로 구조를 면밀히 살핀다.

명령행 인수 구문을 분석하는 프로그램 Args

 

Args는 생성자로 인수 형식 문자열과 명령행 인수를 받는다.

Args인스턴스로 인수 값을 질의한다.

public class Main {
  public static void main(String[] args) {
    try {
      Args arg = new Args("l,p#,d*", args);
      boolean logging = arg.getBoolean('l'); // 불린
      int port = arg.getInt('p'); // 정수
      String directory = arg.getString('d'); // 문자열
      executeApplication(logging, port, directory);
    } catch (ArgsException e) {
      System.out.printf("Argument error: %s\n", e.errorMessage());
    }
  }
  private static void executeApplication(boolean logging, int port, String directory) {
    /* 예제 없음....*/
  }
}

 

Args 구현

 

package chap14.ex01;


import static chap14.ex01.ArgsException.ErrorCode.*;
import java.util.*;


public class Args {
  private Map<Character, ArgumentMarshaler> marshalers;
  private Set<Character> argsFound;
  private ListIterator<String> currentArgument;


  public Args(String schema, String[] args) throws ArgsException {
    marshalers = new HashMap<Character, ArgumentMarshaler>();
    argsFound = new HashSet<Character>();
    parseSchema(schema);
    parseArgumentStrings(Arrays.asList(args));
  }
  private void parseSchema(String schema) throws ArgsException {
    for (String element : schema.split(","))
      if (element.length() > 0)
        parseSchemaElement(element.trim());
  }
  private void parseSchemaElement(String element) throws ArgsException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
    if (elementTail.length() == 0)
      marshalers.put(elementId, new BooleanArgumentMarshaler());
    else if (elementTail.equals("*"))
      marshalers.put(elementId, new StringArgumentMarshaler());
    else if (elementTail.equals("#"))
      marshalers.put(elementId, new IntegerArgumentMarshaler());
    else if (elementTail.equals("##"))
      marshalers.put(elementId, new DoubleArgumentMarshaler());
    else if (elementTail.equals("[*]"))
      marshalers.put(elementId, new StringArrayArgumentMarshaler());
    else
      throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
  }
  private void validateSchemaElementId(char elementId) throws ArgsException {
    if (!Character.isLetter(elementId))
      throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null);
  }
  private void parseArgumentStrings(List<String> argsList) throws ArgsException {
    for (currentArgument = argsList.listIterator(); currentArgument.hasNext();) {
      String argString = currentArgument.next();
      if (argString.startsWith("-")) {
        parseArgumentCharacters(argString.substring(1));
      } else {
        currentArgument.previous();
        break;
      }
    }
  }
  private void parseArgumentCharacters(String argChars) throws ArgsException {
    for (int i = 0; i < argChars.length(); i++)
      parseArgumentCharacter(argChars.charAt(i));
  }
  private void parseArgumentCharacter(char argChar) throws ArgsException {
    ArgumentMarshaler m = marshalers.get(argChar);
    if (m == null) {
      throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null);
    } else {
      argsFound.add(argChar);
      try {
        m.set(currentArgument);
      } catch (ArgsException e) {
        e.setErrorArgumentId(argChar);
        throw e;
      }
    }
  }
  public boolean has(char arg) {
    return argsFound.contains(arg);
  }
  public int nextArgument() {
    return currentArgument.nextIndex();
  }
  public boolean getBoolean(char arg) {
    return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
  }
  public String getString(char arg) {
    return StringArgumentMarshaler.getValue(marshalers.get(arg));
  }
  public int getInt(char arg) {
    return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
  }
  public double getDouble(char arg) {
    return DoubleArgumentMarshaler.getValue(marshalers.get(arg));
  }
  public String[] getStringArray(char arg) {
    return StringArrayArgumentMarshaler.getValue(marshalers.get(arg));
  }
}

 

구조가 위에서 아래로 읽힌다. 스크롤을 사용해 이리저리 이동하지 않는다.

import java.util.*;
public interface ArgumentMarshaler {
  void set(Iterator<String> currentArgument) throws ArgsException;
}
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
  private boolean booleanValue = false;
  public void set(Iterator<String> currentArgument) throws ArgsException {
    booleanValue = true;
  }
  public static boolean getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof BooleanArgumentMarshaler)
      return ((BooleanArgumentMarshaler) am).booleanValue;
    else
      return false;
  }
}
import static chap14.ex01.ArgsException.ErrorCode.*;
import java.util.*;


public class StringArgumentMarshaler implements ArgumentMarshaler {
  private String stringValue = "";
  public void set(Iterator<String> currentArgument) throws ArgsException {
    try {
      stringValue = currentArgument.next();
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_STRING);
    }
  }
  public static String getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof StringArgumentMarshaler)
      return ((StringArgumentMarshaler) am).stringValue;
    else
      return "";
  }
}
import static chap14.ex01.ArgsException.ErrorCode.*;
import java.util.*;


public class IntegerArgumentMarshaler implements ArgumentMarshaler {
  private int intValue = 0;
  public void set(Iterator<String> currentArgument) throws ArgsException {
    String parameter = null;
    try {
      parameter = currentArgument.next();
      intValue = Integer.parseInt(parameter);
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_INTEGER);
    } catch (NumberFormatException e) {
      throw new ArgsException(INVALID_INTEGER, parameter);
    }
  }
  public static int getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof IntegerArgumentMarshaler)
      return ((IntegerArgumentMarshaler) am).intValue;
    else
      return 0;
  }
}
import static chap14.ex01.ArgsException.ErrorCode.*;
import java.util.*;


public class DoubleArgumentMarshaler implements ArgumentMarshaler {
  private double doubleValue = 0;
  public void set(Iterator<String> currentArgument) throws ArgsException {
    String parameter = null;
    try {
      parameter = currentArgument.next();
      doubleValue = Double.parseDouble(parameter);
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_DOUBLE);
    } catch (NumberFormatException e) {
      throw new ArgsException(INVALID_DOUBLE, parameter);
    }
  }
  public static double getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof DoubleArgumentMarshaler)
      return ((DoubleArgumentMarshaler) am).doubleValue;
    else
      return 0.0;
  }
}
import static chap14.ex01.ArgsException.ErrorCode.*;
import java.util.*;


public class StringArrayArgumentMarshaler implements ArgumentMarshaler {
  private List<String> strings = new ArrayList<String>();
  public void set(Iterator<String> currentArgument) throws ArgsException {
    try {
      strings.add(currentArgument.next());
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_STRING);
    }
  }
  public static String[] getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof StringArrayArgumentMarshaler)
      return ((StringArrayArgumentMarshaler) am).strings.toArray(new String[0]);
    else
      return new String[0];
  }
}

 

오류 코드

import static chap14.ex01.ArgsException.ErrorCode.*;
@SuppressWarnings("serial")
public class ArgsException extends Exception {
  private char errorArgumentId = '\0';
  private String errorParameter = null;
  private ErrorCode errorCode = OK;
  public ArgsException() {
  }
  public ArgsException(String message) {
    super(message);
  }
  public ArgsException(ErrorCode errorCode) {
    this.errorCode = errorCode;
  }
  public ArgsException(ErrorCode errorCode, String errorParameter) {
    this.errorCode = errorCode;
    this.errorParameter = errorParameter;
  }
  public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) {
    this.errorCode = errorCode;
    this.errorParameter = errorParameter;
    this.errorArgumentId = errorArgumentId;
  }
  public char getErrorArgumentId() {
    return errorArgumentId;
  }
  public void setErrorArgumentId(char errorArgumentId) {
    this.errorArgumentId = errorArgumentId;
  }
  public String getErrorParameter() {
    return errorParameter;
  }
  public void setErrorParameter(String errorParameter) {
    this.errorParameter = errorParameter;
  }
  public ErrorCode getErrorCode() {
    return errorCode;
  }
  public void setErrorCode(ErrorCode errorCode) {
    this.errorCode = errorCode;
  }
  public String errorMessage() {
    switch (errorCode) {
    case OK:
      return "TILT: Should not get here.";
    case UNEXPECTED_ARGUMENT:
      return String.format("Argument -%c unexpected.", errorArgumentId);
    case MISSING_STRING:
      return String.format("Could not find string parameter for -%c.", errorArgumentId);
    case INVALID_INTEGER:
      return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
    case MISSING_INTEGER:
      return String.format("Could not find integer parameter for -%c.", errorArgumentId);
    case INVALID_DOUBLE:
      return String.format("Argument -%c expects a double but was '%s'.", errorArgumentId, errorParameter);
    case MISSING_DOUBLE:
      return String.format("Could not find double parameter for -%c.", errorArgumentId);
    case INVALID_ARGUMENT_NAME:
      return String.format("'%c' is not a valid argument name.", errorArgumentId);
    case INVALID_ARGUMENT_FORMAT:
      return String.format("'%s' is not a valid argument format.", errorParameter);
    }
    return "";
  }
  public enum ErrorCode {
    OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, MISSING_STRING, MISSING_INTEGER,
    INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE
  }
}

단순한 개념이나 코드가 많은 이유는 자바는 정적 타입 언어이기 때문이다.

동적 타입 언어라면 코드량은 획기적으로 줄어든다.

 

저자가 공들인 예제로 천천히 꼼꼼히 보는 것이 좋다.

이름, 함수 크기, 코드 형식, 새로운 코드 추가 기존 코드에 거의 영향 없음(팩토리 같은 코드에만 영향)

 

어떻게 짰느냐고?

번에 위와 같은 깥끔한 코드를 구현하는 것은 전문가도 불가능하다.

깨끗한 코드를 짜려면 먼저 지저분한 코드를 정리해야 한다.

 

문서에 초안을 작성하고, 퇴고하는 것과 같다.

개발도, 일단 목적에 맞는 돌아가는 코드를 목표로 하고, 이후 정리를 한다.

대부분의 개발자는 돌아만 가는 코드만 구현하고 정리를 하지 않는다.

Args: 1 초안

import java.text.ParseException;
import java.util.*;
public class Args {
  private String schema;
  private String[] args;
  private boolean valid = true;
  private Set<Character> unexpectedArguments = new TreeSet<>();
  private Map<Character, Boolean> booleanArgs = new HashMap<>();
  private Map<Character, String> stringArgs = new HashMap<>();
  private Map<Character, Integer> intArgs = new HashMap<>();
  private Set<Character> argsFound = new HashSet<>();
  private int currentArgument;
  private char errorArgumentId = '\0';
  private String errorParameter = "TILT";
  private ErrorCode errorCode = ErrorCode.OK;
  private enum ErrorCode {
    OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT
  }
  public Args(String schema, String[] args) throws ParseException {
    this.schema = schema;
    this.args = args;
    valid = parse();
  }
  private boolean parse() throws ParseException {
    if (schema.length() == 0 && args.length == 0)
      return true;
    parseSchema();
    try {
      parseArguments();
    } catch (ArgsException e) {
    }
    return valid;
  }
  private boolean parseSchema() throws ParseException {
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        String trimmedElement = element.trim();
        parseSchemaElement(trimmedElement);
      }
    }
    return true;
  }
  private void parseSchemaElement(String element) throws ParseException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
    if (isBooleanSchemaElement(elementTail))
      parseBooleanSchemaFlement(elementId);
    else if (isStringSchemaElement(elementTail))
      parseStringSchemaFlement(elementId);
    else if (isIntegerSchemaElement(elementTail)) {
      parseIntegerSchemaFlement(elementId);
    } else {
      throw new ParseException(String.format("Argument: %c has invalid format: %s.", elementId, elementTail), 0);
    }
  }
  private void validateSchemaElementId(char elementId) throws ParseException {
    if (!Character.isLetter(elementId)) {
      throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
    }
  }


  private void parseBooleanSchemaFlement(char elementId) {
    booleanArgs.put(elementId, false);
  }


  private void parseIntegerSchemaFlement(char elementId) {
    intArgs.put(elementId, 0);
  }


  private void parseStringSchemaFlement(char elementId) {
    stringArgs.put(elementId, "");
  }
  private boolean isStringSchemaElement(String elementTail) {
    return elementTail.equals("*");
  }
  private boolean isBooleanSchemaElement(String elementTail) {
    return elementTail.length() == 0;
  }
  private boolean isIntegerSchemaElement(String elementTail) {
    return elementTail.equals("#");
  }
  private boolean parseArguments() throws ArgsException {
    for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
      String arg = args[currentArgument];
      parseArgument(arg);
    }
    return true;
  }
  private void parseArgument(String arg) throws ArgsException {
    if (arg.startsWith("-"))
      parseElements(arg);
  }
  private void parseElements(String arg) throws ArgsException {
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i));
  }
  private void parseElement(char argChar) throws ArgsException {
    if (setArgument(argChar))
      argsFound.add(argChar);
    else {
      unexpectedArguments.add(argChar);
      errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
      valid = false;
    }
  }
  private boolean setArgument(char argChar) throws ArgsException {
   
    if (isBooleanArg(argChar))
      setBooleanArg(argChar, true);
    else if (isStringArg(argChar))
      setStringArg(argChar);
    else if (isIntArg(argChar))
      setIntArg(argChar);
    else
      return false;
   
    return true;
  }


  private boolean isIntArg(char argChar) {
    return intArgs.containsKey(argChar);
  }
  private void setIntArg(char argChar) throws ArgsException {
    currentArgument++;
    String parameter = null;
    try {
      parameter = args[currentArgument];
      intArgs.put(argChar, new Integer(parameter));
    } catch (ArrayIndexOutOfBoundsException e) {
      valid = false;
      errorArgumentId = argChar;
      errorCode = ErrorCode.MISSING_INTEGER;
      throw new ArgsException();
    } catch (NumberFormatException e) {
      valid = false;
      errorArgumentId = argChar;
      errorParameter = parameter;
      errorCode = ErrorCode.INVALID_INTEGER;
      throw new ArgsException();
    }
  }


  private void setStringArg(char argCha) throws ArgsException {
    currentArgument++;
    try {
      stringArgs.put(argCha, args[currentArgument]);
    } catch (ArrayIndexOutOfBoundsException e) {
      valid = false;
      errorArgumentId = argCha;
      errorCode = ErrorCode.MISSING_STRING;
      throw new ArgsException();
    }
  }


  private boolean isStringArg(char argChar) {
    return stringArgs.containsKey(argChar);
  }


  private void setBooleanArg(char argChar, boolean value) {
    booleanArgs.put(argChar, value);
  }


  private boolean isBooleanArg(char argChar) {
    return booleanArgs.containsKey(argChar);
  }
  public int cardinality() {
    return argsFound.size();
  }
  public String usage() {
    if (schema.length() > 0)
      return "-[" + schema + "]";
    else
      return "";
  }
  public String errorMessage() throws Exception {
    switch (errorCode) {
    case OK:
      throw new Exception("TILT: Should not get here.");
    case UNEXPECTED_ARGUMENT:
      return unexpectedArgumentMessage();
    case MISSING_STRING:
      return String.format("Could not find string parameter for -%c.", errorArgumentId);
    case INVALID_INTEGER:
      return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
    case MISSING_INTEGER:
      return String.format("Could not find integer parameter for -%c.", errorArgumentId);
    }
    return "";
  }
  private String unexpectedArgumentMessage() {
    StringBuffer message = new StringBuffer("Argument(s) -");
    for (char c : unexpectedArguments) {
      message.append(c);
    }
    message.append(" unexpected.");
    return message.toString();
  }


  private boolean falseIfNull(Boolean b) {
    return b != null && b;
  }


  private int zeroIfNull(Integer i) {
    return i == null ? 0 : i;
  }


  private String blankIfNull(String s) {
    return s == null ? "" : s;
  }
  public String getString(char arg) {
    return blankIfNull(stringArgs.get(arg));
  }


  public int getInt(char arg) {
    return zeroIfNull(intArgs.get(arg));
  }


  public boolean getBoolean(char arg) {
    return falseIfNull(booleanArgs.get(arg));
  }
  public boolean has(char arg) {
    return argsFound.contains(arg);
  }
  public boolean isValid() {
    return valid;
  }
  private class ArgsException extends Exception {
  }
}

1 초안은 말장난이다. 사실 미완성이다.

인스턴스 변수 수도 많고, 'TILT' 의미도 해석할 없다.

 

최초 Boolean 인수만 지원하던 초기 버전

package chap14.ex03;
import java.util.*;
public class Args {
  private String schema;
  private String[] args;
  private boolean valid;
  private Set<Character> unexpectedArguments = new TreeSet<>();
  private Map<Character, Boolean> booleanArgs = new HashMap<>();
  private int numberOfArguments = 0;


  public Args(String schema, String[] args) {
    this.schema = schema;
    this.args = args;
    valid = parse();
  }


  public boolean isValid() {
    return valid;
  }
  private boolean parse() {
    if (schema.length() == 0 && args.length == 0)
      return true;
    parseSchema();
    parseArguments();
    return unexpectedArguments.size() == 0;
  }
  private boolean parseSchema() {
    for (String element : schema.split(",")) {
      parseSchemaElement(element);
    }
    return true;
  }
  private void parseSchemaElement(String element) {
    parseBooleanSchemaFlement(element);
  }
  private void parseBooleanSchemaFlement(String element) {
    char c = element.charAt(0);
    if (Character.isLetter(c)) {
      booleanArgs.put(c, false);
    }
  }
  private boolean parseArguments() {
    for (String arg : args)
      parseArgument(arg);
    return true;
  }
  private void parseArgument(String arg) {
    if (arg.startsWith("-"))
      parseElements(arg);
  }
  private void parseElements(String arg) {
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i));
  }
  private void parseElement(char argChar) {
    if (isBoolean(argChar)) {
      numberOfArguments++;
      setBooleanArg(argChar, true);
    } else
      unexpectedArguments.add(argChar);
  }
  private boolean isBoolean(char argChar) {
    return booleanArgs.containsKey(argChar);
  }


  private void setBooleanArg(char argChar, boolean value) {
    booleanArgs.put(argChar, value);
  }


  public int cardinality() {
    return numberOfArguments;
  }


  public String usage() {
    if (schema.length() > 0)
      return "-[" + schema + "]";
    else
      return "";
  }
  public String errorMessage() {
    if (unexpectedArguments.size() > 0) {
      return unexpectedArgumentMessage();
    } else
      return "";
  }
  private String unexpectedArgumentMessage() {
    StringBuffer message = new StringBuffer("Argument(s) -");
    for (char c : unexpectedArguments) {
      message.append(c);
    }
    message.append(" unexpected.");
    return message.toString();
  }


  public boolean getBoolean(char arg) {
    return booleanArgs.get(arg);
  }
}

정도만 되어도 단순하고, 이해가 쉽다.

하지만 코드는 나중에 엉망으로 변할 여지가 숨겨져 있다.

이유로 코드가 점점 더러워진 것이다.

 

하나 String 기능 추가

import java.text.ParseException;
import java.util.*;
public class Args {
  private String schema;
  private String[] args;
  private boolean valid;
  private Set<Character> unexpectedArguments = new TreeSet<>();
  private Map<Character, Boolean> booleanArgs = new HashMap<>();
  private Map<Character, String> stringArgs = new HashMap<>();
  private Set<Character> argsFound = new HashSet<>();
  private int currentArgument;
  private char errorArgument = '\0';


  enum ErrorCode{
    OK, MISSING_STRING
  }


  private ErrorCode errorCode = ErrorCode.OK;
  public Args(String schema, String[] args) throws ParseException {
    this.schema = schema;
    this.args = args;
    valid = parse();
  }


  private boolean parse() throws ParseException {
    if (schema.length() == 0 && args.length == 0)
      return true;
    parseSchema();
    parseArguments();
    return valid;
  }
  private boolean parseSchema() throws ParseException {
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        String trimmedElement = element.trim();
        parseSchemaElement(trimmedElement);
      }
    }
    return true;
  }
  private void parseSchemaElement(String element) throws ParseException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
   
    if (isBooleanSchemaElement(elementTail))
      parseBooleanSchemaFlement(elementId);
    else if (isStringSchemaElement(elementTail))
      parseStringSchemaFlement(elementId);
  }


  private void validateSchemaElementId(char elementId) throws ParseException {
    if (!Character.isLetter(elementId)) {
      throw new ParseException(
          "Bad character: " + elementId + " in Args format: " + schema, 0);
    }
  }
  private void parseStringSchemaFlement(char elementId) {
    stringArgs.put(elementId, "");
  }


  private boolean isStringSchemaElement(String elementTail) {
    return elementTail.equals("*");
  }


  private boolean isBooleanSchemaElement(String elementTail) {
    return elementTail.length() == 0;
  }


  private void parseBooleanSchemaFlement(char elementId) {
    booleanArgs.put(elementId, false);
  }
  private boolean parseArguments() {
    for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
      String arg = args[currentArgument];
      parseArgument(arg);
    }
    return true;
  }
  private void parseArgument(String arg) {
    if (arg.startsWith("-"))
      parseElements(arg);
  }
  private void parseElements(String arg) {
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i));
  }
  private void parseElement(char argChar) {
    if (setArgument(argChar))
      argsFound.add(argChar);
    else {
      unexpectedArguments.add(argChar);
    }
  }
  private boolean setArgument(char argChar) {
    boolean set = true;
    if (isBoolean(argChar))
      setBooleanArg(argChar, true);
     
    else if (isString(argChar))
      setStringArg(argChar, "");
    else
      set = false;
   
    return set;
  }
  private void setStringArg(char argChar, String s) {
    currentArgument++;
    try {
      stringArgs.put(argChar, args[currentArgument]);
    } catch (ArrayIndexOutOfBoundsException e) {
      valid = false;
      errorArgument = argChar;
      errorCode = ErrorCode.MISSING_STRING;
    }
  }


  private boolean isString(char argChar) {
    return stringArgs.containsKey(argChar);
  }


  private void setBooleanArg(char argChar, boolean value) {
    booleanArgs.put(argChar, value);
  }


  private boolean isBoolean(char argChar) {
    return booleanArgs.containsKey(argChar);
  }




  public int cardinality() {
    return argsFound.size();
  }


  public String usage() {
    if (schema.length() > 0)
      return "-[" + schema + "]";
    else
      return "";
  }
  public String errorMessage() throws Exception{
    if (unexpectedArguments.size() > 0) {
      return unexpectedArgumentMessage();
    } else
      switch (errorCode) {
      case MISSING_STRING:
        return String.format("Could not find string parameter for -%c",
            errorArgument);
      case OK:
        throw new Exception("TILT: Should not get here.");
      }
    return "";
  }
  private String unexpectedArgumentMessage() {
    StringBuffer message = new StringBuffer("Argument(s) -");
    for (char c : unexpectedArguments) {
      message.append(c);
    }
    message.append(" unexpected.");
    return message.toString();
  }


  public boolean getBoolean(char arg) {
    return falseIfNull(booleanArgs.get(arg));
  }
  private boolean falseIfNull(Boolean b) {
    return b == null ? false : b;
  }


  public String getString(char arg) {
    return blankIfNull(stringArgs.get(arg));
  }
  private String blankIfNull(String s) {
    return s == null ? "" : s;
  }


  public boolean has(char arg) {
    return argsFound.contains(arg);
  }


  public boolean isValid() {
    return valid;
  }
}

코드가 통제를 벗어나기 시작한다.

코드가 엄청나게 지저분해져 버그가 숨어들지도 모르는 코드로 변했다.

그래서 멈췄다

인수 유형이 추가되면 더욱 엉망이 된다. 그러면 더욱 유지보수하기 힘들어 진다.

리팩터링이 필요하다.

분석

신규 인수 유형 추가 곳에 코드를 추가해야 한다.

  • 인수 유형 HashMap
  • 명령행 인수에서 인수 유형 분석
  • get*, set* 메서드

 

인수 유형은 다양하지만 모두가 유사한 메서드를 제공한다. 따라서 클래스 하나로 만든다. ArgumentMarchaler

 

점진적으로 개선하다

프로그램을 망치는 방법 하나는 개선이라는 이름에 아래 이전과 다르게 구조를 크게 뒤집는 행위다.

 

TDD(테스트 주도 개발) 기법을 사용해, 점진적으로 개선해야 한다.

TDD에선 언제라도 시스템이 돌아가야 한다.

, 변경 후에 변경 전과 똑같이 돌아가야 한다.

규칙을 준수하려면, 번에 크게 뜯어 고칠 수가 없다.

 

전과 같음을 보증하려면, 자동화된 테스트 슈트가 필요하다.

리팩터링을 하면서, 조금에 변경에도 테스트를 구동한다.

 

리팩터링을 하는 중요한 일이 생겨 그만 둬도 시스템은 전과 같이 동작할 것이다.

변경 마다 테스트를 돌렸으니까

 

ArgumentMarshaler 골격 추가

  /**  
   * ###### Args 내부 ######
   * 편의상 Args 내부 가장 끝 자리에 추출할 클래스를 정의했다.
   * 사용하지 않으니, 기존 시스템에 영향은 전혀 없다.        
   */
  private class ArgumentMarshaler {
    private boolean booleanValue = false;
   
    public void setBoolean(boolean value) {
      booleanValue = value;
    }
   
    public boolean getBoolean() {
      return booleanValue;
    }
   
   
  }
  private class BooleanArgumentMarshaler extends ArgumentMarshaler {
   
  }


  private class StringArgumentMarshaler extends ArgumentMarshaler {
   
  }


  private class IntegerArgumentMarshaler extends ArgumentMarshaler {
   
  }
}

 

점진적 변경

  • 인수 유형 HashMap
  • 명령행 인수에서 인수 유형 분석
  • get*, set* 메서드

 

 

 

오류가 나는 곳을 수정한다.

 

 

나머지도 동일하게 알맞게 수정한다

 

 

이제 falseIfNull 메서드는 필요가 없다.

null 존재할 없기 때문이다. 따라서 제거한다.

 

 

String 인수

인수 추가 변경될

  • 인수 유형 HashMap
  • 명령행 인수에서 인수 유형 분석
  • get*, set* 메서드

 

 

 

똑같이 변경하다보면, 필요한 메서드가 생긴다.

IDE 기능을 이용해 메서드를 생성하고, 구현하면 된다.

여기서 코드가 자동 생성되는 위치가 ArgumentMarshaler 이유는 지네릭이 Map<Character, ArgumentMarshaler> 으로 get() 메서드로 반환되는 타입이 ArgumentMarshaler 이기 때문이다.

추가로 반대의 경우인 put()메서드로 객체를 저장할 서브타입은 ArgumentMarshaler로 형변환되어 저장된다.

 

노란색 경고는 아직 호출되는 곳이 없어서 그렇다.

 

전과 같은 이유로 blankIfNull 메서드는 불필요하므로 제거한다.

 

서브 클래스가 아닌 ArgumentMarshaler 슈퍼 클래스에 일단 전부 코드를 모은 이유는 모두 기능을 옮긴 다시 리팩터링해 파생 클래스로 코드를 분리하기 위함이다.

과정이 늘어나지만, 쉽고, 점진적이고, 안전하게 리팩터링할 있다.

 

Integer 똑같이 처리한다.

 

 

 

역시나 불필요한 체크 메서드를 제거했다.

String 제외하고 사실 기본형은 기본값으로 초기화되기 때문에 booleanValue = false 사실 필요없다.

 

로직을 집중시킨 ArgumentMarshaler

  private class ArgumentMarshaler {
    private boolean booleanValue = false;
    private String stringValue;
    private int integerValue;
   
    public void setBoolean(boolean value) {
      booleanValue = value;
    }
   
    public void setInteger(int i) {
      integerValue = i;
    }
   
    public int getInteger() {
      return integerValue;
    }
    public void setString(String s) {
      stringValue = s;
    }
   
    public String getString() {
      return stringValue == null ? "" : stringValue;
    }
    public boolean getBoolean() {
      return booleanValue;
    }
  }

 

이제 파생 클래스로 옮길 차례다.

boolean

 

추상 클래스로 만들고 다형성 활용을 위한 추상 메서드 set 정의한다.

그리고 옮길 대상 값을 파생 클래스에서 접근할 있게 protected 변경한다.

 

setBoolean() 메서드를 set() 대체하도록 한다.

 

 

 

이제 불필요한 ArgumentMarshaler.setBoolean() 메서드를 제거한다.

BooleanArgumentMarshaler.set() 메서드는 사실 인수가 필요가 없다.

기본으로 false , set() 호출할 경우는 true 바꿀때 밖에 없기 때문이다.

다른 파생 클래스는 인자가 필요하다.

 

 

불필요한 형변환이 늘어나지만, 이렇게 해도 된다.

 

이제 get()메서드를 구현해야 한다. get() 메서드는 구현하기 훨씬 까다롭다.

반환 타입이 여러 개라 Object 받환해야 한다.

문제는 호출하는 코드에서 적절한 형변환 코드가 필요하다. 이게 까다롭다.

 

ArgumentMarshaler

 

 

Args

 

ArgumentMarshaler

 

이제 마지막으로 boolean 필드를 파생 클래스로 내린다.

 

 

 

String

과정은 동일하다.

Args

 

 

 

 

Integer

Args

 

 

 

 

IntegerArgumentMarshaler 경우 파싱과정에서 예외가 발생할 있어 구현이 복잡했다.

 

사용자 정의 예외로 바꾼다.

 

일단 오류는 무시한다. 현재 try 구문에서 사용하는 메서드에서 절대로 ArgsException 던지지 않기 때문에 IDE 경고를 내보내는 것이다.

 

 

 

별도 Map 점진적 이관

Args

새로 추가한 맵에 동일하게 저장한 뿐이다. 기존 코드 동작에 영향은 없다는 점에 주목한다.

 

 

marshalers.get(argChar) 코드 중복이 보인다.

호출하는 곳을 추적해 중복을 제거한다.

Args

 

 

중복을 제거했다. 이후 코드를 보면 이상 "isXXXArg" 메서드를 굳이 호출할 필요가 없어 메서드를 인라인했다.

인라인 또한 IDE 도움을 받는다.

 

 

이제 개별 map 사용에서 공통 map 사용하도록 리팩터링한다. Boolean 부터 시작

 

나머지도 동일하게 변경한다.

 

 

Args.getBoolean() 리팩터링

 

이제 booleanArgs Map 사용되는 곳은 없다.

booleanArgs  코드를 제거한다.

 

 

 

나머지 인수유형도 리팩토링

 

 

 

marshalers 맵만 남았다.

 

parseXXXXSchemaFlement() 메서드도 굳이 선언할 필요 없어 인라인한다.

번째 리팩터링 완료

import java.text.ParseException;
import java.util.*;


public class Args {
  private String schema;
  private String[] args;
  private boolean valid = true;
  private Set<Character> unexpectedArguments = new TreeSet<>();
  private Map<Character, ArgumentMarshaler> marshalers = new HashMap<>();
  private Set<Character> argsFound = new HashSet<>();
  private int currentArgument;
  private char errorArgumentId = '\0';
  private String errorParameter = "TILT";
  private ErrorCode errorCode = ErrorCode.OK;
  private enum ErrorCode {
    OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT
  }
  public Args(String schema, String[] args) throws ParseException {
    this.schema = schema;
    this.args = args;
    valid = parse();
  }
  private boolean parse() throws ParseException {
    if (schema.length() == 0 && args.length == 0)
      return true;
    parseSchema();
    try {
      parseArguments();
    } catch (ArgsException e) {
    }
    return valid;
  }
  private boolean parseSchema() throws ParseException {
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        String trimmedElement = element.trim();
        parseSchemaElement(trimmedElement);
      }
    }
    return true;
  }
  private void parseSchemaElement(String element) throws ParseException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
    if (isBooleanSchemaElement(elementTail))
      marshalers.put(elementId, new BooleanArgumentMarshaler());
    else if (isStringSchemaElement(elementTail))
      marshalers.put(elementId, new StringArgumentMarshaler());
    else if (isIntegerSchemaElement(elementTail)) {
      marshalers.put(elementId, new IntegerArgumentMarshaler());
    } else {
      throw new ParseException(String.format("Argument: %c has invalid format: %s.", elementId, elementTail), 0);
    }
  }
  private void validateSchemaElementId(char elementId) throws ParseException {
    if (!Character.isLetter(elementId)) {
      throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
    }
  }


  private boolean isStringSchemaElement(String elementTail) {
    return elementTail.equals("*");
  }
  private boolean isBooleanSchemaElement(String elementTail) {
    return elementTail.length() == 0;
  }
  private boolean isIntegerSchemaElement(String elementTail) {
    return elementTail.equals("#");
  }
  private boolean parseArguments() throws ArgsException {
    for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
      String arg = args[currentArgument];
      parseArgument(arg);
    }
    return true;
  }
  private void parseArgument(String arg) throws ArgsException {
    if (arg.startsWith("-"))
      parseElements(arg);
  }
  private void parseElements(String arg) throws ArgsException {
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i));
  }
  private void parseElement(char argChar) throws ArgsException {
    if (setArgument(argChar))
      argsFound.add(argChar);
    else {
      unexpectedArguments.add(argChar);
      errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
      valid = false;
    }
  }
  private boolean setArgument(char argChar) throws ArgsException {
    ArgumentMarshaler m = marshalers.get(argChar);
    try {
      if (m instanceof BooleanArgumentMarshaler)
        setBooleanArg(m);
      else if (m instanceof StringArgumentMarshaler)
        setStringArg(m);
      else if (m instanceof IntegerArgumentMarshaler)
        setIntArg(m);
      else
        return false;
    } catch (ArgsException e) {
      valid = false;
      errorArgumentId = argChar;
      throw e;
    }
   
    return true;
  }


  private void setIntArg(ArgumentMarshaler m) throws ArgsException {
    currentArgument++;
    String parameter = null;
    try {
      parameter = args[currentArgument];
      m.set(parameter);
    } catch (ArrayIndexOutOfBoundsException e) {
      errorCode = ErrorCode.MISSING_INTEGER;
      throw new ArgsException();
    } catch (ArgsException e) {
      errorParameter = parameter;
      errorCode = ErrorCode.INVALID_INTEGER;
      throw new ArgsException();
    }
  }


  private void setStringArg(ArgumentMarshaler m) throws ArgsException {
    currentArgument++;
    try {
      m.set(args[currentArgument]);
     
    } catch (ArrayIndexOutOfBoundsException e) {
      errorCode = ErrorCode.MISSING_STRING;
      throw new ArgsException();
    }
  }


  private void setBooleanArg(ArgumentMarshaler m) {
    try {
      m.set("true");
    } catch (ArgsException e) {
    }
  }


  public int cardinality() {
    return argsFound.size();
  }
  public String usage() {
    if (schema.length() > 0)
      return "-[" + schema + "]";
    else
      return "";
  }
  public String errorMessage() throws Exception {
    switch (errorCode) {
    case OK:
      throw new Exception("TILT: Should not get here.");
    case UNEXPECTED_ARGUMENT:
      return unexpectedArgumentMessage();
    case MISSING_STRING:
      return String.format("Could not find string parameter for -%c.", errorArgumentId);
    case INVALID_INTEGER:
      return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
    case MISSING_INTEGER:
      return String.format("Could not find integer parameter for -%c.", errorArgumentId);
    }
    return "";
  }
  private String unexpectedArgumentMessage() {
    StringBuffer message = new StringBuffer("Argument(s) -");
    for (char c : unexpectedArguments) {
      message.append(c);
    }
    message.append(" unexpected.");
    return message.toString();
  }


  public String getString(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    try {
      return am == null ? "" : (String) am.get();
    } catch (ClassCastException e) {
      return "";
    }
  }


  public int getInt(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    try {
      return am == null ? 0 : (Integer) am.get();
    } catch (Exception e) {
      return 0;
    }
  }


  public boolean getBoolean(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    boolean b = false;
    try {
      b = am != null && (Boolean) am.get();
    } catch (ClassCastException e) {
      b = false;
    }
    return b;
  }
  public boolean has(char arg) {
    return argsFound.contains(arg);
  }
  public boolean isValid() {
    return valid;
  }
  @SuppressWarnings("serial")
  private class ArgsException extends Exception {
  }




  private abstract class ArgumentMarshaler {
    public abstract void set(String s) throws ArgsException;
    public abstract Object get();
  }


  private class BooleanArgumentMarshaler extends ArgumentMarshaler {
    private boolean booleanValue = false;
    public void set(String s) {
      booleanValue = true;
    }
    public Object get() {
      return booleanValue;
    }
  }


  private class StringArgumentMarshaler extends ArgumentMarshaler {
    private String stringValue;
   
    public void set(String s) {
      stringValue = s;
    }
   
    public Object get() {
      return stringValue;
    }
   
  }


  private class IntegerArgumentMarshaler extends ArgumentMarshaler {
    private int integerValue;
    public void set(String s) throws ArgsException{
      try {
        integerValue = Integer.parseInt(s);
      } catch (NumberFormatException e) {
        throw new ArgsException();
      }
    }
    public Object get() {
      return integerValue;
    }
  }
}

 

구조는 개선됐지만, 여전히 미흡한 점이 많다.

특히 setXXX 메서드는 전부 흉하다. 유형을 일일이 확인하기 때문이다.

리팩터링 원칙에 "조건부 로직을 다형성으로 바꾸기" 원칙을 찾아보면 좋다.

 

ArgumentMarshaler.set() 호출해서 처리하려면 현재 로직을 파생 클래스로 옮겨야 한다.

함수의 인수는 적으면 적을 수록 좋다. 인수를 넘기지 말고

인수를 하나만 넘기도록 args, currentArgument 인스턴스 변수를 리팩터링한다.

 

 

 

 

 

 

 

 

Args.setArgument() 리팩터링

if-else 구분을 제거하기 위해 가장 먼저 예외 케이스(else) 밖으로 빼냈다.

 

Boolean 로직이 제일 쉬워 Boolean 부터 처리한다.

 

boolean 에선 currentArgument 필요없지만, String, Integer에선 필요하다.

메서드 리팩터링의 최종 목표인 추상화된 하나의 set() 메서드를 위해선, 선언부를 일치 시켜야 한다.

 

 

신규 추상 메서드 추가로 서브 클래스에서 구현하지 않으면 컴파일에 실패할 것이다.

추가 구현하도록 한다.

신규 메서드이므로 아직 세부 구현은 미뤄두고 컴파일만 있도록 공란으로 둔다.

그리고 본래 목적인 Boolean 리팩터링을 진행한다.

 


이제
Args.setBooleanArg() 제거해도 안전하다.

그리고 Args.setBooleanArg 호출하는 메서드를 리팩터링한다.

 

 

 

나머지도 동일하게 리팩터링을 진행한다.

 

 

 

 

 

불필요한 메서드 제거를 위해

IntegerArgumentMarshaler 클래스를 정리한다.

 

나머지 ArgumentMarshaler 서브 클래스는 set(String s)에서 아무것도 안했으므로 바로 제거한다.

 

이제 구조 개선으로 인해 신규 유형 추가가 얼마나 쉬운지 확인한다.

신규 유형 추가 전에 테스트 케이스부터 추가하고 진행한다.

 

먼저 불필요한 메서드를 인라인한다.

 

 

 

 

Args.getDouble 메서드를 추가한다.

신규 유형 작업이 끝났다.

 

신규 유형에 대한 예외 메시지 추가

 

신규 유형에 따른 테스트 케이스도 추가해야 하지만, 따로 예제로 작성하지 않았다.

모든 리팩터링은 사소한 수정 하나에도 계속 단위테스트를 실행에 코드에 깨짐이 없는지 확인해야 한다.

 

예외 리팩터링

예외 코드 사실 Args 직접적으로 속하지도 않는다.

ParseException 예외 또한 Args 클래스에 속하지도 않는다.

모든 예외를 ArgsException 클래스로 모은다.

 

 

ErrorCode 옮기면서 오류가 것을 위와 같이 수정한다.

 

 

 

오류 코드를 수정한 모든 ParseException 커스텀 ArgsException 변경한다.

 

 

 

 

나머지 모든 예외처리 로직을 옮긴다.

 

 

package chap14.ex05;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
public class Args {
  private String schema;
  private List<String> argsList;
  private Iterator<String> currentArgument;
  private Map<Character, ArgumentMarshaler> marshalers = new HashMap<>();
  private Set<Character> argsFound = new HashSet<>();
//  private boolean valid = true;
//  private Set<Character> unexpectedArguments = new TreeSet<>();
//  private char errorArgumentId = '\0';
//  private String errorParameter = "TILT";
//  private ArgsException.ErrorCode errorCode = ArgsException.ErrorCode.OK;
  public Args(String schema, String[] args) throws ArgsException {
    this.schema = schema;
    this.argsList = Arrays.asList(args);
//    valid = parse();
    parse();
  }
//  private boolean parse() throws ArgsException {
//    if (schema.length() == 0 && argsList.size() == 0)
//      return true;
//    parseSchema();
//    try {
//      parseArguments();
//    } catch (ArgsException e) {
//    }
//    return valid;
//  }
  private void parse() throws ArgsException {
    parseSchema();
    parseArguments();
  }
  private boolean parseSchema() throws ArgsException {
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        String trimmedElement = element.trim();
        parseSchemaElement(trimmedElement);
      }
    }
    return true;
  }
  private void parseSchemaElement(String element) throws ArgsException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
    if (elementTail.length() == 0)
      marshalers.put(elementId, new BooleanArgumentMarshaler());
    else if (elementTail.equals("*"))
      marshalers.put(elementId, new StringArgumentMarshaler());
    else if (elementTail.equals("#"))
      marshalers.put(elementId, new IntegerArgumentMarshaler());
    else if (elementTail.equals("##"))
      marshalers.put(elementId, new DoubleArgumentMarshaler());
    else
      throw new ArgsException(String.format("Argument: %c has invalid format: %s.", elementId, elementTail));
  }
  private void validateSchemaElementId(char elementId) throws ArgsException {
    if (!Character.isLetter(elementId)) {
      throw new ArgsException("Bad character:" + elementId + "in Args format: " + schema);
    }
  }
  private boolean parseArguments() throws ArgsException {
    for (currentArgument = argsList.iterator(); currentArgument.hasNext();) {
      String arg = currentArgument.next();
      parseArgument(arg);
    }
    return true;
  }
  private void parseArgument(String arg) throws ArgsException {
    if (arg.startsWith("-"))
      parseElements(arg);
  }
  private void parseElements(String arg) throws ArgsException {
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i));
  }
  private void parseElement(char argChar) throws ArgsException {
    if (setArgument(argChar))
      argsFound.add(argChar);
    else {
//      unexpectedArguments.add(argChar);
//      errorCode = ArgsException.ErrorCode.UNEXPECTED_ARGUMENT;
//      valid = false;
      throw new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, argChar, null);
    }
  }
  private boolean setArgument(char argChar) throws ArgsException {
    ArgumentMarshaler m = marshalers.get(argChar);
    if (m == null) {
      return false;
    }
    try {
      m.set(currentArgument);
      return true;
    } catch (ArgsException e) {
//      valid = false;
//      errorArgumentId = argChar;
      e.setErrorArgumentId(argChar);
      throw e;
    }
  }
  public int cardinality() {
    return argsFound.size();
  }
  public String usage() {
    if (schema.length() > 0)
      return "-[" + schema + "]";
    else
      return "";
  }
//  ArgsException으로 옮김
//  public String errorMessage() throws Exception {
//    switch (errorCode) {
//    case OK:
//      throw new Exception("TILT: Should not get here.");
//    case UNEXPECTED_ARGUMENT:
//      return unexpectedArgumentMessage();
//    case MISSING_STRING:
//      return String.format("Could not find string parameter for -%c.", errorArgumentId);
//    case INVALID_INTEGER:
//      return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
//    case MISSING_INTEGER:
//      return String.format("Could not find integer parameter for -%c.", errorArgumentId);
//    case INVALID_DOUBLE:
//      return String.format("Argument -%c expects an double but was '%s'.", errorArgumentId, errorParameter);
//    case MISSING_DOUBLE:
//      return String.format("Could not find double parameter for -%c.", errorArgumentId);
//    }
//    return "";
//  }
//  제거 errorMessage()을 옮기면서, 메서드 인라인 진행
//  private String unexpectedArgumentMessage() {
//    StringBuffer message = new StringBuffer("Argument(s) -");
//    for (char c : unexpectedArguments) {
//      message.append(c);
//    }
//    message.append(" unexpected.");
//
//    return message.toString();
//  }
  public String getString(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    try {
      return am == null ? "" : (String) am.get();
    } catch (ClassCastException e) {
      return "";
    }
  }
  public int getInt(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    try {
      return am == null ? 0 : (Integer) am.get();
    } catch (Exception e) {
      return 0;
    }
  }
  public double getDouble(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    try {
      return am == null ? 0 : (Double) am.get();
    } catch (Exception e) {
      return 0.0;
    }
  }
  public boolean getBoolean(char arg) {
    ArgumentMarshaler am = marshalers.get(arg);
    boolean b = false;
    try {
      b = am != null && (Boolean) am.get();
    } catch (ClassCastException e) {
      b = false;
    }
    return b;
  }
  public boolean has(char arg) {
    return argsFound.contains(arg);
  }


//  제거
//  public boolean isValid() {
//    return valid;
//  }
  @SuppressWarnings("all")
  private class ArgsException extends Exception {
    private char errorArgumentId = '\0';
    private String errorParameter = "TILT";
    private ErrorCode errorCode = ErrorCode.OK;
    public ArgsException() {
    }
    public ArgsException(String message) {
      super(message);
    }
    public ArgsException(ErrorCode errorCode) {
      this.errorCode = errorCode;
    }
    public ArgsException(ErrorCode errorCode, String errorParameter) {
      this.errorCode = errorCode;
      this.errorParameter = errorParameter;
    }
    public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) {
      this.errorCode = errorCode;
      this.errorArgumentId = errorArgumentId;
      this.errorParameter = errorParameter;
    }
    public char getErrorArgumentId() {
      return errorArgumentId;
    }
    public void setErrorArgumentId(char errorArgumentId) {
      this.errorArgumentId = errorArgumentId;
    }
    public String getErrorParameter() {
      return errorParameter;
    }
    public void setErrorParameter(String errorParameter) {
      this.errorParameter = errorParameter;
    }
    public ErrorCode getErrorCode() {
      return errorCode;
    }
    public void setErrorCode(ErrorCode errorCode) {
      this.errorCode = errorCode;
    }
   
    public String errorMessage() throws Exception {
      switch (errorCode) {
      case OK:
        throw new Exception("TILT: Should not get here.");
      case UNEXPECTED_ARGUMENT:
        return String.format("Argument -%c unecpected.", errorArgumentId);
      case MISSING_STRING:
        return String.format("Could not find string parameter for -%c.",
            errorArgumentId);
      case INVALID_INTEGER:
        return String.format("Argument -%c expects an integer but was '%s'.",
            errorArgumentId, errorParameter);
      case MISSING_INTEGER:
        return String.format("Could not find integer parameter for -%c.",
            errorArgumentId);
      case INVALID_DOUBLE:
        return String.format("Argument -%c expects an double but was '%s'.",
            errorArgumentId, errorParameter);
      case MISSING_DOUBLE:
        return String.format("Could not find double parameter for -%c.",
            errorArgumentId);
      }
      return "";
    }
    private enum ErrorCode {
      OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER,
      UNEXPECTED_ARGUMENT, MISSING_DOUBLE, INVALID_DOUBLE
    }
  }
  private interface ArgumentMarshaler {
    public abstract void set(Iterator<String> currentArgument) throws ArgsException;
    public abstract Object get();
  }
  private class BooleanArgumentMarshaler implements ArgumentMarshaler {
    private boolean booleanValue = false;
    public void set(Iterator<String> currentArgument) throws ArgsException {
      booleanValue = true;
    }
    public Object get() {
      return booleanValue;
    }
  }
  private class StringArgumentMarshaler implements ArgumentMarshaler {
    private String stringValue;
    public void set(Iterator<String> currentArgument) throws ArgsException {
      try {
        stringValue = currentArgument.next();
      } catch (NoSuchElementException e) {
//        errorCode = ArgsException.ErrorCode.MISSING_INTEGER;
        throw new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER);
      }
    }
    public Object get() {
      return stringValue;
    }
  }
  private class IntegerArgumentMarshaler implements ArgumentMarshaler {
    private int integerValue = 0;
    public void set(Iterator<String> currentArgument) throws ArgsException {
      String parameter = null;
      try {
        parameter = currentArgument.next();
        integerValue = Integer.parseInt(parameter);
      } catch (NoSuchElementException e) {
//        errorCode = ArgsException.ErrorCode.MISSING_STRING;
        throw new ArgsException(ArgsException.ErrorCode.MISSING_STRING);
      } catch (NumberFormatException e) {
//        errorParameter = parameter;
//        errorCode = ArgsException.ErrorCode.INVALID_INTEGER;
        throw new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, parameter);
      }
    }
    public Object get() {
      return integerValue;
    }
  }
  private class DoubleArgumentMarshaler implements ArgumentMarshaler {
    private double doubleValue = 0;
    public void set(Iterator<String> currentArgument) throws ArgsException {
      String parameter = null;
      try {
        parameter = currentArgument.next();
        doubleValue = Double.parseDouble(parameter);
      } catch (NoSuchElementException e) {
//        errorCode = ArgsException.ErrorCode.MISSING_DOUBLE;
        throw new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE);
      } catch (NumberFormatException e) {
//        errorParameter = parameter;
//        errorCode = ArgsException.ErrorCode.INVALID_DOUBLE;
        throw new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, parameter);
      }
    }
    public Object get() {
      return doubleValue;
    }
  }
}

 

이제 Args 던지는 예외는 ArgsException 뿐이다.

Args 예외/오류 처리 코드와 목적 코드가 완전히 분리됐다.

 

개별 클래스로 분리

이제 하나의 클래스 파일에 있던 내부 클래스를 각자 클래스로 분리한다.

 

IDE 도움을 받는다.

이클립스 기준 Alt + Shift + T

 

 

최종

 

소프트웨어 설계는 분할만 잘해도 품질이 크게 올라간다.

관심사를 분리하면 코드를 이해하고 보수하기 쉬워진다.

 

특히나 ArgsException.errorMessage() 메서드는 Args 있었을 명백히 SRP 위반한 사례다.

Args 클래스 책임은 인수를 처리하는 것인데, errorMessage() 추가로 오류 메시지 형식을 결정하는 책임을 가졌기 때문이다.

 

결론

돌아만 가는 코드만으로는 부족하다.

설계와 구조를 개선할 시간이 없다고 변명은 말이 안된다. 나쁜 코드로 인한 악영향기 크기 때문이다.

묵은 나쁜 코드일 수록 좋은 코드로 바꾸기 더욱 힘들어진다. 처음부터 깨끗한 코드를 유지하는 것이 훨씬 쉽다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

13장 동시성  (1) 2024.02.05
12장 창발성  (0) 2024.01.29
11장 시스템  (0) 2024.01.22
10장 클래스  (1) 2024.01.15
9장 단위 테스트  (1) 2024.01.08
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

서로 다른 대상을 한꺼번에 다루는 코드를 각각 별개 모듈로 나누는 방업

모듈에만 집중해 다른 모듈을 신경 필요가 없어짐

 

동작을 연이은 단계로 쪼개기

입력 값이 처리 로직에 적합하지 않은 경우, 전처리로 입력값을 처리에 적합한 값으로 바꾼다.

이후 로직을 수행한다.

 

경우 대표적인 예시가 컴파일러다.

언어에 맞는 문법 텍스트를 입력으로 받는다.

텍스트를 토큰화 한다. 토큰은 파싱한다. 구문 트리를 만든다. 최적화 등의 이유로 구문 트리를 변환한다.

마지막으로 바이트 코드(목적코드) 생성한다.

단계는 자신의 단계만 신경 쓰면 된다.

 

예시

상품 결제 금액 계산

//상품 결제 금액 계산 코드
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold )
        ?shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}
//배송비 계산 부분 함수로 추출
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const price = applyShipping(basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//단계 간 주고받을 중간 데이터 구조
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {};
    const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//중간 데이터 구조에 데이터를 넣고 인자를 하나씩 줄여간다.
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice};
    const price = applyShipping(priceData, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, shippingMethod, quantity, discount) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = priceData.basePrice - discount + shippingCost;
    return price;
}


//과정을 반복하며, 중간 데이터 구조 완성
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice , quantity : quantity, discount : discount};
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
//첫 번째 단계 처리 함수
function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = { basePrice: basePrice, quantity: quantity, discount: discount };
    return priceData;
}
//두 번째 단계 처리 함수
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


//상수 정리
function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    return { basePrice: basePrice, quantity: quantity, discount: discount };
}
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    return priceData.basePrice - priceData.discount + shippingCost;
}

예시 : 명령줄 프로그램 쪼개기(자바)



import java.io.File;
import java.nio.file.Paths;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestEx {
//현재 두 가지 일을 하고 있다. 하나는 갯수 세기, 다른 하나는 인수가 -r 일 시 ready상태 갯수 세기
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            String filename = args[args.length -1];
            File input = Paths.get(filename).toFile();
            ObjectMapper mapper = new ObjectMapper();
            Order[] orders = mapper.readValue(input, Order[].class);
            if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
                System.out.println(Stream.of(orders)
                        .filter(o->"ready".equals(o.status))
                        .count());
            }else {
                System.out.println(orders.length);
            }
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
}



//자바 명령줄 프로그램 테스트 시 매번 JVM 구동은 느리니
//JUnit 호출 가능한 상태로 변경
public class TestEx {
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            System.out.println(run(args));
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }
}


    //먼저, 두 번째 단계에 해당하는 코드를 메서드로 추출
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        return countOrders(args, filename);
    }
    private static long countOrders(String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //중간 데이터 구조 추가
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //두 번째 단계로 가는 인수 분석하기
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        //args 는 두 번째 단계에서 굳이 필요없다. 이를 위해 먼저 조건식을 변수로 추출
        boolean onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        if(onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    private static class CommandLine{
        private boolean onlyCountReady;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        //중간 데이토 구조로 옮김, 이후 첫번째 단계로 옮김, 이후 매개변수 제거
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine, filename);
    }
    private static long countOrders(CommandLine commandLine, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //잔여 매개변수 처리 완료
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        commandLine.filename = args[args.length -1];
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //메인 작업 완료, 잔여 코드 정리(리팩터링)
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        return countOrders(parseCommandLine(args));
    }
    private static CommandLine parseCommandLine(String[] args) {
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine result = new CommandLine();
        result.filename = args[args.length -1];
        result.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return result;
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }

예시 : 번째 단계에 변환기 사용하기(자바)

이전 예시와 다르게 데이터 구조 생성 전달이 아닌 번째 단계에 적합한 인터페이스로 바꿔주는 변환기 객체 사용

 



    //중간 데이터 구조 추가 부분부터
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //최상위 별도 클래스 파일로 뺏다고 가정
    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, filename(args));
    }
    //임시변수를 질의함수로 바꾸기
    private static String filename(String[] args) {
        return args[args.length -1];
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        //질의 함수 옮기기
        public  String filename() {
            return args[args.length -1];
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, commandLine.filename());
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args);
    }
    //매개변수 정리
    private static long countOrders(CommandLine commandLine, String[] args)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        //함수 추출 후, 클래스로 옮김 이후, 불필요한 인자 제거
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady()) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            //검사로직 옮기기, 리팩터링 완료
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }

 

명령줄 프로그램 쪼개기를 번째 단계에 해당하는 코드를 독립 함수로 추출한 중간 데이터 구조를 구조체로 쓸지, 변환기를 쓸지는 상관없다.

핵심은 단계를 명확히 분리하는

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하기도 한다.

도출 정보는 여러 곳에서 사용된다. 이런 반복 도출 작업을 곳에 모아두면 중복을 막을 있다.

 

방법으로 변환 함수가 있다.

원본 데이터를 입력받아 필요한 정보를 모두 도출한 , 각각 출력 데이터의 필드에 넣어 반환한다.

 

리팩터링 대신 "여러 함수를 클래스로 묶기" 처리해도 된다.

 

원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 편이 데이터 일관성 유지 때문에 좋다.

변환 함수는 가공한 데이터를 새로운 레코드에 저장하기에 일관성이 깨질 있다.

 

여러 함수를 묶는 이유는 로직 중복 회피다.

 

예시

차를 수돗물 처럼 제공하는 서비스 제공을 가정한다.

수돗물처럼 계량해 비용을 산정해야 한다.

//차 사용량 레코드
reading = {customer: "ivan", quantity:10, month: 5, year:2017};


//client1.js  기본 요금 계산 코드
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) *aReading.quantity;


//client2.js  차 세금 일부 면제 코드
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) *aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));


/** client3.js
 * 중복코드가 한 곳에 몰려있으면 함수 추출로 제거하면 된다.
 * 현재 중복 코드가 전체에 흩어져 있다고 가정한다.
 */
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
//이미 함수로 만든 코드가 발견됐다.
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
//먼저 입력 객체를 그대로 복사해 반환하는 함수를 만든다.
function enrichReading(original){
    const result = _.cloneDeep(original);
    return result;
}
const rawReading = acquireReading(); //미가공 측정값
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
function enrichReading(original){
    const result = _.cloneDeep(original);
    //미가공 측정값에 기본 소비량을 부가 정보로 덧붙임
    result.baseCharge =  calculateBaseCharge(result );
    return result;
}
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
//enrichReading() 함수가 정보추가 시 원본 함수를 훼손하는지 체크
it('check reading unchanged', function(){
    const baseReading = reading = {customer: "ivan", quantity:10, month: 5, year:2017};
    const oracle = _.cloneDeep(baseReading);
    enrichReading(baseReading);
    assert.deepEquals(baseReading, oracle);
})
//client1도 수정
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;
//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
function enrichReading(original){
    const result = _.cloneDeep(original);
    result.baseCharge =  calculateBaseCharge(result);
    //함수 이동
    result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
    return result;
}


//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
//전체
reading = {customer: "ivan", quantity:10, month: 5, year:2017};
//client1
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;
//client2.js  차 세금 일부 면제 코드
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
//client3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
function enrichReading(original){
    const result = _.cloneDeep(original);
    result.baseCharge =  calculateBaseCharge(result);
    //함수 이동
    result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
    return result;
}
//이미 함수로 만든 코드가 발견됐다.
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
//enrichReading() 함수가 정보추가 시 원본 함수를 훼손하는지 체크
it('check reading unchanged', function(){
    const baseReading = reading = {customer: "ivan", quantity:10, month: 5, year:2017};
    const oracle = _.cloneDeep(baseReading);
    enrichReading(baseReading);
    assert.deepEquals(baseReading, oracle);
})

 

측정값에 부가 정보를 추가하는 지금 방식은 클라이언트가 데이터를 변경하면 데이터 일관성이 깨져 심각한 문제가 생길 있다.

자바스크립트에서 이런 문제를 방지하기 좋은 방법은 여러 함수를 클래스로 묶기다.

불변 데이터 구조를 지원하는 언어라면 이런 문제가 생길 일이 없어 여러 함수를 변환 함수로 묶기를 병용해서 사용해도 괜찮다.

불변성을 제공하지 않은 언어라도 데이터를 읽기전용으로 사용될 때는 변환 함수 사용도 괜찮다.

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

클래스는 대다수 언어가 제공하는 기본적인 빌딩 블록

클래스는 데이터와 함수를 하나의 공유 환경으로 묶어 그중 일부를 외부에 제공한다.

 

함수 호출 인수로 전달되는 공통 데이터를 중심으로 작동하는 함수들을 클래스 하나로 묶는다.

클래스로 묶으면 함수들 공통 환경을 명확히 표현할 있다.

 

클래스로 묶을 장점은 클라이언트가 객체의 핵심 데이터를 변경할 있고, 파생 객체들을 일관되게 관리할 있다.

 

함수들을 중첩함수로 묶는도 되지만, 클래스를 지원하는 언어라면 클래스로 묶는다.

만약 클래스를 지원하지 않는 언어라는 '함수를 객체처럼 패턴' 사용을 고려한다.

//함수를 객체처럼
function Cat(name){
    return {
        name : name,
        getName : function(){return name;},
        setName : function(arg){name=arg},
        sound : function(){sound(name);}
    };
    function sound(name){console.log(name + " 냐아아아옹");}
}
var cat = Cat('나비');
cat.sound();
cat.setName('배추나비');
cat.sound();
///
나비 냐아아아옹
배추나비 냐아아아옹

 

예시

마시는 차를 정부에서 수돗물처럼 제공한다고 가정, 계량기를 읽어서 측정값을 기록 계산하는 코드

//측정값
let reading={customer : "ivan", quantity:10, month:5, uear:2017};
//클라이언트1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
//클라이언트2
const aReading2 = acquireReading();
const baseCharge2 = baseRate(aReading2.month, aReading2.year) * aReading2.quantity;
const taxableCharge2 = Math.max(0,base-taxTreshold(aReading2.year));
//클라이언트3
const aReading3 = acquireReading();
const basicChargeAmount3 = calculateBaseCharge(aReading3);
function calculateBaseCharge(aReading){
    return baseRate(aReading.month, aReading.year);
}
//레코드를 클래스로 캡슐화
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
}


//클라이언트3
const rawReading = acquireReading();
const aReading3 = new Reading(rawReading);
const basicChargeAmount = aReading3.calculateBaseCharge;
//레코드를 클래스로 캡슐화
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    //함수 옮김
    get calculateBaseCharge(){
        return baseRate(this.month, this.year);
    }
}
const rawReading = acquireReading();
const aReading3 = new Reading(rawReading);
const basicChargeAmount = aReading3.baseCharge;
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    //알맞은 이름 짓기
    get baseCharge(){
        return baseRate(this.month, this.year);
    }
}
//클라이언트1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseRate;


//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = Math.max(0,aReading2.baseRate-taxTreshold(aReading2.year));


//클라이언트는 aReading.baseRate  이렇게 호출한다.
이게 필드인지 함수호출로 계산된 값인지 구분할 수없다. 이를 단일 접근 원칙이라 한다.
//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = taxableChargeFn() ;
//함수로 추출
function taxableChargeFn() {
    return Math.max(0, aReading2.baseRate - taxTreshold(aReading2.year));
}


//측정값
let reading={customer : "ivan", quantity:10, month:5, uear:2017};
//클라이언트1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseRate;
//클라이언트2
const rawReading2 = acquireReading();
const aReading2 = new Reading(rawReading);
const taxableCharge2 = aReading2.taxableCharge;
//클라이언트3
const rawReading3 = acquireReading();
const aReading3 = new Reading(rawReading3);
const basicChargeAmount = aReading3.baseCharge;
class Reading{
    constructor(data){
        this._customer = data.customer;
        this._quantity = data.quantity;
        this._month = data.month;
        this._year = data.year;
    }
    get customer(){return this._customer;}
    get quantity(){return this._quantity;}
    get month(){return this._month;}
    get year(){return this._year;}
    get baseCharge(){
        return baseRate(this.month, this.year);
    }
    get taxableCharge() {
        return Math.max(0, this.baseRate - taxTreshold(aReading2.year));
    }
}
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

명확한 프로그래밍의 핵심은 이름짓기.

자바스크립트 같은 동적 타입 언어라면 앞에 타입을 명시하는 식으로 이름 짓는 것도 좋다. (String name ==> let strName)

예시

가장 간단한 바꾸기는 유효범위가 좁은 변수다.

let tpHd = "untitled";
//참조하는 곳
result += `<h1>${tpHd}</h1>`;
obj['articleTitle'] = 'Hello World';
//바꾸는 곳
tpHd = obj['articleTitle'];
let tpHd = "untitled";
result += `<h1>${title()}</h1>`;
obj['articleTitle'] = 'Hello World';
setTitle(obj['articleTitle']);
//캡슐화
//게터
function title(){ return tpHd;}
//세터
function setTitle(arg){tpHd = arg;}
let 이제변수이름바꿔도됨 = "untitled";
result += `<h1>${title()}</h1>`;
obj['articleTitle'] = 'Hello World';
setTitle(obj['articleTitle']);
function title(){ return 이제변수이름바꿔도됨;}
function setTitle(arg){이제변수이름바꿔도됨 = arg;}
이름 변경 다시 함수를 인라인해도 된다. , 캡슐화한 변수가 전역으로 두루 사용된다면, 나중을 위해 함수로 두는 것이 좋다.

 

예시:상수 이름 바꾸기

상수의 이름은 캡슐화하지 않고 복제 방식으로 점진적으로 바꿀 있다.

const cpyNm = "애크미 구스베리";
//원본 이름 바꾼 후 이전 원본 이름에 바꾼 이름 복제본 대입
const companyName = "애크미 구스베리";
const cpyNm = companyName;

방식은 상수 포함. 클라이언트가 읽기전용인 변수에도 적용할 있다.

자바스크립트 익스포트한 변수

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

함수 함수 사이를 같이 다니는 여러 데이터 항목을 하나의 데이터 구조로 묶는다.

 

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이 관계가 명확해진다.

그리고 함수 인자 수가 줄어든다.

묶인 데이터는 이름을 가지기 때문에 인자로 사용 일관성을 가진다.

 

생성된 매개변수 객체는 새로운 리팩터링 기회를 만들 있다.

 

예시

const station = {
    name: "ZB1",
    readings: [
        {temp:47, time: "2016-11-10 09:10"},
        {temp:53, time: "2016-11-10 09:20"},
        {temp:58, time: "2016-11-10 09:30"},
        {temp:53, time: "2016-11-10 09:40"},
        {temp:51, time: "2016-11-10 09:50"},
    ]
};
//정상 범위를 벗어난 측정값 탐색
function readingsOutsideRange(station, min, max){
    return station.readings
        .filter(r => r.temp < min || r.temp > max);
}
//호출
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    operationPlan.temperatureCeiling);
//묶을 클래스
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min};
    get max(){return this._data.max};
}
//새로운 데이터 구조 매개변수 추가
function readingsOutsideRange(station, min, max, range){
    return station.readings
        .filter(r => r.temp < min || r.temp > max);
}
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    operationPlan.temperatureCeiling
    ,null);//일단 null
//점진적으로 하나씩 줄여가며 테스트 한다.
function readingsOutsideRange(station, min, /*max,*/ range){
    return station.readings
        .filter(r => r.temp < min || r.temp > range.max);
}
const range = new NumberRange(operationPlan.temperatureFloor,
                             operationPlan.temperatureCeiling);
let alerts = readingsOutsideRange(station,
     operationPlan.temperatureFloor,
    // operationPlan.temperatureCeiling,
    range);
 // 완료
const station = {
    name: "ZB1",
    readings: [
        {temp:47, time: "2016-11-10 09:10"},
        {temp:53, time: "2016-11-10 09:20"},
        {temp:58, time: "2016-11-10 09:30"},
        {temp:53, time: "2016-11-10 09:40"},
        {temp:51, time: "2016-11-10 09:50"},
    ]
};
function readingsOutsideRange(station, range){
    return station.readings
        .filter(r => r.temp < range.min || r.temp > range.max);
}
const range = new NumberRange(operationPlan.temperatureFloor,
                             operationPlan.temperatureCeiling);
let alerts = readingsOutsideRange(station, range);
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min};
    get max(){return this._data.max};
}
//연관된 데이터를 묶음으로서 새로운 설계의 기회를 얻는다.
class NumberRange{
    constructor(min,max){
        this._data = {min:min, max:max};
    }
    get min(){return this._data.min;}
    get max(){return this._data.max;}
    contains(arg){return arg>=this.min&&arg<=this.max;}
}

 

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

리팩터링은 결국 프로그램의 요소를 조작하는 , 조작은 변수보다 함수가 쉽다.

함수는 변경 기존 함수에 전달 함수를 활용해 임시 버퍼 역할을 수도 있어 유연하다. (차후 기존함수명으로 변경)

 

반면에 변수는 번에 바꿔야 해서 까다롭다.

변수의 유효 범위가 좁으면 문제가 거의 없지만 넓어질수록 문제가 된다.

 

넓은 범위 변수는 함수로 데이터를 캡슐화하는 것이 가장 좋은 방법이다.

데이터 재구성 ==> 함수 재구성으로 변환

 

함수는 동작이기에 중간에 검증 같은 조직을 끼워 넣을 있다.

 

이러한 이유가 객체지향에서 객체 데이터를 최대한 private 유지하는 이유다. (데이터에 대한 결합도를 떨어트리기 위함)

 

불변 데이터는 가변 데이터보다 캡슐화할 이유가 적다. 데이터가 변경될 일이 없어서 그냥 복사해서 사용하면 된다.

예시

let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
//참조하는 코드
spaceship.owner = defaultOwner;
//갱신하는 코드(불변이라면 갱신 걱정이 없다)
defaultOwner = {firstName:"레베카",lastName:"파슨스"};
let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = defaultOwner;
defaultOwner = {firstName:"레베카",lastName:"파슨스"};
//게터/세터 생성
function getDefaultOwner(){return defaultOwner;}
function setDefaultOwner(arg){return defaultOwner = arg;}
let spaceship = {};
let defaultOwner = {firstName:"마틴",lastName:"파울러"};
//게터 호출, 대입문은 세터 호출
spaceship.owner = getDefaultOwner();
setDefaultOwner({firstName:"레베카",lastName:"파슨스"});
function getDefaultOwner(){return defaultOwner;}
function setDefaultOwner(arg){return defaultOwner = arg;}
let spaceship = {};
//접근 제한자가 없는 언어에서는 이런 식으로 표현하는 것도 도움된다.
let __privateOnly_defaultOwner = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = getDefaultOwner();
setDefaultOwner({firstName:"레베카",lastName:"파슨스"});
//저자는 자바스크립트에서는 게터에 get을 빼는 것을 선호함
function defaultOwner(){return __privateOnly_defaultOwner;}
function setDefaultOwner(arg){return __privateOnly_defaultOwner = arg;}

 

자바스크립트에선 게터와 세터 이름을 똑같이 짓고 인수 존재 여부로 구분하는 방식을 많이 쓰기때문에 get

 

캡슐화하기

//test_refac.mjs
import {defaultOwner} from './test_refac.mjs';
import { strict as assert } from 'node:assert';
const owner1 = defaultOwner();
assert.equal("파울러",owner1.lastName,"처음 값 확인");
const owner2 = defaultOwner();
owner2.lastName ="파슨스";
assert.equal("파슨스",owner2.lastName,"owner2 변경 후");
import {defaultOwner} from './test_refac.mjs';
import { strict as assert } from 'node:assert';
const owner1 = defaultOwner();
assert.equal("파울러",owner1.lastName,"처음 값 확인");
const owner2 = defaultOwner();
owner2.lastName ="파슨스";
console.log(owner1);
assert.equal("파슨스",owner1.lastName,"owner2 변경 후");
console.log(owner1);
////// 객체 값이 변경된 것을 볼 수 있다.
Debugger attached.
{ firstName: '마틴', lastName: '파슨스' }
{ firstName: '마틴', lastName: '파슨스' }
Waiting for the debugger to disconnect…
let spaceship = {};
let defaultOwnerData = {firstName:"마틴",lastName:"파울러"};
spaceship.owner = defaultOwner();
//방법1 게터에서 객체 복사본을 리턴한다.
export function defaultOwner(){return Object.assign({},defaultOwnerData);}
export function setDefaultOwner(arg){defaultOwnerData = arg;}
결과


클라이언트에서 오히려 원본을 원하는 경우를 주의해야 한다.
let defaultOwnerData = {firstName:"마틴",lastName:"파울러"};
//방법 2 레코드 캡슐화하기
export function defaultOwner(){return new Person(defaultOwnerData);}
export function setDefaultOwner(arg){defaultOwnerData = arg;}
class Person{
    constructor(data){
        this._lastName = data.lastName;
        this._firstName = data.firstName;
    }
    get lastName(){return this._lastName;}
    get firstName(){return this._firstName;}
}

경우에 따라 세터도 복사본을 저장하는게 좋을 수도 있다.

 

복제가 성능에 주는 영향을 미미하다. 반면에 원본을 그대로 사용하면 나중에 디버깅하기 어렵고 시간도 오래 걸릴 위험이 있다.

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

함수는 프로그램을 작은 부분으로 나누는 주된 수단이다.

이런 함수 선언부는 소프트웨어의 연결부 역할을 한다. 따라서 선언부를 정의할 필요가 있다.

 

가장 중요한 함수 이름이다. 이름이 좋으면 구현부를 필요가 없다.

이름은 IDE기능으로 쉽게 변경가능하니 좋은 이름이 있다면 주저하지 않고 바꾸자

 

함수 매개변수는 함수를 사용하는 문맥을 설정한다.

전화번호 포맷팅 함수가 있는데 매개변수로 사람 타입을 받아 전화번호를 포맷팅한다고 하면, 활용도가 적다. 전화번호만 인자로 받는다면 어떤 전화번호라도 활용이 가능하다.

절차

간단한 절차와 마이그레이션 절차가 있다.

간단한 절차를 최대한 사용하되 불가능하다면 마이그레이션 절차를 사용한다.

 

간단한 절차

  1. 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
  2. 메서드 선언을 원하는 형태로 바꾼다.
  3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
  4. 테스트한다.

 

마이그레이션 절차

  1. 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
  2. 함수 본문을 새로운 함수로 추출한다.
  3. 추출한 함수에 매개변수를 추가해야 한다면 '간단한 절차' 따라 추가한다.
  4. 테스트한다.
  5. 기존 함수를 인라인한다.
  6. 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 적용해서 원래 이름으로 되돌린다.
  7. 테스트한다.

 

상속 구조 속에 있는 클래스 메서드 변경할 때는 다른 서브 클래스들에도 변경이 반영되어야 한다. 이때 복잡하기에 간접 호출 방식으로 우회하는 방법도 쓴다.

타이핑 처럼 슈퍼클래스와의 연결을 제공하지 않는 언어라면 전달 메서드를 모든 구현 클래스에 추가해야 한다.

 

 

 

예시 : 함수 이름 바꾸기 간단한 절차

function circum(radius){
    return 2 * Math.PI * radius;
}
function circumference(radius){
    return 2 * Math.PI * radius;
}

 

함수 이름 변경 핵심은 함수를 호출하는 부분을 얼마나 쉽게 찾는가에 있다.

정적 타입 언어와 좋은 IDE 조합이라면 쉽게 함수 이름 바꾸기를 처리할 있다.

정적 타입 언어가 아니라면 상당히 귀찮아지고 버그 발생 가능성이 높아진다.

추가로 상속관계에 있는 클래스라면 다른 메서드도 수정해야 된다.

 

리팩터링은 항상 문제를 작게 쪼개서 정복한다.

매개변수 추가/삭제 같은 변경은 반드시 함수 호출 부분을 모두 찾아 수정한 수행한다.

 

간단한 방식은 함수 이름 바꾸기로 코드에 영향이 적을 적합하다.

 

예시: 함수 이름 바꾸기 마이그레이션 절차

function circum(radius){
    return 2 * Math.PI * radius;
}
//마이그레이션 절차
function circum(radius){
    return circumference(radius);
}
function circumference(radius) {
    return 2 * Math.PI * radius;
}
상태에서 정상 동작을 테스트하고,  circum()호출하는 부분을 전부 찾아 circumference() 바꾼다.

 

방식은 API같은 선언부를 고칠 없는 외부 코드 사용 부분을 리팩터링 하기 좋다.

circumference() 만들어 두고, circum() @Deprecated 같은 표시를 해두어 circumference() 호출하도록 유도를 한다. 이후 모든 클라이언트가 변경 했으면 circum() 지운다.

 

 

 

예시: 매개변수 추가하기

예약 우선순위 큐를 지원하라는 요구사항이 들어왔다.

기존 큐와 우선순위 어떤 것으로 할지 결정 매개변수를 추가해야 한다.

 

class Book{
    addReservation(customer){
        this._reservations.push(customer);
    }
}
class Book{
    addReservation(customer){
        this.zz_addReservation(customer);
    }
    //임시이름
    zz_addReservation(customer) {
        this._reservations.push(customer);
    }
}
class Book{
    addReservation(customer){
        this.zz_addReservation(customer, false);
    }
    //매개변수 추가
    zz_addReservation(customer,isPriority) {
        //자바스크립트 한정 assert 테스트
        assert(isPriority === true || isPriority === false);
        this._reservations.push(customer);
    }
}
이제 호출 부분을 전부 zz_addReservation() 변경 zz_addReservation() 함수 이름을 기존 함수 이름으로 바꾼다.
class Book{
    addReservation(customer,isPriority) {
        this._reservations.push(customer);
    }
}

 

 

예시: 매개변수를 속성으로 바꾸기

고객이 뉴잉글랜드 살고있는지 파악하는 함수

/* 현재 문제는 매개변수를 객체로 받고 있다. 재사용성이 제한된다.*/
function inNewEngland(aCustomer){
    return ["MA","CT","ME","VT","NH","RI"].includes(aCustomer.address.state);
}
//호출부
const newEnglanders = someCustomers.filter(c=>inNewEngland(c));
function inNewEngland(aCustomer){
    const stateCode = aCustomer.address.state;
    return ["MA","CT","ME","VT","NH","RI"].includes(stateCode);
}
function inNewEngland(aCustomer){
    const stateCode = aCustomer.address.state;
    return newFunction(stateCode);
}
//임시 함수 추출
function newFunction(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
function inNewEngland(aCustomer){
    //변수 인라인하기
    return newFunction(aCustomer.address.state);
}
function newFunction(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
//호출부 변경
const newEnglanders = someCustomers.filter(c=>newFunction(c));
//함수 선언 바꾸기로 새함수이름을 기존함수이름으로 변경
function inNewEngland(stateCode) {
    return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
//호출부 변경
const newEnglanders = someCustomers.filter(c=>inNewEngland(c));

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

함수 안에서 변수를 사용하는 것보다 원래 표현식과 다를바 없는 경우 인라인한다.

예시

function isOver(anOrder){
    let basePrice = anOrder.basePrice;
    return (basePrice > 1000);
}
function isOver(anOrder){
    return (anOrder.basePrice > 1000);
}
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

표현식이 너무 복잡해서 이해하기 어려울 지역 변수를 활용하면 표현식을 쪼개 관리하기 쉽게 만들 있다.

 

이렇게 추가된 변수는 디버깅에도 도움된다. 디버거 breakpoint 활용할 있기 때문이다.

 

추출된 변수에 이름을 붙일 변수의 문맥을 고려해야한다. 함수 내에서만 의미있다면, 변수로 추출한다. 함수 외부까지 의미가 있다면, 함수로 추출해야 한다.

예시

//기본형
function price(order){
    return order.quantity * order.itemPrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    //가격 = 기본 가격 = 수량 할인 + 배송비
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    //똑같은 표현식 부분 치환
    return basePrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(basePrice * 0.1, 100);
}
function price(order){
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    const shipping = Math.min(basePrice * 0.1, 100);
    return basePrice - quantityDiscount + shipping;
}

사용한 자동 리팩터링

예시: 클래스 안에서

추출하는 대상이 price()메서드 범위를 넘어 Order 클래스 전체에 영향을 미친다. 이럴 변수가 아닌 메서드로 추출한다.

//기본형
class Order {
    constructor(aRecord){
        this._data = aRecord;
    }
    get quantity() {return this._data.quantity;}
    get itemPrice() {return this._data.itemPrice;}
    get price(){
        return this.quantity * this.itemPrice -
        Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 +
        Math.min(this.quantity * this.itemPrice * 0.1, 100);
    }
}
//리팩터링
class Order {
    constructor(aRecord){
        this._data = aRecord;
    }
    get quantity() {return this._data.quantity;}
    get itemPrice() {return this._data.itemPrice;}
    get price(){
        return this.basePrice - this.quantityDiscount + this.shipping;
    }
    get shipping() {
        return Math.min(this.basePrice * 0.1, 100);
    }
    get quantityDiscount() {
        return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
    }
    get basePrice() {
        return this.quantity * this.itemPrice;
    }
}

이렇게 메서드로 추출해두면 객체를 데이터를 외부에서 쉽게 활용할 있다.

 

사용한 자동 리팩터링

주의 것은 메서드를 추출해주지 게터 메서드로 만들어 주지 않아 주의해야 한다.

 

+ Recent posts