클린 코드 (Clean Code) - 1

Sehee Jeong·2021년 9월 18일
9

Clean Code

목록 보기
1/5

Intro

깨끗한 코드를 짜는 방법에 대해 소개하는 책에서는 대부분 "나쁜코드"에 대한 사례에 대해 우선적으로 짚고 넘어간다. 제품을 출시하는데만 집중해 코드퀄리티를 챙기지 못하는 상황이 지속되어, 새로운 코드를 짜는 속도가 급격히 하락하는 사태가 발생하는 것이다. 이 과정에서 기존 레거시를 알고있던(혹은 만들어냈던) 직원은 퇴사하고, 관리자는 이미 하락한 생산성을 높이기 위해 직원을 더 고용하게 된다. 관리자는 직원을 고용하는만큼 생산성 향상을 기대하지만, 비용 대비 감당할 수 없는 만큼의 코드 퀄리티는 회사를 벼랑끝까지 몰게 만들고 결국 회사는 망하게 된다는 새드엔딩을 알려준다.

클린코드에서는 초반에 개발자가 가져야하는 절대적인 습관 중 한가지를 언급하고 있다. 나중은 결코 오지 않기 때문에 제품을 만든 후에 코드를 개선하는 것보다, 초반 설계를 잘 이뤄내자.

나 또한 냄새나는 코드를 만들어낸 장본인이다. 일정에 급급해 빠르게 제품을 개발하면서, 코드 퀄리티는 챙기지 못한 적이 많다. 미래의 코드 생산성보다 당장의 배포가 중요했었던 탓에 지금에서만 편한 코드를 생산해내는 행위를 반복했었다. "배포가 끝나고나면 조금은 한가해질테니, 그 시간동안 리팩토링을 해보자" 라는 지키지도 못할 약속과 함께 (ㅎㅎ)

그리고 몇개월이 지난 뒤, 다시 그 냄새나는 코드에 새로운 제품을 추가해야할 때, 과거에 나를 원망한 적이 한두번이 아니다. 아 대체 누가 짠 코드야? 속으로 분노하면서 코드의 git annotation 을 확인하면, 내 jsh-me 가 떡하니 적혀있지 뭐람? 정말 아이러니하다. (과거의 나는 반성해라!)

또한 이 책은 깨끗한 코드를 유지하려는 습관을 가지자는 교훈도 담고 있다. 많은 케이스 중 관리자와 프로그래머의 관계에 대한 가벼운 예시를 들어보려고 한다.

관리자에게 가장 우선순위가 높은 것(혹은 제일 방어적인 것)은 완성된 제품과 기한일 것이다. 제시한 기한 내에 완성된 제품을 보여주어야 하는 것이 그들의 할 일이기 때문이다. 관리자가 프로그래머의 일정과, 프로젝트 계획에 깊게 관여하게 되는데, 대체로 프로그래머는 그들의 요구사항을 대부분 들어주게된다. 프로그래머는 관리자가 요청한 일정에 맞추어 개발을 진행하다가 어느순간 일정이 타이트 해짐을 느끼는 순간이 존재하게 될텐데, 그 때부터 나쁜 코드가 생산되는 지름길이 열린다고 생각하면 된다.

클린코드에서는 프로그래머란 언제나 코드를 깨끗하게 유지할 수 있는 사람이어야 한다고 언급한다. 기획자가 일정을 사수해야하는 것은 그들의 의무니까 당연한 행동이다. 하지만 우리 프로그래머도 퀄리티 높은 코드를 지켜야하는 의무를 가지고 있으므로, 우리의 의무를 다 해낼 수 있는 습관을 가져보자고 당부한다.

What is Clean Code?

유명한 프로그래머들은 깨끗한 코드란 무엇인가? 에 대한 질문에 다양한 답변을 남겼는데, 그 중 기억에 남는 문구를 몇가지 적어보았다.

  • 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 깨끗한 코드는 한가지를 제대로 한다.
  • 깨끗한 코드는 설계자의 의도를 결코 숨기지 않는다.
  • 깨끗한 코드는 읽기 쉽고, 고치기도 쉽다. (가독성)
  • 깨끗한 코드는 고칠 곳이 없다.
  • 깨끗한 코드는 중복이 없다.
  • 작게 추상화해라
  • 메소드, 클래스 이름에서 예상했던 그대로를 수행한다.

