Clean Code

2022년 1월 28일 · #독서


저자: 로버트 C. 마틴

드디어 이 책을 읽었다. 클린코드에 대해 관심이 없던 것은 아니고, 인터넷 상에는 클린코드에 대해 정리한 글들이 많았고, 비슷한 주제의 <개발자의 글 쓰기> 라는 책도 읽었었다. 하지만 책을 읽어야 더 깊고 많은 내용을 이해할 수 있을 것 같아서 책을 구입했다.

이 글은 책에서 나오는 내용들을 정리하며 내 생각을 더했다.


2-3년 동안 클린하지 못한 코드가 쌓인다. 얽히고 섥힌 코드를 해독하고, 얽히고 섥힌 코드를 더한다. 시간이 지날수록 쓰레기 더미는 점점 높아지고 싶어진다. 생산성은 제로에 가까워진다... 생산성이 떨어지고 새 인원이 추가 되지만 새 인력은 시스템 설계에 대한 조예가 깊지 않다. 그들은 생산성을 높여야 한다는 압력에 시달리고 나쁜 코드들을 더 많이 양산한다. 더 이상 복구할 수 없는 수준에 이른다. (책에서 나오는 상황을 요약함)

많은 스타트업 회사들이 이러한 문제를 겪고 있을 거라고 생각한다. 빠른속도로 개발하느라 코드의 품질을 고려하지 않기 때문이다.

이 것은 누구의 탓일까?

인정하기 어려우리라. 어째서 우리 잘못입니까? 요구사항은 어쩌구요? 일정은요? 멍청한 관리자와 쓸모없는 마케팅 인간들은요? 그들에게는 잘못이 없다는 말입니까?

저자는 스파게티 코드의 잘못을 프로그래머의 책임이라고 말한다. 나도 이 부분에 대해서 일정을 몰아치는 관리자의 탓이라고 생각했었다. 여기서 저자의 비유가 난 인상깊었다.

자신이 의사라 가정하자. 어느 환자가 수술 전에 손을 씻지 말라고 요구한다. 시간이 너무 걸리니까. 확실히 환자는 상사다. 하지만 의사는 단호하게 거부한다. 왜? 질병과 감염의 위험은 환자보다 의사가 더 잘 아니까. 환자 말을 그대로 따르는 행동은 (범죄일 뿐만 아니라) 전문가답지 못하다.

프로그래머도 마찬가지. 나쁜 코드의 위험을 이해하지 못하는 관리자의 말을 그대로 따르는 행동은 전문가답지 못하는 저자의 말이다. 이 부분에 대해서 많은 공감이 되고, 이러한 개발자가 되기 위해 노력할 것이다.

하지만 현실적으로, 한국에서는 지위가 낮고 경험이 부족한 개발자들은 의견에 설득력을 얻지 못하는건 사실이다. 어떤 팀에서는, 개발 팀장이 코드 퀄리티는 무시하고 무조건 일정에 맞춰 달라고 이야기 하기도 한다. 이 부분을 이해하지 못하는건 아니다. 시리즈 A 투자를 받으려면 빠르게 결과를 보여줘야 하기 때문이다.

하지만 그럼에도 불구하고 개발자는 클린코드를 지향해야 한다. 초반 생산성은 떨어질 수 있지만 어느정도 시간이 지나면 오히려 생산성은 높아진다. (클린하지 못한 코드의 생산성이 급격히 떨어진다는 표현이 맞을지도)

그리고 다른 중요한 한가지는 여러가지 버그를 발생시킬 위험을 낮출 수 있다는 것이다. 스파게티 코드는 버그를 찾기도 힘들며 A를 고치면 B에서 터지고 여기저기서 마구마구 터져버리는 일이 발생하기 쉽다.

저자는 이렇게 말한다.

한두 해 이상 우리 분야에 몸 담은 프로그래머라면 누구나 나쁜 코드가 업무 속도를 늦춘다는 사실을 익히 안다. 그럼에도 모든 프로그래머가 기한을 맞추려면 나쁜 코드를 양산할 수밖에 없다고 느낀다. 간단히 말해, 그들은 빨리 가려고 시간을 들이지 않는다.

진짜 전문가는 두 번째 부분이 틀렸다는 사실을 잘 안다. 나쁜 코드를 양산하면 기한을 맞추지 못한다. 오히려 엉망진창인 상태로 인해 속도가 곧바로 늦어지고, 결국 기한을 놓친다.


