클린 코드와 소프트웨어 장인 정신

백중원 (Leopold)
21 min readDec 23, 2017

--

필자가 중요하게 생각하는 것 중에 하나가 바로 장인 정신이다. 장인 정신의 사전적 의미를 우선 살펴보자.

자기가 하고 있는 일에 전념하거나 한 가지 기술을 전공하여 그 일에 정통하려고 하는 철저한 직업 정신을 말함.

간단한 사전적 의미에 추가로 필자가 생각하는 장인 정신의 의미를 적자면 자신이 만들어낸 결과물을 남이 보았을 때 부끄럽지 아니하고 시작부터 끝까지 자신의 철학을 유지하며 일관성 있게 작업하는 것이라 생각한다.

흔히 미디어에서 장인 정신에 대해서 언급되는 케이스를 보면 몇십 년을 갈고닦은 예술가나 나이 지극하신 분들이 하시는 일들에 대해서 조명되는 경우가 많다.

꼭 한가지 일에 대해서 몇십 년을 반복하고 특정 분야에 국한돼서만 장인이 존재하는 것은 아니다. 우리도 소프트웨어 개발 장인이 될 수 있으며 장인이 되어야만 한다. 그것이 우리들의 가치를 함께 끌어올리는 일이니까..

하지만 이상과 현실은 다르기 때문에 쉽지는 않다. 프로젝트를 시작하고 첫 개발을 시작했을 때의 마음가짐이 프로젝트 종료 시점까지 유지되기가 쉽지가 않다. 끊임없이 변경되는 기능과 쉴 새 없는 요구 사항 등을 대응하면서 코드를 일관성 있게 작성하고 클린 코드를 유지하려면 정말 내가 만드는 코드에 대한 지극한 애정이 담겨 있어야 하며 자신의 철학에 대한 믿음이 확고해야 한다.

코드 한 줄 한 줄 정성을 다해서 작성하고 함수 하나를 작성하더라도 여러 번 고쳤다가 다시 만들고 이러한 과정들이 코드에 대한 애정이 없다면, 그리고 클린 코드를 지향하지 않는다면 개발자에겐 엄청난 피로감으로 다가올 것이다.

필자는 작성하는 코드에 항상 애정을 갖는다. 그래서 항상 피곤하다. 기능 구현을 다하고 테스트 코드도 패스했고 버그도 발견되지 않는 코드여도 항상 코드를 볼 때마다 마음에 들지 않는 부분이 계속 생기고 끊임없이 개선해 나간다. 사소한 것부터 해서 다양하다. 아래는 필자가 주로 개발을 하면서 개선 포인트로 잡는 것들이다.

  • 변수나 함수 네이밍이 마음에 들지 않음
  • 코드의 흐름이 자연스럽게 이어지지 않음
  • 함수 하나에서 한가지 이상의 작업을 함
  • 클래스가 하는 일이 점점 많아짐
  • 중복 발견
  • 일정한 패턴으로 작성되는 코드들 중 일관성이 없는 코드 발견
  • 함수 간의 줄 간격이 다르게 설정됨
  • 불필요한 변수 할당이 이루어짐
  • 불필요한 상태 값을 가짐
  • 파라미터가 2개 이상인 함수 발견
  • 의미를 다른 사람이 쉽게 이해하기 힘든 조건문

일단 생각나는 것들 위주로 작성해봤다. 위에 나열된 것들 이외에도 다양한 이유로 코드를 개선하는 작업을 하곤 했었다. 흔히 리팩토링과 성능 튜닝에 대해서 이야기를 할 때 보통 첫 번째로 하지 말라고, 두 번째로 하지 말라고, 세 번째도 하지 말라 한다.

그 말도 일리는 있다. 이미 개발이 완료되어 출시되었거나 서비스 중일 경우 기존 시스템의 성능을 개선하고자 튜닝했을 때 발생할 사이드 이펙트를 예측 불가능한 경우가 많다. 또한 리팩토링도 마찬가지로 테스트 코드 없이 이루어지는 리팩토링은 죄악이라고 볼 수 있다. 뭐든지 한 번에 몰아서 하면 부작용이 있을 수밖에 없다.

