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

 

 

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

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

ebook-product.kyobobook.co.kr

코드

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

변경 사항은 git history 참고

 

객체는 처리의 추상화다. 스레드는 일정의 추상화다.

-제임스 O. 코플리엔

 

동시성과 깔끔한 코드는 양립하기 어렵다.

스레드 하나만 실행하는 코드는 짜기 쉽다.

겉으로 멀쩡 해보이는 실상은 문제가 숨겨진 다중 스레드 코드는 짜기 쉽다. 이런 코드의 심각한 문제는 시스템이 동작하다가 부하가 발생하거나 아니면 갑자기 문제가 발생한다.

 

동시성이 필요한 이유?

동시성은 결합(무엇과 언제) 없애는 전략

무엇과 언제를 분리하는 전략

 

스레드가 하나인 프로그램은 무엇과 언제가 밀접하다

콜스텍을 보면 쉽게 있다.

 

무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다.

 

구조적인 관점에서 하나의 프로그램은 거대한 루프(단일 쓰레드) 아닌 작은 협력 프로그램 여럿으로 보인다.

따라서 시스템을 이해하기, 문제를 분리하기 쉽다

 

서블릿 모델, WAS 서블릿 컨테이너를 보면, 동시성을 부분적으로 관리한다.

요청 마다 서버는 비동기식으로 서블릿을 실행한다.

서블릿 스레드는 다른 서블릿 스레드와 무관하다.

 

컨테이너가 제공하는 결합분리 전략은 완벽하지 않아 주의가 필요하다.

그래도 서블릿 모델이 제공하는 구조점 이점이 더욱 크다.

 

구조점 이점을 제외하고도, 응답시간과 작업 처리량 개선도 많이된다.

단일 쓰레드 프로그램은 병목에 취약하다.

I/O 블락, 소켓 통신 대기하는 시간

 

미신과 오해

미신과 오해

  • 동시성은 항상 성능을 올려준다.
    대기시간이 길고, 프로세서를 여러 스레드가 공유 가능하거나
    여러
    프로세서가 동시에 처리할 독립적인 계산이 많은 경우에만 성능이 높아진다.
    경우 전부 일반적인 상황은 아니다.
     
  • 동시성을 구현해도 설계는 그대로다.
    단일 쓰레드와 멀티 쓰레드는 설계 자체가 다르다.
    무엇과 언제를 분리하면 시스템 구조가 크게 달라져야 한다.
     
  • 컨테이너를 사요하면 동시성을 이해할 필요가 없다.
    컨테이너가 어떻게 동작하는지 알아야 동시 수정, 데드락 등과 같은 문제를 회피할 있다.

사실

  • 동시성은 다소 부하를 유발한다.
    성능 측면에서 부하가 걸린다. 그리고 코드량도 늘어난다
  • 동시성은 복잡하다. 간단한 문제라도 복잡하다
  • 일반적으로 동시성 버그를 제현하기 어렵다.
    문제로 자주 발현되지 않은 동시성 버그를 버그로 생각하지 않고, 고치려는 시도를 하지 않는 경우가 있다.
  • 동시성을 구현하려면 근본적인 설계 전략을 고려해야 한다.

 

난관

동시성 구현이 어려운 이유

public class X {
  private int lastIdUsed;


  public void resetLastIdUsed() {
    lastIdUsed = 0;
  }
  public int getNextId() {
    return ++lastIdUsed; // lastIdUsed = lastIdUsed + 1
  }
}

 

JIT 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소 단위를 알아야 무엇이 문제인지 파악할 있다.

 

getNextId() 메서드를 동작을 피연산자스택 기준으로 추상화하여 한글로 풀면 다음과 같다.

  • lastIdUsed 읽는다.
  • 상수 1 읽는다.
  • lastIdUsed, 상수 1 한다.

단위 사이 마다 다른 쓰레드가 개입할 가능성이 있다.

 

 

원자적 연산

중간에 중단이 불가능한 연산을 원자적 연산으로 정의한다.

 

자바 메모리 모델에 의하면 32bit 메모리에 값을 할당하는 연산은 중단이 불가능하다.

 

int 에서 long으로 바꾼다면, 원자적 연산이 아니다.

JVM 명세에 따르면 64 bit 값을 할당하는 연산 개로 나눠진다. , 프로세서에 따라 원자적 연산으로 처리할 있다.

https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-2.html#jvms-2.6.1

, 32bit 할당 다음 32bit 할당 사이 다른 쓰레드가 끼어들 가능성이 존재한다.

다른 쓰레드가 32bit 하나를 수정할 있다.

 

resetLastIdUsed() 메서드는 원자적 연산이다.

전후 맥락과 관계 없이 상수 0 할당하고 있다.

피연산자 스택에서 값을 할당할 사용되는 정보(this, 0) 다른 스레드에서 간섭을 없다.

추가로 데이터 형을 int -> long으로 변경해도 원자적 연산이다. 중간에 다른 쓰레드가 간섭할 경우의 수는 의미가 없다. 결국 상수 0 할당하기 때문이다.

 

getNextId()는 원자적 연산이 아니다.

++ 연산자는 바이트 코드상으로 lastIdUsed = lastIdUsed + 1 연산과 같은다.

피연산자 스택에서 lastIdUsed 값을 가져오고 다른 쓰레드가 끼어들어 lastIdUsed + 1 수행을 하면, +1 소실된다.

public class Main {
  public static void main(String[] args) {
    X x = new X();
   
    for (int i = 0; i < 100_000; i++) {
      createThread(() -> System.out.println(x.getNextId()));
    }
  }


  static void createThread(Runnable run) {
    new Thread(run).start();
  }


}

 

100000 결과로 나와야 하지만, 동시성 문제로 최종 결과로 999981 출력됐다.

쓰레드가 코드 레벨 ++lastIdUsed 작업을 수행하던

