Clean Code - 코딩 기본

10 minute read

삶이 그대를 속일지라도.


태도

나쁜 코드의 위험을 이해하지 못하는 관리자의 말을 그대로 따르는 행동은 전문가 답지 못하다.

  • 깨끗한 코드는 ‘보기에 즐거운’ 코드다. 깨끗한 코드는 한가지에 집중한다.
  • 코드는 추측이 아니라 사실에 기반해야한다. 반드시 필요한 내용만 담아야 한다.
  • 깨끗한 코드란 다른 사람이 고치기 쉽다고 단언한다. 테스트케이스가 없는 코드는 깨끗한 코드가 아니다.
  • 깨끗한 코드는 주의 깊게 작성한 코드다. 누군가 시간을 들여 깔끔하고 단정하게 정리한 코드다. 세세한 사항까지 꼼꼼하게 신경쓴 코드다.
  • 모든 테스트를 통과한다. 중복이 없다. 시스템 내 모든 설계 아이디어를 표현한다. 클래스, 메서드, 함수등을 최대한 줄인다. 중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기.
  • 깨끗한 코드는 읽으면서 놀랄 일이 없어야 한다고 워드는 말한다. 코드를 독해하느라 머리를 쥐어짤 필요가 없어야 한다. 읽으면서 짐작한대로 돌아가는 코드가 깨끗한 코드다.

의미 있는 이름

  • 의도를 분명히 밝혀라.

변수나 함수 그리고 클래스 이름은 다음과 같은 굵직한 질문에 모두 답해야 한다.

- 변수(혹은 함수나 클래스)의 존재이유는?
- 수행 기능은?
- 사용 방법은?
- 따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 이야기이다.
  • 그릇된 정보는 피하라.

프로그래머는 코드에 그릇된 단서를 남겨서는 안된다. 그릇된 단서는 코드 의미를 흐린다. 나름 대로 널리 쓰이는 의미가 있는 단어를 다른의미로 사용해도 안된다.

  • 의미 있게 구분하라.

읽는 사람이 차이를 알도록 이름을 지어라.

  • 발음하기 쉬운 이름을 사용하라.

  • 검색하기 쉬운 이름을 사용하라.

문자로 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다는 문제점이 있다. MAX_CLASSES_PER_STUDENT 는 grep으로 찾기 쉽지만, 숫자 7은 은근히 까다롭다. 간단한 메서드에서 로컬 변수만 한 문자를 사용한다. 이름 길이는 범위 크기에 비례해야한다. 변수나 상수를 코드 여러곳에서 사용한다면 검색하기 쉬운 이름이 바람직 하다.

  • 인코딩을 피하라.

변수 이름이 타입을 인코딩할 필요가 없다. 객체는 강한 타입이며 IDE는 코드를 컴파일 하지 않고도 타입 오류를 감지할 정도로 발전했다. 멤버 변수에 m_ 이라는 접두어를 붙일 필요도 없다. 인터페이스 클래스와 구체 클래스를 사용할 경우에, I라는 접두어보다는 구체 클래스에 SubjectImp , Imp라고 붙이는 것이 더욱 효과적이다.

  • 자신의 기억력을 자랑하지 마라.

명료함이 최고
일반적으로 문제 영역이나 해법 영역에서 사용하지 않는 이름을 선택했기 때문에 생기는 문제이다.

  • 클래스이름 : 클래스이름과 객체이름은 명사나 명사구가 적합하다. 동사는 사용하지 않는다.

  • 메서드 이름 : 동사나 동사구가 적합하다.

생성자(Constructor)를 중복 정의할 때는 정적 팩토리 메서드를 사용한다.

  • 기발한 이름은 피하라.

재미난 이름보다 명료한 이름을 선택하라.

  • 한 개념에 한 단어를 사용하라.

    • 추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.
    • 일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.
  • 말장난을 하지마라.

한 단어를 두가지 목적으로 사용하지 마라. 다른 개념에 같은 단어를 사용한다면 그것은 말장난에 불과하다.

  • 해법 영역에서 가져온 이름을 사용하라.

    • 코드를 읽는 사람도 프로그래머라는 사실을 명심한다.
    • 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어등을 사용해도 괜찮다.
  • 문제 영역에서 가져온 이름을 사용하라.
    • 적절한 ‘프로그래머 용어’가 없다면 문제 영역에서 이름을 가져온다.
    • 우수한 프로그래머와 설계자라면 해법 영역과 문제 영역을 구분할 줄 알아야 한다.
  • 의미있는 맥락을 추가하라.
    • 스스로 의미가 분명한 이름이 없지 않다.
    • 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다.