2장. 의미 있는 이름

2장에서는 변수, 함수, 클래스 등에 붙이는 이름을 잘 짓는 방법에 대해 소개한다.

1. 의도를 분명히 밝혀라
의도가 분명한 이름은 정말 중요하다. 좋은 이름을 지으려면 시간이 오래 걸리지만, 좋은 이름으로 절약하는 시간이 더 많다는 것을 기억하자.

var d: Int // 경과 시간

위에서 d는 경과시간을 나타내는 변수이다. 사실 의미없는 변수와 주석의 조합보다는, 의미를 충분히 나타낼 수 있는 변수 하나가 큰 힘을 발휘할 수 있다.

var elapsedTimeInDays: Int
var daysSinceCreation: Int

의도가 드러난 이름을 사용한다면, 전반적인 코드 이해력이 높아지고 추후에 코드 변경도 쉬워진다.


2. 그릇된 정보는 피하라
프로그래머는 적합하지 않은 단어를 사용하면 안된다. 예시로, 여러 계정의 그룹을 묶을 때, 실제 list가 아니라면 accountList라는 변수를 사용해서는 안되고, 단순하게 accounts 로 명명해서 명확한 정보를 제공해야한다.
또한 흡사한 이름 사용을 지양하도록 하자. 만약 다른 모듈 간에 비슷한 변수명을 사용하고 있다면, 코드를 읽는 개발자는 이들의 차이를 인지하는데 시간이 걸릴 수 있다.


3. 의미있게 구분해라
변수의 이름이 달라야 한다면, 의미도 달라야 한다. 연속적인 숫자를 덧붙인 이름(a1, a2, a3, ..., aN)은 의도적인 이름도 아니며, 아무런 정보도 제공하지 못한다.

val moneyAmount: Long
val money: Long

val customerInfo: Customer
val customer: Customer

moneyAmount와 money는 구분이 되지 않는다. 또한 customerInfo와 customer도 구분이되지 않는다. 읽는 사람이 차이를 알 수 있도록 이름을 지어라.


4. 검색하기 쉬운 이름을 사용해라
상수는 텍스트코드에서 쉽게 눈에 띄지 않는다는 단점이 있다.

private const val MAX_CLASSES_PER_STUDENT = 7

MAX_CLASSES_PER_STUDENT를 사용하는 클래스를 찾아 값을 바꿔야하는 상황일 때, 우리는 해당 키워드를 이용해 쉽게 검색이 가능하다. 하지만 위처럼 상수화 해놓지 않는다면 7이라는 숫자만을 이용해 찾아야 하는 경우가 생기는데, 자칫 잘못하다 다른 클래스에서 사용하고 있는 7을 변경할 수 있는 문제가 생길 수 있다.


5. 클래스 이름 / 메서드 이름

  • 클래스, 객체 이름은 명사 or 명사구가 좋다. 하지만 Manager, Processor, Data, Info등과 같은 단어는 피하자.
  • 메서드 이름은 동사 or 동사구가 좋다.

6. 한 개념에 한 단어를 사용하라
추상적인 개념 하나에 단어 하나만을 사용하자. 예시로, 똑같은 메서드 클래스마다 fetch, retreive, get 등등 여러가지로 불리게 되면 혼란스러워지고, 어느 클래스에 어느 이름을 썼는지 기억하기가 어렵다.
일관성있는 어휘는 프로그래머가 쉽게 코드를 읽을 수 있도록 도와줄 것이다.

7. 해법 영역 / 문제 영역에서 가져온 이름을 사용해라
코드를 읽는 사람도 프로그래머다. 그러므로 문제영역(domain)에서 사용되는 용어보다는 기술개념에서 사용되는 용어를 사용해라. 하지만 적절한 기술 용어가 없는 경우에는, 문제영역(domain)에 사용되는 용어를 사용해라.