바이트 코드 레벨 lastIdUsed 가져오고, 상수 1 가져오고, 둘을 더해야하는데

lastIdUsed 가져온 상태에서 다른 쓰레드가 끼어들어 일부 데이터가 손실된 것이다.

 

public synchronized int getNextId() {
   return ++lastIdUsed; // lastIdUsed = lastIdUsed + 1
}

 

 

synchronized 키워드를 사용해 임계영역을 지정하면 정상적으로 동작한다.

 

동시성을 위해 바이트 코드를 전부 이해할 필요는 없다.

공유 객체/값이 있는곳, 동시 읽기/수정 문제를 일으킬 있는 코드, 동시성 문제를 방지하는 방법은 알아야 한다.

 

 

동시성 방어 원칙

단일 책임 원칙

주어진 메서드, 클래스, 컴포넌트를 변경할 이유는 하나여야 한다는 원칙

동시성은 복잡성 하나만으로도 분리할 이유가 충분하다.

동시성 관련 코드는 다른 코드와 분리해야 한다.

 

동시성 구현 고려사항

  • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다
  • 동시성 코드는 독자적인 난관이 있다. 훨씬 어렵다.
  • 잘못 구현된 동시성 코드는 추측하기 힘든 방식으로 실패한다.

 

동시성 관련 코드는 다른 코드와 분리해야 한다.

 

따름 정리: 자료 범위를 제한하라

객체 하나를 공유한 동일 필드를 수정하면, 쓰레드 간섭이 발생할 있다.

문제를 방지하려면, 공유 객체 내에 synchronized 키워드로 임계 영역으로 보호해야 한다.

임계영역은 성능에 악영향으로 사용 수를 최대한 줄여야 한다.

 

공유 자료를 수정하는 위치가 많을 수록 발생할 있는 경우의

  • 임계영역을 빼먹기, 하나라도 빼먹으면 전체에 영향
  • 모든 임계영역 체크로 고된 노력이 필요
  • 어려운 버그 사유를 찾기 어려워

 

자료를 캡슐화하고, 공유 자료를 최대한 줄여야 한다.

 

따름 정리: 자료 사본을 사용하라

객체를 복사해 읽기 전용으로 사용하는 방법

 

쓰레드가 객체를 복사해 사용한 쓰레드가 사본에서 결과를 가져오는 방법도 가능

 

일반적으로 사본을 생성하는 비용이 공유 자료 임계영역으로 인한 비용보다 적다.

  • 사본 생성 비용
    메모리
    , 메모리로 인한 GC 발생
  • 임계영역 비용
    동기화로
    인한

 

따름 정리: 쓰레드는 가능한 독립적으로 구현하라

스레드 하나 마다 싱글 스레드 어플리케이션으로 생각하고 개발한다.

이러면 다른 스레드와 자료 공유라는 생각을 못하게 된다. 단일 쓰레드 프로그램이니까

 

쓰레드는 클리이언트 요청 하나를 처리한다.

모든 정보는 비공유 출처에서 가져와 로컬 변수에 저장한다.

로컬 변수만 사용한다면, 동기화 문제를 일으킬 수가 없다.

이렇게 되면 스레드는 마치 서로 다른 어플리케이션 처럼 동작하게 된다.

DB 같은 공유하는 외부 자원을 사용하면 문제가 생길 있다.

 

예시는 WAS 동작 방식이다. Request 마다 하나의 쓰레드를 생성해 동작한다.

 

권장

독자적인 쓰레드로, 가능하면 다른 프로세서에서, 돌려도 갠찮도록 자료를 독립적인 단위로 분할하라

 

라이브러리를 이해하라

자바 5부터 동시성이 많이 개선됐다.

java.util.concurrent 패키지

참고로 자바 8부터 추가된 java.util.stream 패키지에선 동시성을 훨씬 쉽게 사용할 있다.

 

자바 5 이후 버전을 사용한다면 동시성 코드 구현 다음을 고려한다.

  • 스레드 환경에 안전한 컬렉션 사용
  • 서로 무관한 작업을 수행할 때는 executor 프레임 워크를 사용한다
  • 가능하면 스레드가 블락킹 되지 않는 방법을 사용한다.
  • 일부 클래스 라이브러리는 스레드에 안전하지 못하다.

 

스레드 환경에 안전한 컬렉션

java.util.concurrent 패키지는 다중 쓰레드 환경에서 안전하다. 심지어 성능도 좋다.

ConcurrentHashMap의 경우 HashMap보다 거의 모든 면에서 빠르다.

https://stackoverflow.com/questions/1378310/performance-concurrenthashmap-vs-hashmap

 

동시 읽기/쓰기를 지원한다. 그리고 자주 사용하는 복합 연산을 다중 쓰레드 상에서 안전하게 만든 메서드를 제공한다.

https://codepumpkin.com/hashtable-vs-synchronizedmap-vs-concurrenthashmap/

 

복잡한 동시성 설계를 위해 자바 5 추가된 클래스

  • ReentrantLock
    메서드에서 잠그고, 다른 메서드에서 푸른
  • Semaphore
    진입
    가능한 프로세스/스레드 카운트 제한이 존재하는
  • CountDownLatch
    지정한
    만큼 이벤트 발생하면 대기 중인 스레드를 모두 해제하는
    모든
    쓰레드는 동시에 공편하게 시작할 기회를 가진다.

권장

자바 네이티브 라이브러리 사용을 검토

 

실행 모델을 이해하라