저자는 의도가 분명한 이름이 정말로 중요하다고 강조한다. 이름을 짓는데에 시간이 걸리겠지만, 좋은 이름으로 절약하는 시간이 더 많다고 한다.

  • 어떠한 날짜를 표기할때는 let d 보다는 let elapsedTimeInDayslet daysSinceCreation등의 이름이 더 알아보기 쉬울 것이다.
  • 지뢰찾기 게임을 만든다고 가정했을 때 게임판의 각 칸을 담은 배열은 theList 라는 이름 보다는 gameBoard라는 이름이 더 어울릴 것이다.

내가 경험한 사례가 있다. 회사에 처음 입사하고 소스코드를 인수인계 받았다. 많은 함수의 매개변수가 아래 함수 처럼 한 글자로 표시 되어 있었다.

function func(s, m, t) {
  ...
}

심지어 타입스크립트도 아니었고 일반 자바스크립트였다. 저게 뭘 의미하는지도 모르겠고 타입도 없어서 더 모르겠다. 지금도 다른건 기억 안나는데 mdata를 의미 했었다는 것만 기억난다.... 대체 왜 m이었을까?

이런 경험을 해 보았기에 의도가 분명한 이름이 중요하다는 사실을 몸으로 느꼈다.

코드레 그릇된 단서를 남기는 것은 코드의 의미를 흐린다. 나름 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용해서도 안된다.

예를들어 여러 계정을 그룹으로 묶을 때 실제 List가 아니라면 accountList라 명명하지 않는다. 프로그래머에게 List라는 단어는 특수한 의미다.

문자 하나를 사용하거나 상수등은 텍스트 코드에서 쉽게 눈에 띄지 않는다. 검색해도 너무 많이 나올 것이다. 아래와 같이 이름을 의미 있게 지으면 함수가 길어질 수는 있겠지만 검색하기도 쉽고 다른 개발자가 이해하기도 쉽다.

const int WORK_DAYS_PER_WEEK = 5;

클래스와 객체 이름에는 명사나 명사구가 적합하다. Manager, Processer, Data, Info 등의 단어는 피한다.

  • Customer
  • WikiPage
  • Account
  • AddressParser

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

  • postPayment
  • deletePage
  • save

접근자, 변경자, 조건자는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.

추상적인 개념 하나에 단어 하나를 선택하여 이를 고수해야 한다. 예를들어 똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.

마찬가지로 동일 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스럽다. DeviceManagerProtocolController는 근본적으로 어떻게 다른가? 어째서 둘 다 Controller가 아닌가? 어째서 둘 다 Manager가 아닌가? 정말 둘 다 Driver가 아닌가? 이름이 다르면 독자는 당연히 클래스도 다르고 타입도 다르리라 생각한다.

스스로 의미가 분명하지 않은 이름들은 맥락을 부여한다. 예를들어 firstName, lastName, street, city, state, zipcode 라는 변수가 있다. 변수들을 훑어보면 주소라는 사실을 금방 알아챈다. 하지만 어느 메서드가 state라는 변수 하나만 사용한다면?

addr이라는 접두어를 추가해 맥락을 분명하게 해주면 좋다. addrFirstName, addrState, ...


저자는 함수를 만드는 첫째 규칙은 작게 두번째 규칙은 더 작게 라고 강조한다. 함수가 작을수록 더 좋다는 증거나 자료를 제시하기를 어렵지만 40여년간의 경험과 시행착오를 바탕으로 작은 함수가 좋다고 확신한다고 얘기한다.

저자는 TDD의 창시자인 켄트백의 코드를 봤을때의 경험을 이야기한다.

켄트가 짠 코드는 모든 함수가 2, 3, 4줄정도 였다. 각 함수가 너무나도 명백했고 하나의 이야기만을 표현했다. 각 함수가 너무나도 멋지게 다음 무대를 준비했다.

if / else / while문 등에 들어가는 블록은 오직 한줄이어야 하고 대개 거기서 다른 함수를 호출하는 방향이 좋다. 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다. 이 말은 중첩구조가 생길 만큼 함수가 커져서는 안 된다는 뜻이라고 저자는 말한다.

한 함수 내에서 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려울 것이다. 문제는 이 뿐만이 아니다. 깨진 창문의 법칙처럼 사람들이 함수에 세부사항을 점점 추가한다.

리액트 컴포넌트를 개발할때도 비슷한 문제를 겪은 적이 있다.

return (
  <div>
    <div>
      <Flex>
        <h2>Title</h2>
        <Icon />
      </Flex>
      <여러 컴포넌트 들 ...>
    </div>
    <div> ... </div>
    <UserList />
    <여러 컴포넌트 들 ...>
    <div>
      <여러 컴포넌트 들 ...>
    </div>
  </div>
)