그래서 리팩토링이나 성능 튜닝은 처음 개발 시작할 때부터 하는 것이다. 클래스 하나를 만들더라도 계속해서 코드를 개선하고 성능 튜닝 포인트는 없는지 고민하고 성능 테스트를 수시로 하고 이러한 과정을 제품을 출시하기까지 반복해야 한다. 이러한 개발 방식이 몸과 마음을 지치게 할 수도 있다. 하지만 위와 같이 하면 전체 소스 코드의 일관성을 유지할 수 있고 뒤늦게 성능을 개선하고자 구조를 뒤엎을 필요도 없다. 이렇게 했을 경우 반복되는 요구 사항 및 변경사항에 큰 수정사항 없이 능숙하게 대처하고 있는 자신을 발견 할 수 있을 것이다. 코드를 일관성 있게 작성한다는 것은 그만큼 자신이 작성한 코드의 설계에 대해서 잘 이해하고 있다는 방증이기도 하다.

필자의 경우 함수 이름을 10번도 넘게 변경한 적이 있다. 뭐 흔하다. 이름 짓는 게 가장 어렵고 중요한 것 같다. 함수나 변수나 클래스 이름이나 이름 하나만 잘 지어도 가독성 문제의 대부분은 해결할 것이다.

필자는 클린 코드 신봉자다. 아름답게 작성된 코드를 보는 것만큼 개발자로서 즐거운 것 또한 없는 것 같다. 코드가 계속해서 개선되고 있는 것을 보면 어느새 희열을 느끼고 있는 내 자신을 느낄 때가 있다. 필자가 클린 코드에 관심을 가지게 해준 책이 두 권 있다. 바로 로버트 C. 마틴의 Clean Code 와 마틴 파울러의 Refactoring이다. 두 책을 정독하고 책에 있는 내용들이 자신이 그동안 해왔던 방식이고 준수하려고 했던 것들이라면 자신이 그래도 좋은 코드를 만들기 위해 노력했다고 해도 될 것 같다. 물론 그렇지 않더라도 실망할 필요는 없다. 이제부터라도 좋은 코드를 만들기 위해 노력하면 되니까.

Clean Code 책에 있는 내용 중에 냄새나는 코드와 발견법이라는 파트가 있다. 해당 파트에 있는 내용을 공유하면 이 글을 읽는 분들에게 도움이 될 것 같아서 필자가 중요하게 생각하는 몇 가지를 정리하고 사견을 추가하였다.

주석

쓸모없는 주석

오래된 주석, 엉뚱한 주석, 잘못된 주석은 더 이상 쓸모가 없다. 주석이 들어간다는 것은 해당 코드가 코드만으로 설명이 불충분하여 핑계를 대고자 주석이라는 도구로 설명을 추가하는 것이다. 쓸모없는 주석은 일단 들어가고 나면 코드에서 쉽게 멀어지며 코드와 무관하게 혼자서 따로 놀며 코드를 오도하게 만든다. 물론 주석이 필요한 부분도 있다. 예를 들면 공개 API를 작성하고 API 사용법을 설명하기 위한 주석이나 코드만으로 설명하기 어려운 경우에 부연 설명을 달기 위해 작성하는 주석은 괜찮다고 생각한다.

중복된 주석

이미 코드만으로도 충분히 설명이 되는데 굳이 구구절절 설명하는 주석은 중복이다. 다음 예를 살펴보자.

첫 줄을 살펴보면 i++이라는 코드를 i를 증가시킨다는 것을 모르는 개발자는 없을 것이다. 위 코드는 아주 극단적인 예지만 저와 비슷한 케이스로 쓸데없이 설명을 추가하는 주석을 흔치 않게 발견하곤 한다. 또 다른 예로 함수의 시그니처만 달랑 기술하는 주석이다. 위와 같은 주석들은 중복이며 필요 없다.

성의 없는 주석

작성할 가치가 있는 주석은 잘 작성할 가치도 있다. 주석을 달 참이라면 시간을 들여서 최대한 멋지게 작성한다. 단어를 신중하게 선택한다. 문법과 구두점을 올바로 사용한다. 주절대지 않는다. 당연한 소리를 반복하지 않는다. 간결하고 명료하게 작성한다.

주석 처리된 코드