기본 용어

  • 한정된 자원(Bound Resource)
    모든 컴퓨팅 자원이 아닌, 공유 자원을 의미한다.
    CPU,
    메모리 같은 나의 자원부터, 소켓, DB연결 외부 연결 ...
  • 상호 배제(Mutual Exclusion)
    번에 쓰레드만 공유 자료나 공유 자원을 사용할 있는 경우를 의미한다.
  • 기아(Stravation)
    우선순위같은 스케줄링 문제로 특정 쓰레드나 쓰레드들이 오랫동안 혹은 영원히 자원 할당을 못받는 상황을 의미한다.
  • 데드락(Deadlock)
    여러 쓰레드가 각기 자원을 들고 있으면서 서로 자원을 원해 계속 대기하는 상황
  • 라이브락(Livelock)
    락을 거는 단계에서 쓰레드가 서로를 방해
    획득과 헤제를 반복하면서, 오랫동안 혹은 영원히 실행하지 못하는 상황

 

생산자-소비자

스레드를 생산자/소비자 스레드로 분류한다.

역할 쓰레드는 하나일 수도 여러 쓰레드 수도 있다.

 

종류 쓰레드는 실행 대기열을 공유한다. 대기열을 한정된 자원

 

생산자 쓰레드는 대기열에 공간이 있을 정보를 만들어 채워 넣는다.

소비자 쓰레드는 대기열에 정보가 있어야 가져온다. 정보가 채워질 때까지 대기한다.

 

대기열을 효과적으로 사용하기 위해, 생산자/소비자 쓰레드는 서로 통신한다.

생산자 쓰레드는 대기열에 정보를 채웠으면, 정보가 있다는 사실을 소비자 쓰레드에게 알려준다.

소비자 쓰레드는 대기열에 정보를 가져오고, 대기열에 빈공간이 존재한다는 사실을 생산자 쓰레드에게 알려준다.

순환 구조로 잘못하면, 생산자/소비자 쓰레드는 진행할 있는 상황에서 서로의 신호를 대기할 수도 있다.

 

읽기-쓰기

상황

읽기 쓰레드가 공유 자원을 사용한다. 쓰기 쓰레드가 공유 자원을 가끔씩 갱신

구조는 읽기 쓰레드 처리율이 중요하다.

 

읽기 처리율만 강조하면 쓰기 쓰레드가 기아 상태에 빠져, 정보를 갱신하지 못할 수도 있다.

갱신을 허용하면, 처리율이 떨어진다.

공유 자원을 읽는 동안, 공유 자원을 쓰지 못하게, 반대로 공유 자원을 쓰는 동안, 공유 자원을 읽지 못하게, 사이 균형이 중요하다.

일반적으론 쓰기 작업을 대기하느라 읽기 쓰레드들이 대기하는 상황 때문에 처리율이 떨어진다.

 

식사하는 철학자들

둥근 식탁에 철학자들이 앉아 있다.

철학자는 왼쪽에 포크가 있다.

식탁 가운데에 스파게티 접시가 있다.

포크를 집어야 먹을 있다.

철학자들은 배고프지 않으면 생각을 하고, 배고프면 포크를 집어들고 스파게티를 먹는다.

배고픈 철학자는 왼쪽/오른쪽 철학자가 포크를 사용 중이라면, 대기한다.

스파게티를 먹고, 포크를 내려놓고 다시 생각에 잠긴다.

 

상황에서 철학자는 쓰레드, 포크는 자원이다.

여러 프로세스는 한정된 자원을 얻으려 경쟁한다.

경쟁을 효과적으로 제어하지 못하면, 기아, 데드락, 라이브락 등과 같은 상황이 발생한다.

 

권장

대부분의 다중 쓰레드 문제는 틀에서 보면 세가지 범주에 속한다.

알고리즘과 해법을 이해해야 한다.

 

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾기 어려운 버그가 생긴다.

자바는 synchronized 키워드로 임계영역을 만들어 개별 메서드를 보호할 있다.

공유 클래스 하나에 동기화된 메서드가 여럿이면 구현이 올바른지 확인해야 한다.

 

권장

공유 객체 하나에는 메서드 하나만 사용

 

공유 객체 하나에 메서드 이상 필요한 경우는 다음을 고려

  • 클라이언트에서 잠금
    클라이언트에서
    번째 메서드 호출 서버를 잠근다
    마지막
    메서드 호출까지 잠금을 유지
  • 서버에서 잠금
    클라이언트가
    호출한 별도 메서드를 하나 만든다.
    메서드는 서버를 잠그고 모든 메서드 호출 잠금을 해제한다.
  • 연결 서버
    잠금을
    수행하는 중간 단계를 생성
    서버에서
    잠금과 유사하나 원래 서버는 변경하지 않는다

 

메서드 사이에 존재하는 의존성을 조심하라

public class IntegerIterator implements Iterator<Integer>{
  private Integer nextValue = 0;
  @Override
  public synchronized boolean hasNext() {
    return nextValue < 100_000;
  }
  @Override
  public synchronized Integer next()  {
    if (nextValue >= 100_000) {
      return -1; //예외라고 가정, 비정상값
    }
    return nextValue++;
  }


  public synchronized Integer getNextValue() {
    return nextValue;
  }
}
public class Main {
  public static void main(String[] args) {
    IntegerIterator iterator = new IntegerIterator();
    while (iterator.hasNext()) {
      int nextValue = iterator.next();
     
      // nextValue 로 무언가를 계산
    }
  }
}

코드는 하나의 쓰레드가 실행하면 문제없지만,

이상 쓰레드가 IntegerIterator 객체를 공유하면, 문제가 생긴다.

 

거의 문제가 없지만, 지점쯤에 스레드 간섭으로 정상값을 가져오지 못할 있다.

 

nextValue 현재 99998

스레드 1, hasNext() 호출로 true 얻었다.

스레드 1, next() 호출 전에

스레드 2, 끼어들어 hasNext() 호출로 true 얻었다.

스레드 2, next() 호출한다. 99998 + 1 = 99999 반환한다. 스레드 2 종료

nextValue 현재 99999 이므로 false 이다.

하지만 스레드 1 이미 true 받아, hasNext() 호출한다. 비정상 값을 반환받는다(예외라고 가정)

 

상황은 실제 다중 스레드 환경에서 발생할 있는 문제다.