public class GuessStatisticsMessage {
  private String number;
  private String verb;
  private String pluralModifier;

  public String make(char candidate, int count){
    createPluralDependentMessageParts(count);
    return String.format(
      "There %s %s %s%s", verb, number, candidate, pluralModifier
    );
  }
}

  • 불필요한 맥락을 없애라 일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 한다.

좋은 이름을 선택하려면 설명 능력이 뛰어나야 하고 문화적인 배경이 같아야 한다. 이것이 제일 어렵다.
좋은 이름을 선택하는 능력은 기술, 비즈니스, 관리 문제가 아니라 교육의 문제다. 우리 분야 사람들이 이름 짓는 방법을 제대로 익히지 못하는 이유가 바로 여기에 있다.

작게 만들어라

함수를 만드는 첫번째 규칙은 ‘작게!’ 다. 함수를 만드는 둘째 규칙은 ‘더 작게!’ 다.
20줄에서 30줄 정도인 함수도 길다!.

켄트가 코드를 보여줬을 때 나는 함수가 너무도 작아 깜짝 놀랐다. 그때까지 나는 장황하게 긴 스윙 프로그램 함수에 익숙했다. 그런데 Sparkle은 모든 함수가 2줄, 3줄, 4줄 정보였다. 각 함수가 너무도 명백했다. 각 함수가 이야기 하나를 표현했다. 각 함수가 너무도 멋지게 다음 무대를 준비했다. 바로 이것이 답이다.

블록과 들여쓰기

if 문/else 문/while 문 등에 들어가는 블록은 한줄이어야 한다는 의미. 대게 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수가 작아질 뿐만 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기 쉬워진다. 이 말은 중첩 구조가 생길만큼 함수가 커져서는 안된다는 뜻이다. 당연한 말이지만 함수는 읽고 이해하기 쉬워진다

한가지만 해라!

함수는 한 가지를 해야한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야한다. 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서 일것이다. 함수가 ‘한가지’ 만 하는지 판단하는 방법이 하나더 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

함수당 추상화 수준은 하나로 !

함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 한 함수 내에서 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 근본 개념과 세부 사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

위에서 아래로 코드 일기 - 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한단계 낮은 함수가 온다. 죽, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이것을 내려가기 규칙이라고 한다.

Swith 문

switch 문은 작게 만들기 어렵다. case 분기가 단 두 개인 switch 문도 내 취향에는 너무 길며, 단일 블록이나 함수를 선호한다. 본질적으로 switch 문은 N가지의 일을 처리한다. 하지만 각 switch 문을 저차원 클래스로 숨기고 절대로 반복하지 않는 방법은 있다. 물론 다형성을 이용한다.


public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED :
      return calculateCommissionPay(e);
    case HOURLY :
      return calculateHourlyPay(e);
    case SALARIED :
      return calcualateSalariedPay(e);
    default :
      throw new InvalidEmployeeType(e.type);  
  }
}                          

// calculatePay와 동일한 구조의 코드 
public Money isPayday(Employee e, Date data);

// calculatePay와 동일한 구조의 코드 
public Money deliverPay(Employee e, Money money);

위의 함수는 여러가지 문제가 있는데, 함수가 길며, ‘한가지’ 작업만 수행하지 않으며, SRP를 위한반다. OCP를 위반한다.



public abtract class Employee {
  public abstract boolean isPayday();
  public abstract Money calculatePay();
  public abstract void deliverpay(Money pay);
}

