클린 코드의 중요성

클린 코드를 작성려고 노력하는 행위는 개발자라면 당연히 갖춰야한다. 클린 코드는 이해하기 쉽고 변경하기 쉬운 코드를 의미한다.

컴퓨터가 이해하는 코드는 어느 바보나 짤 수 있다. 좋은 프로그래머는 사람이 이해하는 코드를 짠다. - Martin Fowler

읽기와 쓰기에 소요되는 시간의 비율은 10:1 이 훨씬 넘는다. 우리는 새로운 코드를 작성하기 위한 노력의 일환으로 계속해서 오래된 코드를 읽고 있다. 그러므로 읽기가 쉬워지면 쓰기도 쉬워진다. - Robert C Martin

클린 코드는 코드를 읽는 방법, 공백이 있는 위치, 사물이라고 부르는 것과 같은 세부 사항에 중점을 둔 프로그래밍 철학이다.

클린 코드는 간단하지만 쉽지 않다.

장기적인 관점

일반적으로 소프트웨어를 개발하는 기간보다, 만들어진 소프트웨어를 유지보수하는 기간이 훨씬 길다. 그 말은, 내가 작성한 코드를 다른 사람이 읽을 가능성이 크다는 것이며, 읽기와 쓰기를 비교했을때, 읽기에 드는 시간이 훨씬 많다는 것을 암시한다.

양질의 코드는 작성하는 데 시간이 더 오래 걸리므로 초기 비용이 더 많이 들지만 코드의 가독성은 시간이 지남에 따라 소프트웨어를 성장시키는 데 도움이 된다. 파란색과 노란색 선이 교차하는 지점은 장기적으로 품질 코드가 나쁜 코드를 기하급수적으로 추월하는 지점이다.

가독성이 좋은 코드

클린 코드는 가독성이 좋은 코드라고 할 수 있다. 가독성이 좋은 코드란 로직을 쉽게 파악할 수 있는 코드를 의미한다. 말은 되게 쉬워보이지만 가독성 좋은 코드를 작성하기 위해서는 많은 노력들이 필요하다.

나쁜 코드

나중은 결코 오지 않는다. - leblanc's Law

나쁜 코드는 출시에 바빠 코드를 마구짜거나 엉망으로 만든 코드를 의미한다. 빨리 가는 길은, 언제나 코드를 깨끗하게 유지하는 습관이다.

From: Clean Code

Clean Code 에서 깨끗한 코드란 아래와 같이 소개하고 있다.

  • 꼼꼼하게 처리하는 코드
  • 한 가지에 집중하는 코드
  • 가독성이 좋은 코드
  • 다른 사람이 고치기 쉬운 코드
  • 테스트 케이스가 잘 작성된 코드
  • 아무리 우아하고 가독성이 높아도 테스트 케이스가 없으면 깨끗하지 않다.
  • 주의 깊게 짠 코드
  • 누군가 시간을 들여 깔끔하고 단정하게 정리한 코드이다.

켄트 백이 제안한 단순한 코드 구현 규칙

  • 모든 테스트를 통과한다.
  • 중복이 없다.
  • 시스템 내 모든 설계 아이디어를 표현한다.
  • 클래스, 메서드, 함수 등을 최대한 줄인다.

엉클 밥이 깨끗한 코드를 만드는 비결

  • 중복 줄이기
  • 표현력 높이기
  • 초반부터 간단한 추상화 고려하기

워드 커닝햄

워드는 깨끗한 코드란 읽으면서 놀랄 일이 없어야하는 코드라고 말합니다. 즉, 코드를 독해하느라 머리를 쥐어 짤 필요가 없어야 한다. 또한 언어를 단순하게 보이도록 만드는 책임 이 우리에게 있다고 한다.

보이스카우트 규칙

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라 - 보이스카우트 규칙

잘 짠 코드가 전부는 아니다. 시간이 지나도 언제나 깨끗하게 유지해야 한다. 즉, 유지보수할 때에는 보이스카우트 규칙을 적용하여 시간이 지날수록 코드가 좋아지도록 해야 한다.

네이밍

클린 코드를 추구하는 개발자라면 변수 명을 짓는데 오랜 시간을 소요해본 경험이 있을 것이다. 좋은 이름(변수, 클래스, 함수)은 품질 좋은 코드를 결정하는데 중요한 요소이다.

Naming Conventions

좋은 네이밍이란?

클린 코드에서는 좋은 네이밍을 의도가 분명한 이름을 짓는 것 이라고 한다. 좋은 네이밍은 코드 가독성 뿐만 아니라, 원할한 의사소통에도 큰 도움이 된다. 특히 복잡한 비지니스 로직일수록 의도가 분명한 좋은 네이밍은 빛을 발한다.