문제는 스레드 간섭할 때만 문제가 발생하기 때문에 훨씬 까다롭다.

 

해결 방안

  • 실패를 용인한다
  • 클라이언트를 바꿔 문제 해결
    클라이언트
    기반 잠금 구현
  • 서버를 바꿔 문제 해결
    서버
    기반 잠근 구현

 

실패를 용인한다

실패를 전부 허용한다는 뜻이 아니라, 때로는 실패해도 괜찮도록 한다.

예를들어 가끔 발생하는 실패를 예외처리로 재상태로 돌린다. 다만, 근본적인 문제해결이 아니므로 조잡한 방식이다. 근단적인 예시로, 메모리 누수를 못잡아서 달에 번씩 재부팅하는 것과 같다.

 

클라이언트-기반 잠금

다중 스레드 환경에서 클라이언트 코드를 모두 다음과 같이 변경

  static void threadSafe() {
    IntegerIterator it = new IntegerIterator();
    while (true) {
      int nextValue;
      synchronized (it) {
        if (!it.hasNext()) {
          break;
        }
        nextValue = it.next();
      }
      doSomethingWith(nextValue);
    }
  }

클라이언트는 IntegerIterator 객체에 락을 건다.

안전해 졌지만 ,DRY 원칙을 위배해 중복이 많아진다.

 

중복도 문제고. 다른 문제는 모든 프로그래머가 클라이언트 코드에서 IntegerIterator 객체를 사용할 synchronized 키워드로 락을 걸어야 한다고 인지를 하고, 빼먹으면 안된다.

 

문제는 아주 심각하고, 빼먹은 코드를 찾기도 힘들다.

, 절대 클라리언트-기반 잠금을 사용해선 안된다.

다중 쓰레드 환경에 안전하지 못한 라이브러리 사용해야 하는 상황에서도 절대 쓰지 않는다.

Adapter 패턴으로 해당 라이브러리를 감싸서 사용한다.

 

서버-기반 잠금

IntegerIterator 를 다음과 같이 변경한다.

public class IntegerIteratorServerLocked {
  private Integer nextValue = 0;


  public synchronized Integer getNextOrNull() {
    if (nextValue < 100_000) {
      return nextValue++;
    } else {
      return null;
    }
  }
}

클라이언트 코드

  private static void serverBaseThreadSafe() {
    IntegerIteratorServerLocked it = new IntegerIteratorServerLocked();
    while (true) {
      Integer nextValue = it.getNextOrNull();
      if (nextValue == null) {
        break;
      }
      // nextValue 로 무언가를 계산
    }
  }

API 변경했다.

클라리언트는 hasNext() 대신 null 체크가 필요해졌다.

그래도 일반적으로 서버기반 잠금이 옳다.

이유

  • 관련 코드 중복 제거
  • 성능 증가(멀티 쓰레드 환경 서버를 단일 쓰레드 환경으로 수정)
  • 오류 발생 가능성 감소( 관련 코드를 기억할 필요 없음)
  • 중앙 집중화된 코드
  • 공유 변수 범위 축소

 

서버 코드에 손을 대지 못할 경우 예시

public class IntegerIteratorAdapter {
  private IntegerIterator it = new IntegerIterator();


  public synchronized Integer getNextOrNull() {
    if (it.hasNext()) {
      return it.next();
    } else {
      return null;
    }
  }
}

 

동기화하는 부분을 작게 만들어라

synchronized 키워드로 감싼 코드 영역은 번에 쓰레드만 실행 가능하다.

락은 스레드 지연과 HW 부하를 유발한다.

필요한 곳만 임계영역을 설정해 수를 최대한 줄여야 한다.

단편적으로 임계영역 수만 집중하여 전체를 임계영역으로 만들면 안된다.

임계영역 크기는 스레드 경쟁을 촉발시켜 성능이 저하된다.

 

작업 처리량 높이기

예시

URL 목록을 받아 네트워크 연결 페이지를 읽어오는 코드

페이지를 분석해 통계를 구함

모든 페이지 분석 통계 보고서 출력

//URL 하나를 받아 해당 페이지 내용을 반환한다.
public class PageReader {
  // ...
  HttpClientImpl httpClient = new HttpClientImpl();


  public String getPageFor(String url) {
    HttpMethod method = new GetMethod(url);
   
    try {
      httpClient.executeMethod(method);
      String response = method.getResponseBodyAsString();
      return response;
    } catch (Exception e) {
      return handle(e);
    } finally {
      method.releaseConnection();
    }
  }
  private String handle(Exception e) {
    return "";
  }
}
public class PageIterator {
  private PageReader reader;
  private URLIterator urls;


  public PageIterator(PageReader reader, URLIterator urls) {
    this.reader = reader;
    this.urls = urls;
  }


  public synchronized String getNextPageOrNull() {
    if (urls.hasNext()) {
      return getPageFor(urls.next());
    } else {
      return null;
    }
  }
  private String getPageFor(String url) {
    return reader.getPageFor(url);
  }
}

 

PageIterator 인스턴스는 여러 스레드가 공유

스레드는 PageIterator 인스턴스를 공유하며, Iterator 사용한다.

 

코드에서 synchronized 키워드를 필요한 곳에 최대한 적은 범위로 적용한 것을 있다. 처럼 제한적으로 사용해야 한다

 

작업 처리량 계산 - 단일 스레드 환경

연산 속도 가정

  • 페이지 읽어오는 평균 I/O시간 : 1
  • 페이지 분석하는 평균 처리 시간 : 0.5
  • 처리 CPU 100% 사용, I/O CPU 0% 사용

 

쓰레드 하나가 N 페이지를 처리하는 시간은 1.5 * N

 

작업 처리량 계산 - 다중 스레드 환경

순서에 무관하게 페이지를 읽어, 독립적으로 처리해도 좋다면, 다중 스레드가 처리율을 높여줄 있다.

 