코드를 읽다가 주석 처리된 코드를 발견하면 일단 멈칫하게 된다. 연속해서 줄줄이 주석 처리된 코드를 발견하다 보면 코드를 보는 사람은 혼란스럽게 된다. 아마 보통은 그렇게 코드를 주석 처리하는 게 나중에 다시 쓸 코드이거나 임시로 처리하는 경우가 대부분이다. 이러한 방식은 구시대의 산물이다. 코드를 주석 처리하지 말고 과감하게 삭제하자. 요즘은 버전 관리 시스템에서 다 기억해준다. 혹시 나중에 필요하면 그때 기록을 찾아서 다시 코드를 복원시키면 된다. 불필요한 혼란을 만들지 말자.

함수

너무 많은 인수

함수에서 인수 개수는 작을수록 좋다. 아예 없으면 가장 좋고 하나, 둘, 셋 차례로 좋다. 만약 함수로 전달해야 될 인수가 많다면 별도의 자료 객체를 작성해서 객체 하나로 전달하는 것을 추천한다. 함수 인수가 많다는 것은 그 함수가 해야 될 일이 많다는 것을 의미하고 함수를 호출하는 쪽에서 신경 써야 할 부분이 많다는 것이다. 예를 들면 함수의 인자의 유형이 동일한 타입이라면? 함수를 호출하는 시점에서 인자의 순서를 신경 써야 하거나 하는 등의 버그 발생 요지가 늘어난다. Java에서 함수 인자를 여러 개 선언하는 경우 발생하던 문제들을 보완하기 위해 Kotlin에서는 Named parameter라는 기능을 지원한다. 물론 Kotlin에서 Named parameter를 사용함으로써 함수 인자를 여러 개 전달해도 된다는 소리는 아니다. 함수 인자는 적을수록 좋고 더 간결하고 명확하다.

플래그 인수

함수의 인수로 boolean 타입을 전달하는 경우가 있다. 이는 해당 함수가 최소 2가지 이상의 기능을 수행한다는 명백한 증거다. 함수에 boolean 타입을 전달하는 것보다는 2개의 함수를 선언하여 별도로 호출하는 것이 좋다. 아래는 필자가 참여했던 프로젝트에서 발견했던 코드이다.

showProgress라는 좋은 함수 이름을 작성해 놓고 인자로 boolean 값을 전달한다. 그럼 호출하는 쪽에서 showProgress(true), showProgress(false) 이런 식으로 호출한다. 함수 바디를 살펴보지 않고 해당 함수에 전달하는 인자가 무슨 역할을 하는지 예측할 수 있겠는가? 이러한 코드는 호출하는 측의 코드를 볼 때마다 헷갈리게 하고 해당 함수의 바디를 다시 살펴보는 등 계속해서 혼란을 주게 만든다. 위 코드보다는 showProgress(), hideProgress() 2개의 함수를 선언하여 각각 호출하는 것이 더 좋다.

죽은 함수

아무도 호출하지 않는 함수는 삭제한다. 죽은 코드는 낭비다. 과감히 삭제해도 된다. 버전 관리 시스템에서 다 기억해주니까. Clean Code라는 게 별거 없다. 항상 코드를 깔끔하게 유지해주는 것이다. 혹시 나중에 다시 사용할 수 있으니 남겨둬야겠다는 생각은 안하는 게 좋다.

명령과 조회를 분리하고 부수 효과를 일으키지 마라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안 된다. 개체 상태를 변경하거나 아니면 개체 정보를 반환하거나 둘 중 하나다. 둘 다 하면 혼란을 초래한다. 함수가 호출하는 측에서 예상하는 행동과 일치하지 않는 무언가 부수적인 효과를 발생시키면 안 된다. 부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고서는 남몰래 다른 짓도 하면서 호출자를 속이는 행동이다. 다음은 Clean Code 책에서 가져온 코드이다.

호출자는 userNamepassword를 인수로 전달하면 password가 올바른지 여부만 판단할 것이라고 예측할 것이다. 하지만 checkPassword 함수는 몰래 다른 짓도 한다. 바로 Session.initialize() 함수 호출이다. checkPassword 함수 이름만 봐서는 세션을 초기화한다는 사실을 예측할 수 없다. 이 부수 효과가 일시적인 결합을 초래한다. 즉 checkPassword 함수는 특정 상황에서만 호출이 가능하다. 다시 말해서, 세션을 초기화해도 괜찮은 경우에만 호출이 가능하다. 자칫 잘못 호출하면 의도하지 않게 세션 정보가 날아간다. 이렇듯 부수 효과로 숨겨진 경우에는 더더욱 혼란이 커진다.