8. 불필요한 맥락을 없애라
고급 휘발유 충전소(Gas Station Deluxe) 라는 어플리케이션을 만든다고 가정했을 때, 모든 클래스 이름의 접두사를 GSD 로 시작할 이유는 없다. 오히려 부적절한 명명법이다.
GSD를 어플리케이션에서 포괄적으로 나타내고 있기 때문에 세부 사항에서도 GSD를 언급하는 것은 불필요한 맥락이며 코드를 읽는 난이도도 높아질 수 있다고 생각한다. 의미가 분명한 경우에는 짧은 이름이 긴 이름보다 좋다. 이름에 불필요한 맥락을 추가하지 않도록 주의하자.

3장. 함수

공통된 로직을 빼내 함수로 추출하고 변수명을 변경하는 것만으로 프로그램 내부를 직관적으로 파악할 수 있을 만큼 읽기 쉬운 코드로 완성될 수 있다. 그렇다면 함수에 어떤 속성을 부여해서 추출해야지만, 읽는 사람으로부터 직관적이다는 느낌을 받을 수 있을까?


1. 작게 만들어라
첫번째는 함수를 작게 만드는 것이다. 클린코드에서는 정말정말 짧은 길이의 함수를 만들고, 그 의미를 명백하게 만들 것을 강조하고 있다.

fun renderPageWithSetupsAndTeardowns(
    pageData: PageData,
    isSuite: Boolean
) {
    if(isTestPage(pageData)) 
      includeSetupAndTeardownPages(pageData, isSuite)
    return pageData.getHtml()
}

해당 함수는 단 3줄만으로 행위를 다 담아내고 있다. 내부에 있는 함수가 이야기 하나를 표현하고있고, 그 의도가 너무 명백하다.
다시 말해서 if/else & white 문에 들어가는 블록은 한줄이어야 한다.
그렇다면 바깥을 감싸고 있는 함수가 작아질 뿐만 아니라, 이해하기도 쉬워진다. 이 의미는 곧 중첩구조가 생길만큼 함수가 커져서는 안된다는 뜻이기도 하다.


2. 한 가지만 해라
제일 중요하다. 함수는 한 가지만 해야한다. 그 한가지를 잘해야 한다. 하지만 언급하고 있는 "한가지" 가 무엇을 의미하는지는 알기 어렵다.


3. 함수 당 추상화 수준은 하나로
함수가 확실히 "한 가지" 작업만 하려면 함수의 모든 문장의 추상화 수준이 동일해야 한다. 책에서는 메소드에 따라 추상화 수준이 어떤지 정의하고 있다.

getHtml() {} // 추상화 수준이 매우 높음
val pagePathName = PathParser.render(pagepath) // 추상화 수준이 중간
buffer.append("\n") // 추상화 수준이 낮음

getHtml() 은 html을 가져올 수 있는 코드이다. 어떠한 인자 없이 실행되고 있으며, 그 내부는 다양한 로직과, 또다른 메소드를 통해 파싱이 이루어지고 있을 테지만 우리는 해당 메소드를 통해 어떻게 진행되고있는지 모른다. "html 만 얻어와" 만 알 뿐이다. 그리고 이 메소드는 다른 곳에서도 범용적으로 사용할 수 있다. 그래서 추상화 수준이 높다고 정의한다.

PathParser.render(pagePath) 는 페이지 경로를 파싱해서 렌더링해주는 함수이다. pagePath 를 인자로 넣어주어야 실행될 수 있는 메소드이다. pagePath 값이 아닌 pageName, pageExtension 을 넣으려면 원하는 대로 작동하지 않을 것이다. 이처럼, 특정 범위(pagePath 범위 내)에서 사용할 수 있는 메소드들을 추상화 수준이 중간이라고 표현한다.

append("\n") 과 같은 경우는 추상화 수준이 아주 낮다. 우선 하드코딩되어있는 문자열이 사용할 수 있는 메소드의 범위를 매우 축소시켜주고 사용처 또한 제한적이기 때문이다.

만약 한 함수내에 다양한 추상화 수준이 들어간다면, 읽는 사람으로 하여금 헷갈리게 만들 수 있다. 특정 표현이 근본 개념인지 세부사항인지 구분하기 어렵기 때문이다. 절대 근본 개념과 세부사항을 뒤섞지 말아라.

  • 위에서 아래로 코드 읽기: 내려가기 규칙
    코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 위에서 아래로 내려갈수록 추상화 수준이 한 단계씩 낮아진다. 책에서는 아래와 같은 예시를 들고 있다.