다중 쓰레드를 사용하면 페이지 분석과 I/O 동시에 처리할 있다.

스레드가 3, CPU 100% 활용한다고 가정하면,

페이지 읽기 번에 페이지 분석을 번을 겹칠 있다.

따라서 1 마다 단일 쓰레드와 비교해서 3배를 처리할 있다.

 

올바른 종료 코드는 구현하기 어렵다

영구 구동 시스템과 잠시 구동하다 종료하는 시스템을 구현하는 방법은 다르다.

 

깔끔하게 종료하는 코드는 구현하기 어렵다.

가장 흔한 문제는 데드락으로 스레드가 오지 못하는 종료 시그널을 계속 기다린다.

 

가정

부모 스레드가 자식 스레드를 여러 만든 모두 끝나기를 기다렸다 자원을 해제하고 종료하는 시스템

 

자식 쓰레드 하나라도 락이 걸리면, 부모 쓰레드는 영원히 대기상태로 시스템 종료를 못한다

 

유사한 시스템에서 사용자에게서 종료 명령을 받았다고 가정

부모 쓰레드는 자식 쓰레드에게 종료하라고 명령을 내린다.

이때 자식 쓰레드 개가 생산자/소비자 관계

생산자 자식 쓰레드는 명령을 받아 종료를 했다.

소비자 자식 쓰레드는 생상자 자식 쓰레드로부터 오지 못할 명령을 대기 중이라 부모 쓰레드 종료 명령을 수행하지 못하고 영원히 종료를 못한다.

 

이게 끝이 아니다. 반드시 시간을 들여 올바른 종료 코드를 구현해야 한다.

 

권장

종료 코드를 개발 초기부터 고민하고 구현하고, 좋은 알고리즘 찾아보고 있다면 것을 사용하라

 

데드락

가정

개수가 한정된 자원 개를 공유하는 어플리케이션

  • 로컬 임시 DB 연결
  • 중앙 저장소 MQ 연결

 

생성과 갱신 연산 개를 수행

  • 생성
    중앙
    저장소 연결 임시 DB 연결
    중앙
    저장소 통신 임시 DB 작업을 저장
  • 갱신
    임시
    DB 연결 중앙 저장소 연결
    임시
    DB에서 작업을 읽어 중앙 저장소로 전송

 

크기보다 사용자 수가 많은 상황, 크기는 10

  • 사용자 10명이 생성 시도
    중앙
    저장소 연결 10 전부 확보
    임시
    DB 연결 확보 전에 중단
  • 사용자 10명이 갱신 시도
    임시
    DB 연결 10 모두 확보
    중앙
    저장소 연결 확보 전에 중단
  • 생성 스레드 10개는 임시 DB 연결 확보를 위해 대기한다.
    갱신 스레드 10개는 중앙 저장소 연결 확보를 위해 대기한다.
  • 데드락 발생

 

이런 동시성 버그는 증상 재현이 어렵다. 증상이 어쩌다 발생하기도 하고, 원인 파악을 위해 디버깅 문을 추가하면, 디버깅 문이 추가 됨에 따라 데드락 양상이 바뀔 수도 있다.

 

데드락을 해결하려면 원인을 이해해야 한다.

  • 상호 배제 (Mutual exclusion)
  • 잠금 & 대기 (Lock & Wait)
  • 선점 불가 (No Preemption)
  • 순환 대기 (Circular Wait)

 

상호 배제 (Mutual exclusion)

여러 스레드가 자원을 공유하나 자원은 여러 스레드가 동시에 사용하지 못하며 개수가 제한적이라면 상호 배제 조건을 만족한다

예시

DB 연결, 쓰기용 파일 열기, 레코드 , 세마포어 같은 자원

 

잠금 & 대기 (Lock & Wait)

스레드가 자원을 점유하면 필요한 나머지 자원까지 모두 점유해 작업을 마칠 때까지 점유한 자원을 내놓지 않는다.

 

선점 불가 (No Preemption)

스레드가 다른 스레드로부터 자원을 뺏지 못한다.

자원을 점유한 스레드가 자원을 내놓기 전까지 다른 스레드는 자원을 점유하지 못한다.

 

순환 대기 (Circular Wait, == 죽음의 포옹)

T1, T2 쓰레드 존재, R1, R2 자원 존재

 

T1 -> R1 점유

T2 -> R2 점유

T1 -> R2 필요

T2 -> R1 필요

 

 

가지 조건을 모두 충족하면, 데드락이 발생한다.

하나라도 해결하면, 발생하지 않는다.

 

상호 배제 조건 깨기

  • 동시에 사용해도 괜찮은 자원을 활용

  • 스레드 이상으로 자원 수를 늘린다.
  • 자원을 점유하기 전에 필요한 자원이 모두 있는지 확인한다.

 

실무에선 대다수 자원은 수가 제한적이다. 그리고 번째 자원을 사용하고 추가로 필요한 자원이 있을 수도 있다.

, 실제로 상호 배제 조건을 깨기는 쉽지 않다.

 

잠금 & 대기 조건 깨기

대기하지 않으면 데드락은 발생하지 않는다.

자원을 점유하기 전에 확인 자원 하나라도 점유를 못하면 지금까지 점유한 자원을 반환하고 처음부터 재시작한다.

 

문제점

  • 기아(Starvation)
    스레드가 필요한 자원을 점유하지 못한다.
    이유는 번에 점유하기 힘든 자원 조합
  • 라이브락(Livelock)
    여러 스레드가 한번에 잠금 단계로 진입을 시도한다. 때문에 쓰레드는 자원을 점유했다가 반환했다가를 반복한다.

 

상황 작업 처리량을 크게 저해한다.

기아는 CPU 효율을 저하시켜 작업 처리량을 저하시킨다.

라이브락은 CPU 자원만을 쓸데없이 많이 사용한다.

 