public interface EmployeeFactory {
  pulic Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory {
  public Employee makeEmployee(EmployRecord r) throws InvalidEmployeeType {
    switch ( r.type ){
      case COMMISSIONED :
        return new CommissionEmployee(r);
      case HOURLY :
        return new HourlyEmployee(r);
      case SALARIED :
        return new SalariedEmployee(r);
      default : 
        throw new InvalidEmployeeType(r.type);
    }
  }
}

switch 문은 다형적 객체를 생성하는 코드 안에서 단 한 번만 참아 줄 수 있다.

함수 인수

함수에서 이상적인 인수 개수는 0개 ( 무항 ) 이다. 다음은 1개(단항)고, 다음은 2개(이항)다. 3개(삼항)는 가능한 피하는게 좋다. 최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다. SetupTeardownIncluder.render(pageData)는 이해하기 쉽다. pageData 객체 내용을 렌더링하겠다는 뜻이다. 인수가 2~3개가 필요하면 일부를 독자적인 클래스 변수라 선언할 가능성을 짚어본다.

부수효과를 일으키지 마라!

부수 효과는 거짓말이다. 함수에서는 한가지를 하겠다고 하고선 남몰래 다른 짓도 하기 때문이다. 함수 내부에 의도하지 않은 추가적인 역할이 존재한다는 것은 개발자로 하여금 혼란을 야기할 수 있다.

명령과 조회를 분리하라.

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다. 둘 다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다. 둘 다 하면 혼란을 초래한다.

오류 코드 보다 예외를 사용하라.

오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.


public void delete(Page page) {
  try {
    deletePageAndAllReferences(page);
  }
  catch(Exception e){
    logError(e)
  }
}     

private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e){
  logger.log(e.getMessage());
}

오류 처리도 한 가지 작업이다.

함수는 ‘한 가지’ 작업만 해야 한다. 오류 처리도 ‘한 가지’ 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅한다. 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재 컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

반복하지 마라 ( DRY - Don’t Repeat Yourself )

중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다. 객체 지향 프로그램은 코드를 부모 클래스로 몰아 중복을 없앤다. 구조적 프로그래밍, AOP( Aspect Oriented Programming), COP ( Component Oriented Programming) 모두 어떤 면에서는 중복 제거 전략이다.

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다. 즉, 함수는 return 문이 하나여야 한다는 말이다. 루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로,절대로 안된다.

함수를 어떻게 짜죠 ?

소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 만든다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다. 함수도 이와 동일하다. 대가 프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다.

프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현젹이 강한 언어를 만들어 이야기를 풀어간다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.

주석은 나쁜 코드를 보완하지 못한다.

코드로 의도를 표현하라!

코드에 주석을 포함하는 일반적인 이유는 코드 품질이 나쁘기 때문이다. 모듈을 짜고 보니 짜임새가 엉망이고 알아먹기 어렵다. 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.


package TEST.CommentSample;

public class CommentSample {

    public static Flag flag;

    public static void main(String[] args) {
        // 직원의 복지 혜택 자격 여부를 검사한다.
        if(  (worker.flags & Flag.HOURLY_FLAG == flag) && ( worker.ages > 65) ){
            System.out.println("문법을 통과했어~!");
        }


        if(worker.isEligiableForFullBenefits()){
            System.out.println("문법을 통과했어~!");
        }

    }

}

class worker {
    public static boolean flags = true;
    public static int ages = 70;
    public static Flag flag;

    public static boolean isEligiableForFullBenefits(){
        return (worker.flags & Flag.HOURLY_FLAG == flag) && ( worker.ages > 65);
    }

}

enum Flag {
    HOURLY_FLAG
}    

좋은 주석

  • 법적인 주석
  • 정보를 제공하는 주석
  • 의도를 설명하는 주석
  • 의미를 명료하게 밝히는 주석
  • 결과를 경고하는 주석
  • TODO 주석
  • 중요성을 강조하는 주석
  • 공개 API에서 Javadocs

나쁜주석

  • 주절거리는 주석
  • 같은 이야기를 중복하는 주석
  • 오해할 여지가 있는 주석
  • 의무적으로 다는 주석
  • 있으나 마나 한 주석
  • 무서운 잡음
  • 함수나 변수로 표현할 수 있다면 주석을 달지 마라
  • 닫는 괄호에 다는 주석
  • 위치를 표시하는 주석
  • 주석으로 처리한 코드
  • HTML 주석
  • 전역정보
  • 너무 많은 정보
  • 모호한 관계
  • 함수 헤더
  • 비공개 코드에서 Javadocs

코드의 형식

  • 적절한 행 길이를 유지하라.

  • 신문 기사처럼 작성하라.

아주 좋은 신문 기사를 떠올려라. 족자는 위에서 아래로 기사를 읽는다. 최상단에 기사를 몇 마디로 요약하는 표제가 나온다. 독자는 표제를 보고서 기사를 읽을지 말지 결정한다. 첫 문단은 전체 기사 내용을 요약한다. 세세한 사실은 숨기고 커다란 그림을 보여준다. 쭉 읽으며 내려가면 세세한 사실이 조금씩 드러난다.

  • 개념은 빈 행으로 분리하라.

  • 세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다.

  • 수직거리