일반

당연한 동작을 구현하지 않는다.

최소 놀람의 원칙(Principle of least astnishment)에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.

예를 들어, 요일 문자열을 요일을 나타내는 enum으로 변환하는 함수를 살펴보자.

Day day = DayDate.StringToDay(String dayName);

우리는 함수가 “Monday”를 Day.MONDAY로 변환하리라 기대한다. 또한 일반적으로 쓰는 요일 약어도 올바로 변환하리라 기대한다. 대소문자는 당연히 구분하지 않으리라 기대한다. 당연한 동작을 구현하지 않으면 코드 독자와 코드 사용자가 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵다. 저자를 신뢰하지 못하므로 코드를 일일이 살펴야 한다.

디미터의 법칙

디미터의 법칙은 잘 알려진 발견법으로, 모듈은 자신이 조작하는 객체의 속 사정을 몰라야 한다는 법칙이다. 객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 의미다. 그러면 내부 구조를 노출하는 셈이니까. 디미터의 법칙이 주장하는 바는 다음과 같다. 클래스 C의 메소드 f가 있을 때 다음 객체의 메소드만 호출해야 한다.

  • 클래스 C
  • f가 생성한 객체
  • f 인수로 넘어온 객체
  • C 인스턴스 변수에 저장된 객체

하지만 위 객체에서 허용된 메소드가 반환하는 객체의 메소드는 호출하면 안 된다. 다시 말해, 낯선 사람은 경계하고 친구하고만 놀라는 의미다.

경계를 올바로 처리하지 않는다.

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 우수한 소프트웨어 설계는 변경을 위해 많은 투자와 재작업이 필요하지 않다. 엄청난 시간과 노력과 재작업을 요구하지 않는다. 경계에 위치하는 코드는 깔끔하게 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 낫다. 자칫하면 오히려 외부 코드에 휘둘리고 만다. 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. 새로운 클래스로 경계를 감싸거나 아니면 어댑터 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지면, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.

중복

우수한 설계에서 중복은 커다란 적이다. 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻하기 때문이다. 중복의 종류는 여러 가지 형태로 표출된다. 똑같은 코드는 당연히 중복이며 구현의 중복도 중복의 한 형태다. 중복 제거는 소프트웨어 설계의 핵심 규칙 중 가장 중요한 규칙 중의 하나이다. 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하고 기뻐해야 한다. 중복되는 코드들을 추상화로 정리하면 구현이 빨라지고 오류가 적어진다. 가장 뻔한 유형은 똑같은 코드가 여러 차례 나오는 중복이다. 여기저기서 마구 긁어다가 복사한듯한 코드다. 흔히 자주 발견할 수 있는 중복 중 하나가 switch/caseif/else 문으로 똑같은 조건을 거듭 확인하는 코드다. 이러한 중복은 다형성을 적용하는 것을 추천한다. 동일한 루틴에 대해 switch/case 문 등은 단 한 번만 허용하는 게 원칙에 맞는다고 본다. 중복은 발견하는 즉시 제거하도록 하자. 하나둘 중복되는 코드가 늘어갈 수록 코드 변경에 따른 코드의 유지 보수 범위가 상상할 수 없이 늘어간다. 단 한 줄만 변경해도 되는 요구 사항인데 무분별한 중복 코드로 인해 코드를 수정하는 작업이 그저 불안한 작업처럼 느껴지고 요구 사항에 대해 점점 방어적인 태도로 변하게 될 것이다.

기본 클래스가 파생 클래스에 의존한다

개념을 기본 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기본 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다. 그러므로 기본 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 소리다. 일반적으로 기본 클래스는 파생 클래스를 아예 몰라야 마땅하다.

과도한 정보