👉 1) TO 설정 페이지(2)와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지를 포함하고, 해제 페이지를 포함한다.
👉 2) TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함(3)한 후 일반 설정 페이지를 포함한다.
👉 3) TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "suiteSetUp" 페이지를 찾아(4) include 문과 페이지 경로를 추가한다.
👉 4) TO 부모 계층을 검색하려면, ....

코드를 내려가면서 현재 추상화 수준을 설명하면서 그 수준을 일관되게 유지해야한다.

4. 서술적인 이름을 사용해라
함수가 하는 일을 잘 표현하기 위해 서술적인 이름을 사용해라. 길고 서술적인 이름은 길고 서술적인 주석, 어렵고 짧은 이름보다 훨씬 좋다.

5. 함수 인수
이상적인 함수의 인수는 0개이다. 인수는 함수의 개념을 이해하기 어렵게 만든다. 함수의 이름과 인수의 갯수의 차이에 따라 추상화 수준이 달라지게된다. 코드를 읽는 사람에게 includeSetupPageInfo(PageContent()) 보다 includeSetupPageInfo()가 더 이해하기 쉬울 것이다.

테스트 관점에서도 동일하다. 여러 인수 조합으로 구성된 함수는 검증하기 위한 테스트코드가 복잡해질 것이다. 최선은 입력인수가 없는 경우이며, 차선은 입력인수가 1개인 경우이다.

함수에 1개의 인자를 넘기는 가장 큰 경우는 두가지이다. 첫번째는 인수에게 질문을 던지는 경우, 두번째는 인수를 무언가로 변환해 결과로 반환하는 경우이다.

fun fileExists(fileName: String): Boolean // 첫번째 케이스
fun fileOpen(fileName: String): InputStream // 두번째 케이스

이 경우가 아니라면 단항함수는 가급적 피하는 것이 좋다.

플래그 인수는 함수가 한번에 여러가지의 일을 한다는 것을 암시하기 때문에, 절대로 지양해야한다.

인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다. 하지만 point(x, y) 와 같이 인수가 2개여도 자연스럽게 읽히는 관계에 대해서는 사용해도 좋다.

인수가 3개인 함수는 신중이 사용하라고 한다. 예시로, assertEquals(msg, expected, actual) 이라는 함수가 존재하게 될때 코드를 읽는 사람은 매번 함수를 볼 때마다 주춤하다가 msg 를 무시해야한다는 사실을 상기할 것이다. 인자가 3개인 함수는 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다.

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면, 함수와 인수가 동사/명사 쌍을 이루는 것이 좋다. 예를들어 assertEquals 보다 assertExpectedEqualsActual 처럼 말이다. 그렇다면 인수 순서를 기억할 필요가 없어진다.


6. 부수효과를 일으키지 마라
함수에서 한 가지 일만 하기로 약속하고선 다른 일을 해서는 안된다. 한 함수에서 부수효과가 나타난다는 것은 거짓말을 하고있음과 같은 이야기이다.


7. 명령과 조회를 분리해라
함수는 무엇인가를 수행하거나 답하거나 둘 중 하나만 담당해야한다.

fun set(attribute: String, value: String) Boolean

if (set("username", "unclebob")) { }

set 함수는 이름이 attribute인 속성을 찾아 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다. 그렇다면 코드를 읽는 사람에게는 if 문을 어떻게 해석할까? username 이 unclebob으로 설정되어있는지 확인하는 코드인가? username 을 unclebob으로 설정하는 코드인가? 해석하기가 어려워진다. 코드만 봐서는 의미가 모호하기 때문이다.

if (attributeExists("username")) { 
  setAttribute("username", "unclebob")
}

이 문제를 해결하기 위해서는 명령과 조회를 분리해 혼란을 제거하는 것이다.

profile
android developer @bucketplace

2개의 댓글

comment-user-thumbnail
2021년 9월 25일

좋은 글 감사합니다 ^^

답글 달기
comment-user-thumbnail
2021년 9월 26일

도움이 많이 되었습니다.

답글 달기