위와 같은 코드에서 <UserList />는 꽤나 크고 중요한 컴포넌트였다. 하지만 수십줄의 코드들 사이에서 아주 작고 중요하지 않은 컴포넌트인 것 처럼 사이에 끼워져 있을 뿐이었다. 코드를 찾기도 파악하기도 힘들었다. 이것도 아마 일정에 쫒겨 급하게 개발하느라 저렇게 되어있는 것 같았다. 위 코드는 아래처럼 정리하면 더 좋을 것 같다.

return (
  <div>
    <TitleSection />
    <UserList />
    <SomeComponent />
  </div>
)

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. 워드 커닝햄 (위키 창시자)

길고 서술적인 이름이 짧고 어려운 이름 보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

저자는 함수 이름을 정할 때 이름이 길어지는 것을 겁먹지 말라고 표현했다. 서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해진다.

함수에서 가장 이상적인 인수 개수는 0개이다. 다음은 1개이고 다음은 2개이다. 3개부터는 피하는 편이 좋다.

단항

함수에 1개의 인수를 넘기는 이유는 흔히 인수에 질문을 던지는 경우(myFile: File) => boolean인수를 뭔가로 변환해 결과를 반환하는 경우(myFile: File) => InputStream로 본다.

플래그 인수

저자는 이렇게 말한다. "플래그 인수는 추하다. 함수로 boolean 값을 넘기는 관례는 정말로 끔찍하다. 왜냐고? 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까! 플래그가 참이면 이걸 하고 거짓이면 저걸 한다는 말이니까!"

이항

인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다. 직교좌표계 (x, y)등 이항함수가 어울리는 경우도 있다. 하지만 일반적인 상황에서, 가능하다면 단항함수로 바꾸도록 애써야 한다.

삼항

인수가 3개인 함수는 훨씬 더 이해하기 어렵다. 순서, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다. 그래서 삼항함수를 만들 때는 신중히 고려해야 한다.

부수 효과는 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하는 것이다. 때로는 예상치 못하게 클래스 변수를 수정하고 때로는 함수로 넘어온 인수나 시스템 전역 변수를 수정한다.

여기서 부수효과란, 함수 밖에 있는 값에 영향을 주는 것을 말한다. 함수 내에서 외부 값에 영향을 주는 것이 아니라 적절한 값을 return 해주는 형태로 가야한다. (순수함수)

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다.

중복은 문제다. 코드 길이가 늘어날 뿐 아니라 알고리즘이 수정되면 여러 곳을 손봐야 한다. 중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법들이 중복을 없애거나 제어할 목적으로 나왔다.


if-else문으로 오류를 처리하면 호출자 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야한다. 불행히도 이 단계는 잊어버리기 쉽다. 그래서 오류가 발생하면 예외를 던지는 편이 낫다. 그러면 호출자 코드가 더 깔끔해진다. 논리가 오류처리 코드와 뒤섞이지 않으니까.

예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하자. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하가 쉬워진다.

예외를 던진 때는 전후 상황을 충분히 덧붙인다. 오류메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.


캡슐화란?

  • 객체의 속성(data fields)과 행위(메서드, methods)를 하나로 묶고,
  • 실제 구현 내용 일부를 내부에 감추어 은닉한다.

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다. 때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다.

저자는 함수에서 설명한 것과 마찬가지로, 클래스를 만들때 첫째는 작아야하고 둘째도 작아야하고 셋째도 작아야한다. 라고 말한다. 여기서 그 유명한 단일책임원칙이 등장한다.

클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다. SRP는 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다.

책임. 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화 하기도 쉬워진다. 더 좋은 추상화가 쉽게 떠오는다.

많은 개발자는 자잘한 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려한다. 하지만 작은 클래스가 많은 시스템이든 큰 클래스가 몇 개뿐인 시스템이든 돌아가는 부품은 그 수가 비슷하다.

도구 상자를 어떻게 관리하고 싶은가? 작은 서랍을 많이 두고 기능과 이름이 명확한 부품들을 나눠 넣고 싶은가? 아니면 큰 서랍 몇개를 두고 모두를 던져 넣고 싶은가?

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

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

변수가 아주 많은 큰 함수 하나가 있다. 큰 함수의 일부를 작은 함수 하나로 빼내고 싶은데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다. 그렇다면 변수 네개를 새 함수에 인수로 넘겨야 옳을까?

전혀 아니다! 만약 네 변수를 클래스 인스턴스 변수로 승겨한다면 새 함수는 인수가 필요 없어진다. 그 만큼 함수를 쪼개기 쉬워진다.

이렇게 하면 클래스가 응집력을 잃는다. 몇몇 함수만 사용하는 인스턴스 변수가 점점 늘어나기 때문이다. 여기서 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리해도 된다. 클래스가 응집력을 잃는다면 쪼개라!