잘 정의된 모듈은 인터페이스가 아주 적다. 하지만 작은 인터페이스로도 많은 동작이 가능하다. 부실하게 정의된 모듈은 인터페이스가 구질구질하다. 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다. 반면 부실하게 정의된 인터페이스는 온갖 함수를 제공한다. 그래서 결합도가 높다. 클래스가 제공하는 메소드 수는 적을수록 좋다. 함수가 아는 변수 수도 작을수록 좋다. 클래스에 들어 있는 변수 수도 작을수록 좋다. 자료를 숨기고 메소드나 인스턴스 변수가 넘쳐나는 클래스를 피한다. 하위 클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 않고 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어야 한다.

죽은 코드

죽은 코드란 실행되지 않는 코드를 가리킨다. 앞서 주석 처리된 코드와 마찬가지로 발견 즉시 제거하는 게 좋다. 요즘 IDE 들은 기본적으로 사용되지 않는 코드들이나 실행되지 않는 코드 블록들을 알아서 분석하여 친절하게 개발자에게 알려준다. 친절하게 알려주는데도 그대로 방치하는 것은 매우 게으른 행동이다.

일관성 부족

어떤 개념을 한 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다. 위에서 언급했던 “최소 놀람의 원칙”에도 부합한다. 코드 가독성 측면이나 유지 보수 측면에서 바라봤을 때 설계가 유연하지 않더라도 코드들의 일관성이 유지가 된다면 저자의 설계 원칙에 대해서 이해할 수 있고 코드를 읽고 수정하기가 대단히 쉬워진다. 일관성을 유지하기 위한 한 사례 중 하나가 바로 팀원들 간에 코딩 컨벤션을 통일하는 것이다.

잡동사니

죽은 코드와 마찬가지로 볼 수 있다. 비어 있는 생성자가 왜 필요하며 아무도 사용하지 않는 변수, 아무도 호출하지 않는 함수 등은 코드만 복잡하게 만들 뿐이고 쓸데없이 코드량만 증가시킨다. 요즘은 IDE 에서 알아서 힌트를 제공해준다. 소스 파일은 언제나 항상 깔끔하게 유지하도록 정리하는게 좋다.

인위적 결합

서로 무관함 개념을 인위적으로 결합하지 않는다. 인위적 결합 케이스는 코드 작성 시 별다른 고민도 하지 않고 그때그때 편한 대로 코딩하는 습관에 의해 발생한다. 예를 들어 범용 static 함수가 특정 클래스에 속할 이유가 없다. 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편하다 보니 잘못된 위치에 넣어버리게 되는 것이다. 게으르고 부주의한 행동이다. 그저 당장 편한 곳에 선언하고 내버려 두지 말고 나중에라도 발견하게 되면 적절한 위치에 선언하여 무관한 개념이 뒤섞이지 않도록 하자.

부적절한 static 함수

Math.max(double a, double b)는 좋은 static 메소드다. 특정 인스턴스와 관련한 기능이 아니다. new Math().max(a, b)나 a.max(b)라고 하면 오히려 우습다. max 메소드가 사용하는 정보는 두 인수가 전부다. 메소드를 소유하는 객체에서 가져오는 정보가 아니다. 결정적으로 Math.max 메소드를 재정의할 가능성은 거의 아니 전혀 없다. static 함수로 선언하기로 결정할 때 고려해야 할 사항이 무엇일까? 일반적으로는 인스턴스 함수가 static 함수보다 낫다. static 함수로 정의해야 한다면 메소드를 소유하는 객체에서 가져오는 정보가 없고 해당 함수를 재정의할 가능성이 없는지 꼼꼼히 살펴봐야 한다. 예를 들어 static 함수를 소유하고 있는 클래스가 다형성을 적용하여 서로 다른 알고리즘을 수행하는 클래스로 분리가 되었을 때 static 함수는 인스턴스 함수가 되어야 맞다.

서술적 이름

서술적인 이름을 사용하는 것은 독자에게 코드의 의미를 조금 더 명확하게 전달한다. 아무런 의미 없이 대충 지어진 이름은 코드를 보는 독자에게 코드를 이해하는데 많은 시간을 투자하게 하지만 서술적인 이름을 사용한 변수는 금방 쉽게 읽힌다. 이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 낫다. 이름을 정하느라 시간을 들여도 괜찮다. 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해져서 코드를 개선하기 쉬워진다. 함수나 클래스 등에 이름을 지을 때 작명의 고통을 느끼게 되는 경우는 보통 크고 복잡하기 때문이다. 함수나 클래스를 작고 단순하게 만들수록 서술적인 이름을 고르기도 쉬워진다.