변수선언
변수는 사용하는 위치에 최대한 가까이 선언한다. 우리가 만든 함수는 매우 짧으므로 지역 변수는 각 함수 맨 처음에 선언한다.

인스턴스변수
인스턴스 변수는 클래스 맨 처음에 선언한다. 변수 간에 새로로 거리를 두지 않는다. 잘 설계한 클래스는 많은 클래스 메서드가 인스턴스 변수를 사용하기 때문이다.

종속함수
한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다.

  • 개념적 유사성 : 어떤 코드는 서로 끌어 당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을 수록 코드를 가까이 배치한다.

세로순서
일반적으로 함수 호출 종속성은 아래 방향으로 유지한다. 다시 말해, 호출되는 함수를 호출하는 함수보다 나중에 배치한다. 그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.

가로 형식 맞추기
한 행은 가로로 얼마나 길어야 적당할까? 한 행에 글자수는 120자 이하가 적합하다.

가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

가로 정렬

들여쓰기

소스 파일은 윤곽도와 계층이 비슷하다. 파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보가 있고, 클래스 내 각 메서드에 적용되는 정보가 있고, 블록내 블록에 재귀적으로 적용되는 정보가 있다. 계층에서 각 수즌은 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위다.

올바르게 작성한 코드


import javax.naming.directory.Attributes;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

public class CodeAnalyzer implements JavaFileAnalysis {
  private int lineCount;
  private int maxLineWidth;
  private int widestLineNumber;
  private LineWidthHistogram lineWidthHistogram;
  private int totalChars;

  public CodeAnalyzer() {
    lineWidthHistogram = new LineWidthHistogram();
  }

  public static List<File> findJavaFiles(File parentDirectory) {
    List<File> files = new ArrayList<File>();
    findJavaFiles(parentDirectory, files);
    return files;
  }

  private static void findJavaFiles(File parentDirectory, List<File> files) {
    for (File file : parentDirectory.listFiles()) {
      if (file.getName().endsWith(".java"))
        files.add(file);
      else if (file.isDirectory())
        findJavaFiles(file, files);
    }
  }

  public void analyzeFile(File javaFile) throws Exception {
    BufferedReader br = new BufferedReader(new FileReader(javaFile));
    String line;
    while ((line = br.readLine()) != null)
      measureLine(line);
  }

  private void measureLine(String line) {
    lineCount++;
    int lineSize = line.length();
    totalChars += lineSize;
    lineWidthHistogram.addLine(lineSize, lineCount);
    recordWidestLine(lineSize);
  }

  private void recordWidestLine(int lineSize) {
    if (lineSize > maxLineWidth) {
      maxLineWidth = lineSize;
      widestLineNumber = lineCount;
    }
  }

  public int getLineCount() {
    return lineCount;
  }

  public int getMaxLineWidth() {
    return maxLineWidth;
  }

  public int getWidestLineNumber() {
    return widestLineNumber;
  }

  public LineWidthHistogram getLineWidthHistogram() {
    return lineWidthHistogram;
  }

  public double getMeanLineWidth() {
    return (double)totalChars/lineCount;
  }

  public int getMedianLineWidth() {
    Integer[] sortedWidths = getSortedWidths();
    int cumulativeLineCount = 0;
    for (int width : sortedWidths) {
      cumulativeLineCount += lineCountForWidth(width);
      if (cumulativeLineCount > lineCount/2)
        return width;
    }
    throw new Error("Cannot get here");
  }

  private int lineCountForWidth(int width) {
    return lineWidthHistogram.getLinesforWidth(width).size();
  }

  private Integer[] getSortedWidths() {
    Set<Integer> widths = lineWidthHistogram.getWidths();
    Integer[] sortedWidths = (widths.toArray(new Integer[0]));
    Arrays.sort(sortedWidths);
    return sortedWidths;
  }
}


interface JavaFileAnalysis {

}

class LineWidthHistogram {
  public void addLine(int lineSize, int lineCount) {
    //TODO: Auto-generated
  }

  public Attributes getLinesforWidth(int width) {
    return null;  //TODO: Auto-generated
  }

  public Set<Integer> getWidths() {
    return null;  //TODO: Auto-generated
  }
}