선점 불가 조건 깨기

다른 쓰레드로부터 자원을 뺏을 있으면, 데드락은 발생하지 않는다.

 

필요한 자원이 잠금 상태라면, 소유자 스레드에게 잠금 해제 요청을 한다.

소유자 스레드는 현재 상태가 다른 자원을 기다리는 중이라면, 자신이 점유한 자원을 모두 해제하고 처음부터 다시 시작한다

 

잠금 해제 요청이 없다면, 스레드가 자원을 기다려도 된다는 이점이 있다.

따라서 처음부터 다시 시작하는 수가 줄어든다.

다만 요청 관리하기 어렵다.

 

순환 대기 조건 깨기

가장 흔한 전략

대부분 시스템이 채용

 

T1, T2 똑같은 순서로 자원을 할당하게 만들면 순환 대기는 불가능하다.

 

기존

T1 -> R1 점유

T2 -> R2 점유

T1 -> R2 필요

T2 -> R1 필요

 

해결

모든 쓰레드가 자원 점유 순서 동일하게 가져가도록 한다.

R1 점유 R2 점유하도록 변경

 

문제점

  • 자원을 할당하는 순서와 사용하는 순서가 다를 있다.
    먼저 할당 받은 자원을 나중에 사용하게 되어 필요 이상으로 자원을 점유하게 있다
  • 때론 순서에 따라 자원 할당이 어렵다
    자원 사용 둘째 자원을 얻는다면 순서대로 할당하기는 불가능하다.

 

결론

데드락을 피하는 전략은 많다. 각기 장단이 존재하며 어떠한 것도 쉽지 않다.

따라서 쓰레드 관련 코드를 분리해 개발을 해야한다.

그래야 쓰레드 관련 문제를 파악하기 그나마 쉬워지고, 문제 해결 방법도 쉬워진다.

 

스레드 코드 테스트하기

동시성 코드는 올바르다고 증명하기는 불가능하다.

그럼에도 불구하고 충분한 테스트는 위험을 낮춰준다.

 

권장

문제를 노출하는 테스트 케이스 작성

각종 설정과 부하 정도를 바꿔가며 반복 테스트

실패하면, 반드시 원인을 추적, 다시 돌렸을 통과했다고 그냥 넘어가면 안된다.

 

고려사항 지침

  • 말이 되는 실패는 잠정적인 스레드 문제로 취급하라
  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 있도록 스레드 코드를 구현하라
  • 다중 스레드 쓰는 코드 부분을 상황에 맞춰 조정할 있게 작성하라
  • 프로세서 수보다 많은 스레드를 돌려보라
  • 다른 플랫폼에서 돌려보라
  • 코드에 보조 코드를 넣어 돌려, 강제로 실패를 일으키게 해보라

 

말이 되는 실패는 잠정적인 스레드 문제로 취급하라

다중 쓰레드 코드는 말이 안되는 오류를 일으킬 때도 있다.

문제는 이를 개발자가 직관적으로 원인을 이해할 없다.

100 번에 1 문제가 발생할 수도 있어, 문제를 재현하기가 어렵다

이런 문제를 일회성 문제로 취급해선 안된다.

 

권장

시스템 실패를 일회성이라 치부하지 말기

 

다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자

스레드 환경은 배제한 코드부터 테스트해야 한다.

스레드가 별개 코드로 분리되어 있다면, 코드가 호출하는 POJO 스레드를 모른다.

POJO 스레드 환경

 

권장

스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 말기

먼저 스레드 환경 밖에 코드를 테스트

 

다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 있도록 스레드 코드를 구현하라

  • 스레드도 실행, 여러 스레드로 실행, 실행 스레드 변경
  • 실제 환경과 테스트 환경에서 스레드 코드 실행
  • 빨리, 천천히 다양한 속도로 테스트
  • 반복 가능하도록 테스트 케이스 작성

 

다중 스레드 쓰는 코드 부분을 상황에 맞춰 조정할 있게 작성하라

적절한 스레드 개수를 번에 파악할 수는 없다.

스레드 개수를 조절하기 쉽게 코드를 구현한다.

런타임 스레드 수를 조절하는 방법도 고려한다.

처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 코민한다.

 

프로세서 수보다 많은 스레드를 돌려보라

강제로 스레드 스와핑을 유발하기 위해 프로세서 수보다 많은 스레드를 돌린다.

스와핑이 잦을 수록 임계영역을 빼먹은 코드나 데드락을 코드를 찾기 쉬워진다.

 

다른 플랫폼에서 돌려보라

다중 스레드 코드는 운영체제마다 스레드를 처리하는 정책에 따라 결과가 달라질 수도 있다.

 

코드에 보조 코드를 넣어 돌려, 강제로 실패를 일으키게 해보라

스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 코드가 실행될 있는 수천, 수만 가지의 경로 하나에서 오류가 발생하기 때문이다.

때문에 버그를 발견해도 다시 찾기도, 재현하기도 힘들다.

 

드물게 발생하는 오류를 자주 일으키기 위해 보조 코드를 사용한다.

Object.wait(), sleep() 등과 같은 스레드를 조정하는 메서드를 추가해 다양한 순서로 실행한다.

 

코드에 보조 코드를 추가하는 방법 가지

  • 직접 구현
  • 자동화

 

직접 구현

  public synchronized String nextUrlOrNull() {
    if (hasNext()) {
      String url = urlGenerator.next();
      Thread.yield(); //보조 코드
      return url;
    }
    return null;
  }

yield() 메서드를 추가해 실행되는 경로에 변화를 줬다.

실패할 가능성을 높였다.

다만, 실패가 yield() 때문은 절대 아니다. 원래 실패했어야 것이 드러났을 뿐이다.

 

문제점

  • 직접 보조 코드 삽입할 위치 선정
  • 어떤 함수를 어디서 호출할
  • 배포 환경에 보조 코드를 계속 남겨두는 것은 성능이 저하됨
  • 보조 코드를 넣어도 실패했어야 코드가 실패하지 않을 확률일 훨씬 높다.

 