의도가 분명한 이름을 짓는 방법

변수명을 짓는데 가장 중요한 고려 사항은 다음과 같다.

  • 이름은 가능한 구체적이어야 한다. 모호하거나 하나 이상의 목적으로 사용될 수 있는 일반적인 이름은 보통 나쁜 이름이다.
  • 변수 이름이 변수가 표현하고 있는 것을 완벽하고 정확하게 설명해야 한다.
  • 변수를 한 가지 목적으로만 사용해야 한다.
  • 축약어는 최신 프로그래밍 언어에서는 거의 필요하지 않다. 정말로 축약어를 사용해야 한다면 프로젝트 사전에 축약어를 기록하거나 표준 접두사 접근 방법을 사용한다.

줄여 쓰지 말 것

불충분한 정보를 담고 있는 축약어보다는 긴 이름이 더 낫다.

// 나쁜 변수 명을 사용한 예제
x = x - xx
xxx = fido + SalesTax( fido )
x = x + LateFee( x1, x ) + xxx
x = x + Interest( x1, x )
// 좋은 변수 명을 사용한 예제
balace = balace - lastPayment
monthlyTotal = newPurchases + SalesTax( newPurchases )
balace = balace + LateFee( customerID, balace )
balace = balace + Interest( customerID, balace )

최적의 이름 길이

완벽함이란 더 이상 더할 것이 없는게 아니라, 무언가를 더 이상 뺄 것이 없는 것이다. - 생텍쥐페리

변수명이 길다고 다 좋은 것은 아니다.

변수 이름의 길이가 평균적으로 10~16일 때 프로그램을 디버깅하기 위해서 들이는 노력을 최소화 할 수 있고, 변수의 평균 길이가 8~20인 프로그램은 디버깅하기가 쉽습니다. 이름이 너무 길다면, 불필요한 이름을 담고 있지는 않은지 확인해봐야 한다.

변수명이 짧다고 항상 나쁜 것도 아니다. Code Complete 2 에서는 "i 라는 이름의 변수는 “일반적인 루프 카운터이거나 배열 인덱스이며, 몇 줄의 코드 외부에서는 전혀 중요하지 않다.” 하지만 짧은 이름은 문제를 야기할 수 있기 때문에, 주의 깊은 프로그래머들은 방어적인 프로그래밍 정책으로 그것들을 피한다." 라고 설명이 되어 있다.

모호하게 이름 짓지 말것

모호함이 발생하는 원인은 서술성이 부족하기 때문이다.

예를 들어 StringProcessor 는 문자열 관련한 어떤 작업을 처리하는 구나 정도로만 알 수있고, 그 이상의 정보는 알 수 없습니다. 하지만 StringFormatter 는 문자열 포매팅 관련 역할을 담당하는구나 라고 보다 분명한 의도를 우리에게 전달한다.

  • 모호한 함수 작성의 예시
// 모호한 함수 작성의 예시
// getFirstElementOrEmpty 와 같은 이름이 더 좋다.
String getFirstElement(List<String> list) {
    if(list == null || list.isEmpty()) {
    return "Empty";
    }

    return list.get(0);
}

난해함을 피하라

코드는 절대로 신조어 알아 맞히기가 되어서는 안된다. 낯선 전문 용어나, 내가 개발한 특별한 이름을 변수명에 사용하면 안된다.

단어의 뉘앙스를 고려하라

함수의 이름을 짓다 보면 유사한 단어 중 하나를 선택하느라 고민할 때가 있다. 특히 두 단어가 모두 자주 사용되는 단어라면 결정하기가 어려워 진다.

예를 들어 get 과 find 가 있다. get 은 주로 존재하는 값을 가져온다라는 의미로 사용되고 find 는 존재하지 않을 수도 있는 어떤 값을 가져올 때 사용된다.

체크리스트: 변수 이름

  • 일반적인 고려 사항
    • 이름이 변수가 표현하고자 하는 것을 완벽하고 정확하게 설명하는가?
    • 변수가 프로그래밍 언어의 해결책보다는 현실 세계의 문제를 가리키고 있는가?
    • 의미를 고민할 필요가 없을 만큼 이름의 길이가 긴가?
    • 계산 값 한정자가 이름의 끝에 있는가?
  • 짧은 이름
    • 코드가 긴 이름을 사용하는가(짧은 이름을 반드시 사용하지 않아도 되는 경우)?
    • 코드가 한 문자를 줄이기 위한 축약을 피하고 있는가?
    • 모든 단어가 일관성 있게 축약되었는가?
    • 이름을 발음하기가 쉬운가?
    • 잘못 읽히거나 발음되는 이름을 피했는가?
    • 짧은 이름을 변환 테이블에 기록했는가?