if/else 혹은 switch/case 문보다 다형성을 사용하라.

switch 문은 작게 만들기 어렵다. 이는 if/else 도 마찬가지다. 본질적으로 switch 문은 N 가지를 처리한다. switch 문을 완전히 피할 방법은 없다. 하지만 다형성을 이용하여 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법이 있다. switch 문은 단 한 번만 사용해야 한다. 다형성 객체를 생성하는 코드 안에서다. 보통 대다수 개발자가 switch 문을 사용하는 이유는 그 상황에서 가장 올바른 선택이라서가 아니라 당장 손쉬운 선택이기 때문이다. 그러므로 switch 또는 if/else를 선택하기 전에 다형성을 먼저 고려해야 한다. 물론 다형성이 만능은 아니다. 다형성을 이용해 객체화하기 부담스러운 switch 가 있을 수 있다. 예를 들면 특정 상수값만 비교하는 switch 문 같은 경우이다. 이런 경우 static 함수로 분리하여 중복되지 않도록 한 곳에서 관리하도록 하는 게 좋다.

null을 반환하거나 전달하지 마라

개발자들이 흔히 저질러서 오류를 유발하는 행위 중 하나가 null을 반환하는 습관이다. null을 반환한다는 것은 일부 특별한 케이스를 제외하곤 문제가 발생했을 경우 해당 문제를 함수 호출자에게 떠넘기는 것이다. 해당 함수를 호출하는 곳에서 어느 한 곳이라도 null을 빼먹는다면 오류가 발생한다. 만약 메소드에서 null을 반환하고픈 유혹이 생긴다면 대신 예외를 던지거나 enum 을 통해 특수한 객체를 반환하는 게 좋다. 메소드에서 null을 반환하는 방식도 나쁘지만 메소드로 null을 전달하는 방식은 더 나쁘다. 정상적인 인수로 null을 기대하는 함수는 아마 거의 없을 것이다. 최근의 프로그래밍 언어들은 이와 같은 null 문제를 해결하기 위해 다양한 방법을 제공한다. @NotNull, @Nullable 애노테이션을 사용한다거나 아예 컴파일 단계에서 nullable, not null 여부를 미리 지정하여 함수나 변수를 사용할 시에 코드 작성자가 조금 더 안전하게 처리할 수 있도록 강제한다.

매직 넘버는 명명된 상수로 교체하라

매직 넘버는 코드를 읽는 독자 입장에서 짜증을 유발하게 하는 요소다. 소프트웨어 개발에서 가장 오래된 규칙 중 하나라고 생각하는데 그동안 필자가 봐 왔던 코드 중에 매직 넘버가 판을 치는 코드들을 많이 봤다. 아래 코드는 기억을 더듬어 매직 넘버가 사용되었던 코드 일부를 작성해봤다.

매우 극단적인 사례라고 볼 수 있지만 위와 비슷한 코드들을 심심치 않게 발견할 수 있을 것이다. type이 1은 뭐고 2는 뭐고 갑자기 10은 왜 나오는지 각 type이 의미하는 것이 무엇인지 코드만 봐서 이해할 수 있다면 당신은 천재일 것이다. 위와 같은 코드는 단순히 코드 레벨뿐 아니라 문서도 살펴봐야 하고 위와 같은 코드가 다른 곳에는 없는지 살펴봐야 한다. 절대로 저렇게 의미 없는 매직 넘버는 사용하지 말자. 대신 상수로 정의하여 의미를 부여하도록 하자.

조건을 캡슐화하라

코드를 읽을 때 ifwhile 등에 들어가는 부울 논리는 기본적으로 이해하기 어렵다. 조건이 한 개 일 때는 그나마 낫지만 조건 여러 개가 And 또는 Or 형태로 작성되어 있다면 한참을 살펴봐야 한다. 만약 ifwhlie 등에 들어가는 조건이 여러 개라면 조건의 의도를 분명히 밝히는 함수로 표현하는 것이 낫다.

if에 조건이 여러 개 있는 것보다 각 조건들의 의도를 밝히는 함수를 만들어 그 함수를 사용하는 것이 더 낫다. 위의 예제 코드는 조건식이 그나마 간단하여 별도의 함수로 캡슐화 하지 않아도 그나마 읽히겠지만 복잡한 조건들을 마주하게 된다면 생각이 달라질 것이다.