POJO 쓰레드를 제어하는 클래스를 분리하면, 보조 코드를 넣기 훨씬 쉽다.

 

자동화

보조 코드 자동화는 AOF(Aspect-Oriented Framework, CGLIB 등과 같은 도구가 필요하다.

public class ThreadJigglePoint {
  public static void jiggle() {
    //무작위로 sleep() yield() 등과 같은 쓰레드 제어 메서드를 랜덤으로 호출한다.
  }
}

 

  public synchronized String nextUrlOrNull() {
    if (hasNext()) {
      ThreadJigglePoint.jiggle();
      String url = urlGenerator.next();
      ThreadJigglePoint.jiggle();
      updateHasNext();
      ThreadJigglePoint.jiggle();
      return url;
    }
    return null;
  }

 

전용 도구를 사용하면 좋지만, 여의치 않으면 위와 같은 간단한 도구를 만들어 사용한다.

public class ThreadJigglePoint {
  private static Action action;


  static {
    //환경에 따라 다르게 사용하도록 구현
    if(isLocal()) {
      action = Action.Op;
    } else {
      action = Action.NoOp;
    }
  }
  private static boolean isLocal() {
    return true;
  }


  public static void jiggle() {
    action.jiggle();
  }




  enum Action {
    Op {
      void jiggle() {
        //무작위로 sleep() yield() 등과 같은 쓰레드 제어 메서드를 호출한다.
      }
    },
    NoOp {
      void jiggle() {
        //아무것도 하지 않는다.
      }
    };
   
    abstract void jiggle();
  }
}

위와 같이 배포환경 마다 구현을 따로 가지고 있으면 편리하다.

 

코드를 흔드는 이유는 스레드를 매번 다른 순서로 실행해 오류를 들어내게 하기 위함이다.

 

결론

다중 스레드 코드는 올바로 구현하기 어렵다.

간단한 코드도 다중 쓰레드와 공유 자료를 사용하는 순간 복잡해진다.

 

SRP 준수한다. 반드시 쓰레드 코드와 쓰레드를 모르는 POJO 코드를 분리한다.

쓰레드 코드를 테스트할 때는 쓰레드 코드만 테스트할 있게 된다.

 

동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다.

 

사용하는 라이브러리와 기본 알고리즘을 이해한다.

특정 라이브러리 기능이 기본 알고리즘과 유사한 문제를 어떻게 해결했는지 파악한다.

 

보호할 코드 영역을 찾는 방법과 코드 영역을 잠그는 방법을 이해한다.

잠긴 영역에서 다른 감긴 영역을 호출하지 않는다.

공유할 정보와 공유하지 말아야할 정보를 명확히 식별한다.

보호할 코드 영역의 수와 크기를 최대한 줄인다.

클라이언트에게 공유 상태를 관리할 책임을 넘기지 않는다.

어쩌다 발생한다고 일회성 문제라 취급하지 않는다.

보조 코드를 넣고, 무작위 설정으로 수천 돌려 최대한 많은 오류를 발견해야 한다.

 

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

14장 점진적인 개선  (0) 2024.02.12
12장 창발성  (0) 2024.01.29
11장 시스템  (0) 2024.01.22
10장 클래스  (1) 2024.01.15
9장 단위 테스트  (1) 2024.01.08

지금까지 깨끗한

깨끗한 코드 블록

올바른 함수

이제 높은 차원인 깨끗한 클래스에 대해 다룬다

클래스 체계

관례상 변수 목록이

변수들 중에서도 static public final 이 맨위

그다음 static private

그다음 private

 

그다음 공개 메서드가 나온다

비공개 함수는 자신을 호출하는 공개 함수 직후에 위치

 

따라서 추상화 단계 순서대로 내려간다.

캡슐화

변수와 유틸리티 함수는 가능한 비공개로 두는 것이 좋다.

때로는 protected 선언해 테스트 코드에 접근을 허용하게 한다.

테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 함수나 변수는 protected public으로 공개한다.

하지만 그 전에 비공개 상태를 유지할 온갖 방법을 강구한다.

캡슐화 풀어주는 결정은 최후의 수단이다

 

클래스는 작아야 한다.

클래스를 만들 번째 규칙은 크기다. 번째도 크기다.

작아야 한다. 작아야 한다.

 

함수와 마찬가지로 작게가 기본규칙이다.

 

얼마나 작게?

함수는 물리적 수로 크기를 측정한다면 클래스는 맡은 책임으로 크기 수준 가늠한다.

맡은 책임은 클래스에 속한 "공개 메서드 수"라고 생각하기 쉽다. 틀린 말은 아니지만 완전히 맞는 말도 아니다.

적은 메서드 수라도 책임이 클 수 있기 때문이다.

 

클래스 작명은 크기를 줄이는 첫 번 단계이다.

만약 작명이 어렵거나 너무 길어진다면 해당 클래스 책임이 너무 많은 것이다.

마찬가지로 클래스 이름이 모호하다면 역시 클래스 책임이 많은 것이다.

Processor, Manager, Super 등과 같은 모호단 단어가 붙는다면 여러 책임을 떠안았다는 표시다.

클래스 설명은 if, and, or, but을사용하지 않고서 25 단어내외로 가능해야한다.

단어들이 붙었다는 것은 여러 책임이 존재한다는 증거이다.

 

단일 책임 원칙 (Single Responsibility Principle : SRP)

클래스나 모듈을 변경할 이유가 하나, 하나뿐이어야 한다는 원칙

SRP 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다. 

클래스는 책임, 변경할 이유가 하나여야 한다는 의미다.

책임, 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.

 

SRP 객체 지향설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이다

그러나 개발자가 가장 무시하는 규칙 하나다.

우리는 수많은 책임을 떠안은 클래스를 꾸준하게 접한다.

 

소프트웨어를 돌아가게 하는 것과 깨끗하게 만드는 것은 완전 별개다

깨끗하고 체계적인 소프트웨어에 초점은 두는게 아니라 프로그램이 돌아가게 하는 것이 나쁜 자세는 아니다.

문제는 거기서 끝이라고 생각하는데 있다. 깨끗하고 체계적인 소프트웨어라는 다음 관심사로 전환하지 않는다.

우리는 반드시 프로그램으로 되돌아가 여러 책임(기능) 가진 클래스를 단일 책임 클래스로 나누어야한다.

 

많은 개발자들이 자잘한 단일 책임 클래스가 많아지면 그림을 이해하기 어려워진다고 우려한다.

그림을 이해하려면 클래스 클래스를 넘나들어야 한다고.

하지만 실제로 작은 클래스가 많은 시스템이던 클래스가 개뿐인 시스템이든 돌아가는 부품은 수가 비슷하다.

그러므로 우리가 고민할 것은 다음과 같다.

도구 상자를 어떻게 관리하고 싶은가?

    작은 서럽을 많이 두고 기능과 이름이 명확하게!!

    아니면 서랍 개를 두고 모두 집어 던져버리기

 

프로젝트는 시스템 논리가 많고 복잡하다. 이런 복잡성을 다루려면 체계적인 정리를 통해 제어하는 것이 중요하다.

그래야 무엇이 어디에 위치한지 알 있다.

또한 변경을 직접 영향이 미치는 컴포넌트만 이해해도 충분하다

큼직한 다목적 클래스로 이뤄진 시스템은 변경을 가할 당장 알필요가 없는 사실까지 들이밀어 방해한다

 

강조한다. 클래스 보다 작은 클래스 여럿으로 이뤄진 시스템이 바람직하다

작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

 

응집도(Cohesion)

클래스는 인스턴스 변수 수가 작아야 한다.

클래스 메서드는 클래스 인스턴스 변수 하나 이상 사용해야 한다.

일반적으로 메서드가 변수를 많이 사용할 수록 메서드와 클래스는 응집도가 높다.

특히 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.

 

위처럼 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다.

우리는 응집도 높든 클래스를 선호한다.

응집도가 높다는 것은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다

 

'함수를 작게, 매개변수 목록을 짧게' 라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.

우리는 이제 알고 있다. 이것이 새로운 클래스로 쪼개야 한다는 신호라는 것을

응집도가 높은 클래스를 유지하도록 서로 응집력이 높은 메서드와 변수들을 그룹지어 여러 클래스로 분리해준다.

 

응집도를 유지하면 작은 클래스 여럿이 나온다

함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.

예를 들어, 변수가 많은 함수 하나에서 일부를 작은 함수 하나로 빼내고 싶다.

빼내려는 코드가 함수에 변수 4개를 사용한다. 그러면 작은 함수 인자에 변수 4개를넣어야 하나? 아니다. 이럴 함수에 로컬 변수를 클래스 인스턴스 변수로 만들면 함수는 인수가 필요 없다. 그만큼 함수를 쪼개기 쉬워진다.

다만 이과정에서 불행이도 응집력을 잃는다. 몇몇 함수만 이용하는 인스턴스 변수가 점점 늘어나기 때문이다.

, 이런 상황에서 클래스가 응집력을 잃는다면 새 클래스를 만들어 분리하는 것이다.

그래서 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.

 

리팩터링 과정

  • 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 작성
  • 그다음 한 번에 하나씩 수 차례 걸쳐 조금씩 코드를 변경
  • 코드 변경마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인
  • 이 과정을 반복 정리한 결과로 최종 프로그램 산출

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다. 과정에서 의도적으로 동작하지 않을 위험이 생긴다. 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반되는 위험을 낮춘다.

 

어떤 변경이던 클래스에 손대면 다른 코드를 망가뜨릴 잠정적인 위험이 있다.

이는 SRP를 위반하는 것이다.

이럴 땐 공통되는 기능을 추상화해 상위 클래스로 만들어 이를 상속하는 작은 클래스로 쪼갠다. 

작은 클래스들은 각기 다른 작은 책임을 수행할 메서드를 구현할 것이다.

추후 변경(수정, 추가)이 발생해도 그 작은 클래스에 국한된다. 

 

위 과정은 객체 지향 설계에서 다른 핵심 원칙인 OCP(Open-Closed Principle)을 지키게 된다.

OCP 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.

파생 클래스를 생성하는 방식으로 기능에 개방적인 동시에 다른 클래스를 닫아놓는 방식으로 수정에 폐쇄적

 

새 기능을 수정하거나기존 기능을 변경할 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 기능을 추가할 시스템을 확장할 기존 코드를 변경하지는 않는다.

 

변경으로부터 격리

요구사항은 변하기 마련이다. 따라서 코드도 변한다.

객체지향 프로그래밍에서 클래스는 구체적인(concreate) 클래스와 추상(abstract) 클래스가 있다

구체적인 클래스는 구현부가 존재하며 추상 클래스는 선언부만 존재한다.

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 따라서 우리는 인터페이스와 추상 클래스를 사용해 구현에 미치는 영향을 격리한다.

 

상세한 구현에 의존하는 코드는 테스트가 어렵다.

 

 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 결합도가 낮다는 소리는 시스템 요소가 다른 요소로부터 그리고 변경으로부터 격리되어 있다는 의미다. 시스템 요소가 서로 격리되어 있다면 요소를 이해하기도 쉽다.

결합도를 최소로 낮추면 자연스럽게 DIP(Dependency Inversion Principle) 따르는 클래스가 나온다.

DIP 본질은 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

 

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

12장 창발성  (0) 2022.11.05
11장 시스템  (0) 2022.11.01
9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18
7장 오류 처리  (0) 2022.10.12

+ Recent posts