함수

DRY

함수를 작성할 때 가장 중요한 것 중 하나는 중복을 제거하는 것이다. DRY(Don't Repeat Yourself) 라는 원칙으로도 잘 알려져 있다.

Don't Repeat Yourself: 반복하지 마라

“Duplication is far cheaper than the wrong abstraction” – Sandi Metz, All the little things

From: 실용주의 프로그래머

소프트웨어를 신뢰성 높게 개발하고, 개발을 이해하고 유지보수하기 쉽게 만드는 유일한 길은 우리가 DRY 원칙이라고 부르는 것을 따르는 것뿐이라 생각한다. DRY 원칙이란 이것이다. 모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을만한 표현 양식을 가져야 한다.

실용주의 프로그래머에서는 중복을 4가지 범주로 분류한다.

  • 강요된(impose) 중복. 개발자들은 다른 선택이 없다고 느낀다. 환경이 중복을 요구하는 것처럼 보인다.
  • 부주의한 중복. 개발자들은 자신들이 정보를 중복하고 있다는 것을 깨닫지 못한다.
  • 참을성 없는 중복. 중복이 쉬워 보이기 때문에 개발자들이 게을러져서 중복을 하게 된다.
  • 개발자간의 중복. 한 팀에 있는(혹은 다른 팀에 있는) 여러 사람들이 동일한 정보를 중복한다.

코드 내의 주석이 너무 많거나 상세하면 DRY 원칙을 위반할 수 있다.

코드에는 주석이 있어야 하지만, 너무 많은 것은 너무 적은 것만큼이나 좋지 않다. 일반적으로 주석은 왜 이렇게 되어 있는지, 그 목적을 논해야 한다. 코드가 이미 어떻게 되어 있는지 보여 주기 때문에 이에 대해 주석을 다는 것은 사족이다. 게다가 이것은 DRY 원칙 위반이다.

From: Clean Code

  • 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.

어쩌면 중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다. 예를 들어, E.F.커드(E.F.Codd)는 자료에서 중복을 제거할 목적으로 관계형 데이터베이스에 정규 형식을 만들었다. 객체지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다. 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming) 모두 어떤 면에서 중복 제거 전략이다. 하위 루틴을 발명한 이래로 소프트웨어 개발에서 지금까지 일어난 혁신은 소스 코드에서 중복을 제거하려는 지속적인 노력으로 보인다.

  • 거의 모두가 이 규칙을 언급한다!