관계보다 구조를 사용하라

설계 결정을 강제할 때는 규칙보다 관례를 사용한다. 명명 관례도 좋지만 구조적으로 강제하면 더 좋다. 예를 들어, enum 변수가 멋진 switch/case 문보다 추상 메소드가 있는 기본 클래스가 더 낫다. switch/case 문을 매번 똑같이 구현하라고 강제하기는 어렵지만, 파생 클래스는 추상 메소드를 모두 구현하지 않으면 안 되기 때문이다.

부정 조건은 피하라

기본적으로 부정 조건은 긍정 조건보다 이해하기 어렵다. if 문에 !이 들어간다면 일단 머릿속이 복잡해진다. 조건이 하나인 경우는 그나마 낫지만 두 개 이상인 조건문에 부정 조건이 들어가 있다면 해당 조건문을 한 번에 이해하기란 어렵다. 만약 부정 조건이 반드시 들어가야 한다면 해당 조건을 별도의 함수로 작성하여 긍정으로 바꾸고 조건식에 해당 함수호출을 추가하자. 코드가 조금 더 읽기 쉬워진다.

함수는 한 가지만 해야 한다

함수를 짜다 보면 한 함수 안에다 여러 단락을 이어서 일련의 작업을 수행하고픈 유혹에 빠진다. 함수가 여러 가지 작업을 수행한다면 그 말은 즉 함수에 좋은 이름을 붙이기 어렵다는 말과 같다. 함수가 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다. 함수가 ‘한 가지’만 하는지 판단하는 방법이 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 해당 함수의 코드에서 다른 함수로 추출할 수 있다면 그 함수는 여러 작업을 하고 있다는 뜻이다. 함수에 관해 좋은 격언이 있다.

함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만 해야 한다.

경계 조건을 캡슐화하라

경계 조건은 빼먹거나 놓치기 쉽상이다. 버그 발생 요지가 높고 버그가 발생해도 쉽게 찾기 힘든 경우가 생긴다. 경계 조건은 한 곳에서 별도로 처리한다. 코드 여리저기에서 처리하지 않는다. 다시 말해, 코드 여기저기에 +1이나 -1을 흩어놓지 않는다. 아래는 Clean Code 책에서 가져온 코드다.

위 코드를 보면 level + 1 하는 부분이 두 번 나온다. 이런 경계 조건은 변수로 캡슐화하는 편이 낫다. 위와 같이 경계 조건이 중복되는 코드들은 버그가 발생할 요지가 높다.

마치며

지금까지 클린 코드에 대해서 살펴보았다. 필자도 위에 기술된 내용을 완벽히 수행하는 것은 아니다. 아직 부족한 점이 많고 익숙해져야 할 부분도 많지만 클린 코드를 지향함으로써 얻은 많은 이점들이 있었다. 코드를 다른 이에게 넘길 때 부끄러워하지 않아도 되었고 잦은 요구 사항에도 유연하게 대처할 수 있었으며 개발한 소프트웨어가 안정적으로 운영될 수 있었다. 특히 수정사항이 발생했을 때 코드의 수정 범위에 대해 코드를 열심히 살펴보지 않아도 머릿속으로 금방 파악이 되었기 때문에 일정 산정에 무리도 없었으며 거의 특별한 일이 발생하지 않는 한 거의 야근을 한 적이 없었다. 개발 관련 서적을 많이 보유하고 있지만 필자가 가장 아끼는 책 중 하나가 Clean Code 와 Refactoring이다. 안 본지 오래되었고 다시 기억을 더듬고 귀차니즘에 의해 일부 대충 개발했던 시간들을 반성하고자 다시 한번 살펴보고 정리하게 되었다. 클린 코드를 지향하며 장인 정신을 갖고 코드를 작성하다 보면 조금 더 나은 소프트웨어를 만들 수 있으리라고 믿는다.

--

--

백중원 (Leopold)

스타트업에서 ‘트리플’ 이라는 여행 서비스를 개발하고 있습니다. 디지털 노마드와 조기 은퇴를 꿈꾸는 평범한 개발자입니다.