소프트웨어 설계를 거론하는 저자라면 거의 모두가 이 규칙을 언급한다. 데이비드 토머스와 앤디 헌트는 이를 DRY(Don't Repeat Yourself) 원칙이라 부른다. 켄트 벡은 익스트림 프로그래밍의 핵심 규칙 중 하나로 선언한 후 "한 번, 단 한 번만(Once, and only once)"이라 명명했다. 론 제프리스는 이 규칙을 "모든 테스트를 통과한다"는 규칙 다음으로 중요하게 꼽았다.

  • 어디서든 중복을 발견하면 없애라.

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. 이렇듯 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어난다. 다른 프로그래머들이 그만큼 어휘를 사용하기 쉬워진다. 추상화 수준을 높였으므로 구현이 빨라지고 오류가 적어진다.

  • 중복의 유형과 제거 방법

가장 뻔한 유형은 똑같은 코드가 여러 차례 나오는 중복이다. 프로그래머가 미친듯이 마우스로 긁어다 여기저기로 복사한 듯이 보이는 코드다. 이런 중복은 간단한 함수로 교체한다.

좀 더 미묘한 유형은 여러 모듈에서 일련의 switch/case 나 if/else 문으로 똑같은 조건을 거듭 확인하는 중복이다. 이런 중복은 다형성(polymorphism)으로 대체해야 한다.

더더욱 미묘한 유형은 알고리즘이 유사하나 코드가 서로 다른 중복이다. 중복은 중복이므로 TEMPLATE METHOD 패턴이나 STRATEGY 패턴으로 중복을 제거한다.

사실 최근 15년 동안 나온 디자인 패턴은 대다수가 중복을 제거하는 잘 알려진 방법에 불과하다. BCNF(Boyce-Codd Normal Form) 역시 데이터베이스 스키마에서 중복을 제거하는 전략이다. OO 역시 모듈을 정리하고 중복을 제거하는 전략이다. 짐작하겠지만, 구조적 프로그래밍도 마찬가지다.

Scope

함수의 Scope 는 수직 스코프와, 수평 스코프가 있다. 수평 스코프란 말 그대로 가로의 길이를 의미하며, 수직 스코프는 세로의 길이를 의미한다.

fun makeCleanCodeTemplate(template: Template) { // 수평 스코프
    ... // 수직 스코프
    ... // 수직 스코프
}

어느 하나라도 너무 길다면, 밸런스를 맞추는 것이 중요하다.

함수 이름 길이

"The length of a variable name should be proportional to its scope. The length of a function or class name is the inverse." - Uncle Bob Martin

변수의 유효 범위가 넓을 수록 긴 변수명이 짧은 변수명보다 낫다. 변수의 의미에 관한 혼란을 줄여주기 때문이다. 특히 함수가 public 이라면 이름은 가능한 짧아야 한다. 다시 말해 내부 구현을 이름에 담지 말아야 한다는 것이다. 반대로 내부 함수는 이름이 길수록 좋다. 내부 함수는 api 의 일부가 아니므로 구체적인 동작을 이름에 충분히 담아 서술성을 만족해줘야 한다. 긴 이름의 내부 함수는 그 함수의 동작에 대한 documentation 역할을 하기 때문이다.

클래스의 이름도 자주 호출될 수록 짧은 이름이 좋다.

그러나 예외가 있는데 하위 클래스가 많은 경우이다. 하위 클래스가 많을수록 각각의 클래스가 확실히 구분될 수 있도록 이름이 충분히 서술적이어야 한다.

SRP

하나의 책임만 가져야한다.

함수가 하나의 책임만 져야한다는 의미는, 함수명과 밀접한 관련이 있다.

http get 요청을 보내는 get() 함수가 있다고 가정한다. 이 함수는 내부적으로

  • tcp 소켓을 생성해
  • 서버와 connection 을 맺고
  • get 요청 메시지를 생성해
  • 요청 메시지를 서버에 전송한다.

그러나 이 함수는 하나의 역할을 수행한다고 볼 수 있다.

그 이유는 get() 이라는 함수 이름이 갖는 추상화 수준 에서는 http get 요청을 한다. 는 단 한 가지 역할만 수행하기 때문이다.

함수가 하나의 일만 해야 한다는 것은 함수 이름이 갖는 추상화 수준 을 기준으로 판별해야 한다.

일관된 추상화 수준

함수 내부의 추상화 수준에 대해서 알아보자.

void validateAndReport() {
  // validate
  validate();
  
  // report
  Reporter reporter = new Reporter();
  reporter.setX();
  reporter.setY();
  reporter.report();
}

위 validateAndReport() 함수는 추상화 수준이 일관되지 않는다. report 라는 추상화 수준은 없고 report 보다 한 단계 아래 수준의 코드가 있다.

위 코드는 아래와 같이 추상화 수준을 일관되게 맞춰 줘야 한다.

void validateAndReport() {
  validate();
  report();
}

함수 배치

  • 코드는 위에서 아래로 이야기 처럼 읽혀야 좋다.
    • Ex. public 메서드를 위에 배치하고, private 메서드를 아래로 배치하는게 좋다.
  • 신문기사 배치 방식 이라고도 부른다.
  • 한 클래스 내의 두 개의 함수에서 공통으로 호출하는 내부 함수는 어디에 위치시키는 것이 좋을까?
    • 개인 적으로는 두 함수의 아래에 두는게 좋다고 생각한다. 위와 같은 내부 함수는 주로 private 으로 선언되는데 public 함수들을 위로 올리고 private 함수들은 아래에 모아두는 방식을 취하는게 좋다.
    • 혹은 public 에서 사용되는 private 한 함수들을 public 과 맞닿은 방식을 취하는 것도 좋다.

Case 1

fun create() {
    val user = findById()
    ...
}

fun update() {
    ...
}

private fun findById() {

}

Case 2

fun create() {
    val user = findById()
    ...
}

private fun findById() {
    
}

fun update() {
    ...
}

Case 1과 2는 둘다 신문기사 배치 방식을 적용한 예이다.

들여쓰기

From: Clean Code

함수를 만드는 첫 번째 규칙은 작게 만드는 것이다. 함수를 만드는 두 번째 규칙은 더 작게 만드는 것이다. 이 규칙은 근거를 대기가 곤란하다. 증거나 자료를 제시하기도 어렵낟. 책의 저자는 오랜 시행착오를 바탕으로 작은 함수가 좋다고 확신한다.

함수는 얼마나 짧아야 할까 ? 2-5 줄 사이면 적당하다.

public static String renderPageWithSetupsAndTeardowns(
  PageData pageData,
  boolean isSuite
) throws Exception {
  includeSetupAndTeardownPages(pageData, isSuite);
  return pageData.getHtml();
}

다시 말해, if/else/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미이다. 대게 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수(enclosing function)가 작아질 뿐만 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.

From: ThoughtWorks Anthology

규칙1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.

메서드당 하나의 제어구조나 하나의 문장 단락(block)으로 되어 있는지 지키려고 노력해야한다.

  • Before
class Board {
    ...
    String board() {
      StringBuffer buf = new StringBuffer();
      for(int i=0; i<10; i++) {
        for(int k=0; k<10; k++) {
          buf.append(data[i][j]);
          }
        buf.append("\n");
      }
      return buf.toString();
   }    
}
  • After
String board() {
  StringBuffer bf = new StringBuffer();
  collectRows(buf);
  return buf.toString();
}

void collectRows(StringBuffer buf) {
  for(int i=0; i<10; i++) {
    collectRow(buf, i);
  }
}

void collectRow(StringBuffer buf, int row) {
  for(int i=0; i<10; i++) {
    buf.append(data[row][i]);
  }
  buf.append("\n");
}

이렇게 작게 잘 분할된 코드는 버그도 찾기 쉬우며, 가독성도 뛰어나서 유지보수에 좋다. 또한 재사용성이 많이 증가한다.

매개변수 개수

함수의 매개변수 개수는 적을수록 좋다. 매개변수는 많을 수록 가독성을 크게 저하시킨다. 단점은 가독성 뿐만 아니라 같은 타입의 매개변수가 연속으로 선언된 경우 실수가 발생할 가능성이 커져 예상치 못한 버그를 일으킬 수 있다.

매개변수가 4개 이상일 경우에 리팩토링을 하는 것을 추천한다.

퍼블릭 함수의 내부 구현

퍼블릭 함수의 이름에는 구현 디테일(how)이 들어가지 않는 것이 좋다. 함수를 호출하는 클라이언트 쪽의 코드가 구현 디테일을 염두하여 작성될 가능성이 커지기 때문이다. 퍼블릭 함수의 구현 디테일을 염두하여 코드를 작성하면 호출자 코드는 구현 디테일의 변경에 취약해진다.

private 한 함수는 구현 디테일이 들어가도 상관 없다.

클린 코드를 위한 팁

  • 매직 넘버(magic number)를 피한다.
    • 매직 넘버란 100, 47524 와 같이 아무런 설명 없이 프로그램에서 사용되는 값을 의미한다.
    • 필요하다면 0과 1은 그냥 사용해도 괜찮다.
  • 0으로 나눔 오류(divide-by-zero)를 미리 방지한다.
  • else 예약어 금지
    • if 절에서 조건을 검사한 후 바로 리턴하거나 예외를 던지도록 코드를 수정해 else 절을 없애주는게 중괄호 중첩을 막는 좋은 방법이다.

Don't reinvent the wheel

이미 개발된 기능을 다시 만드는 데 시간을 쓰지 말라는 뜻이다. 실제로 본인이 사용 중인 클래스에 이미 있는 기능을 직접 개발하거나 다른 라이브러리를 추가해가며 사용하지 말라는 것이다.

YAGNI(You aren't gonna need it)

클래스나 함수는 실제로 필요할 때 만드는게 좋다.

YAGNI(You aren't gonna need it)는 프로그래머가 필요하다고 간주할 때까지 기능을 추가하지 않는 것이 좋다는 익스트림 프로그래밍(XP)의 원칙이다. 익스트림 프로그래밍의 공동 설립자 론 제프리스는 다음과 같이 썼다: "실제로 필요할 때 무조건 구현하되, 그저 필요할 것이라고 예상할 때에는 절대 구현하지 말라."

만약 미리 만들어둔 클래스나 함수가 새로 바뀐 환경이나 설계에 맞게 적절히 수정되지 않았을 경우 버그의 원인이 될지도 모른다. 실제로 사용하지 않는 코드일수록 수정되지 않을 가능성이 커진다.

References

  • Clean Code / Robert C Martin 저 / 인사이트
  • Code Complete 2 / Steve McConnel 저 / 위키북스
  • 실용주의 프로그래머 / 앤드류 헌트,데이비드 토머스 공저 / 인사이트
  • ThoughtWorks Anthology / Martin Fowler 저 / 위키북스