티스토리 뷰

참고사항

  • 이 책의 저자는 객체지향 기본 설계 원칙(SOLID)를 정의한 사람이다.
  • "이 책에서의 교훈이 절대적인 진리"다. 라고 저자는 말한다. (오브젝트 진영에서)

1장. 깨끗한 코드

프로그래밍이란, 기계가 실행할 정도로 상세하게 요구사항을 명시하는 작업이다. 따라서 코드를 자동으로 생성하는 시대가 다가오면서 프로그래머가 필요 없어질 것이란 말은 헛소리이다. 요구사항을 애매모하게 줘도 우리의 의도를 정확히 꽤뚫어 프로그램을 완벽하게 실행하는 기계는 불가능하다. 궁극적으로 코드는 요구사항을 표현하는 언어이다.

 

나쁜 코드를 짜는 이유는 상사의 기한에 대한 쪼임, 업무에 대한 지겨움, 업무가 밀려서 등등 다양한 이유가 있다. 하지만 이러한 코드로 인해 얽히고 설킨 코드를 '해독'해서 코드를 더해가야 하며 이는 장기적으로 팀의 생산성으로 0으로 수렴시키는 결과를 낳는다.

 

상사(관리자)가 일정을 쪼아가며 요구사항을 들이밀더라도 대다수의 관리자들 또한 좋은 코드를 원한다. 단지 그들이 관리자로서 책임이기 때문에 그러한 역할을 하는 것이다. 비유로 의사에게 빠른 수술을 위해 손을 씻지말아달라고 해도 의사는 그런 요구를 거절한다. 무책임하게 요구사항을 그대로 따르는 행위는 범죄이며 전문가 답지 못한 자세이다.
프로그래머도 마찬가지이다. 나쁜 코드의 위험을 인지한채 관리자의 말을 그대로 따르는 행동은 전문가 답지 못하다.

 

결국 가장 빠르게 코드를 작성할 수 있는 방법은 깨끗한 코드를 작성하는 방법이다. 우리는 깨끗한 코드를 보면 깨끗한 코드인지 알 수 있다.

생각해보자. 우리는 좋은 노래, 좋은 그림, 좋은 차를 보면 바로 좋음을 알아차린다. 하지만 그렇다고 우리가 그것을 만들 수 있나? 그렇지 않다. 우리는 깨끗한 코드를 작성할 수 있는 "코드 감각" 을 길러야 한다.

 

깨끗한 코드란 무엇인가

  • 우아한 코드
    • 보기에 즐거운 코드. 잘만든 오르골이나 자동차를 볼 때처럼 보는 사람에게 즐거움을 선사하는 코드.
  • 효율적인 코드
  • 유혹에 빠지지 않은 코드
    • 깨진 유리창 이론처럼 나쁜 코드는 방치되며 나쁜 코드를 부른다.
  • 철저한 오류 처리
  • 깨끗한 코드는 한가지 역할에 '집중'한다.
  • 가독성이 좋은 코드
  • 다른사람이 고치기 쉬운 코드
  • 테스트 케이스가 있는 코드
  • 주의 깊게 작성한 코드. 세세한 사항까지 꼼꼼하게 신경쓴 코드.
  • 중복을 피하라. 한 기능만 수행해라. 제대로 표현해라. 작게 추상화 하라.
  • 코드를 읽으면서 짐작했던 기능을 그대로 수행한다면 깨끗한 코드

2장. 의미 있는 이름

의도를 분명히 밝혀라

int d; // 경과 시간 (날짜)

위와 같은 코드는 아무런 의미가 없다.

int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

이러한 의도가 드러난 이름을 사용하면 코드이해와 변경이 쉬워진다.

 

if (x[0] == 4) ...

상수의 사용에서도 4라는 숫자는 아무 의미가 없지만

if (cell[STATUS_VALUE] == FLAGGED) ...

라는 방식으로 `숫자 4`에 의미를 부여해줄 수 있으며

if (cell.isFlaaged()) ...

Cell 클래스를 사용하여 한단계 더 추상화하여 `FLAGGED`를 감출 수 있다.

그릇된 정보를 피하라

  • 모호한 약어 사용을 피하자.
  • 흡사한 이름을 사용하지 않도록 주의하자.
    • XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStorageOfStrings 이라는 이름을 사용한다면 쉽게 구별이 되겠는가. 두 단어는 겁나 비슷하다.

의미 있게 구분하라

fun copyChars(a1: char[], a2: char[]) ...

이런 코드는 내부를 보기 전까지 a1과 a2의 역할을 알수가 없다.

fun copyChars(source: char[], destination: char[]) ...

이렇게 인수 이름을 작성한다면 더욱 읽기 쉬운 코드가 된다.

 

변수에 쓸데없이 붙어있는 용어도 아무런 의미를 제공해주지 못한다.

Product라는 클래스가 존재할 때, ProductDataProductInfo, aProducttheProduct 두 클래스나 변수에 차이를 알수는 없다. 사용 자체를 금하자는게 아니라 의미를 두고 사용하자는 것이다. 예를들어 a-는 모든 지역변수에만, the-는 함수 인수에만 사용하는 등의 규칙을 두면 사용해도 되겠다.

 

name 변수와 nameString 변수의 차이가 있는가? nameString이 name보다 나은게 뭐가 있는가. name이 부동소수라도 될 가능성이라도 있는가? 즉, 의미를 부여해줄 수 없는 불용어는 피해야한다.

money - moneyAmount, customerInfo - customer, account - accountData, theMessage - message

이런 변수들은 서로 구분이 되지 않는다. 읽는 사람이 차이를 알수 있도록 이름을 지어라!

발음하기 쉬운 이름을 지어라

val genymdhms: Date // generate year, month, dya, hour, minute, second

읽을수도 없는 (쥐 엔 와이 엠 디 에이치 엠 에스 or 젠 야 무다 힘스) 괴랄한 변수로 작성하지 말라.

val generationTimeStamp: Date

이렇게 발음하기 쉬운 변수를 사용하자.

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

문자 하나를 사용하는 이름과 상수는 코드에서 눈에 잘 띄지 않을 뿐더러 검색하기도 여간 어려운게 아니다.

val totalClassCount = studentCount / 7

여기서 7이 의미하는지 알기는 매우 어렵다.

val totalClassCount = studentCount / MAX_STUDENT_PER_CLASS

이런 코드가 이해도 쉽고 찾기도 쉽다.

헝가리식 표기법 지양

val phoneString: PhoneNumber

이처럼 클래스에 타입을 넣어뒀다가 (PhoneString -> PhoneNumber) 변경되어도 변수 이름(phoneString)은 바뀌지 않는 불상사가 생길 수 있다.

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

전문가 프로그래머는 명료함이 최고라는 사실을 이해한다.

클래스 이름

클래스 이름과 객체 이름은 명사/명사구가 적합하다. Customer, WikiPage, Account, AddressParser 등이 좋은 예이다.

Manager, Processor, Data, Info 등과 같은 모호한 단어는 피하고, 동사는 사용하지 않는다.

메서드 이름

메서드 이름은 동사/동사구가 적합하다. postPayment, deletePage, save 등이 좋은 예이다. 접근자, 변경자, 조건자는 javabean 표준에 따라 get-, set-, is-를 붙이자.

 

생성자를 중복 정의할 때는 정적 팩토리 메서드를 활용하자. 메서드는 인수를 설명하는 이름을 사용한다.

val point: Complex = Complex.FromRealNumber(23.0)

위 코드가 아래 코드보다 좋다.

val point: Complex = Complex(23.0)

기발한 이름은 피하라

재밌는 유머감각을 뽐내는 이름이 아닌 명료한 이름을 작성해라.

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

추상적인 개념 하나에 단어하나를 선택하고 이를 고수하라. 메서드 이름은 독자적이고 일관적이어야 한다.

동일 코드 기반에 controller, manager, driver를 섞어서 쓰면 곤란하다. DeviceManagerProtocolController는 어떻게 다른가? 둘다 Controller or Manager or Driver가 될수 있는거 아닌가?

하나를 정했으면 일관성있게 사용해야 할 것이다.

말장난 하지 마라

한 단어를 두가지 목적으로 사용하지 마라. 

지금까지 add-메서드를 기존 값 두개를 더하거나 이어붙여서 새로운 값을 만드는데 사용했다고 하자. 이때 목록 집합에 값 하나를 추가하는 것도 add-로 작성해야할까? 이는 insert 또는 append가 더 적합할 것이다.

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

모든 이름을 문제의 영역에서 가져오는 것은 현명하지 못하다. 읽는 사람도 프로그래머임을 인지하자. jobQueue변수를 보고 모르는 프로그래머는 없을 것이다. 기술 개념에는 기술 이름이 가장 적합하다.

문제 영역에서 가져온 이름을 사용하라

적절한 '프로그래머 용어'가 없다면 문제 영역에서 이름을 가져오자. 이후 코드를 보수하는 프로그래머는 분야 전문가에게 (기획자 등) 의미를 물어 파악할 수 있을것이다.

프로그래머라면 '해법 영역'과 '문제 영역'을 구분할 줄 알아야 한다.

의미 있는 맥락을 추가하라

변수들이 street, houseNumber, city, state, zipcode라면 이게 주소를 나타내는 변수들인지 바로 알 수 있을 것이다. 하지만 그냥 state만 사용한다면? 해당 변수가 바로 주소의 일부라는 것을 알기 쉽지 않다.

접두어를 붙여서 사용하거나 Address 클래스를 이용해 사용하는 것이 변수의 의미가 분명해 진다.

불필요한 맥락을 없애라

고급 휘발유 충전소(Advanced Gasoline Stations) 애플리케이션을 만들면서 메일 주소를 관리하는 MailingAddress를 추가하면서 고객 계정 주소는 AGSAccountAddress로 이름을 지었다고 보자. 이때 다른 고객 관리 프로그램이 추가되면 AGSAccountAddress를 사용할 수 있을까? 예를들어 저가 휘발유 충전소가 추가되면? 이름이 부적절하거나 중복이 된다.

 

accountAddress, customerAddress 같은 것도 인스턴스 변수명으로는 적합하더라도 클래스 명으로는 적합하지 않다. 이런 경우 클래스이름으로 Address가 적합하다.

 

포트주소, MAC주소, URI주소를 구분해야한다면 PortAddress, MAC, URI 라는 이름이 적합하겠다. 이렇게 의미가 분명해지게 작성하자.


3장. 함수

하나의 함수에 추상화 수준이 다양하고 너무 길다면 이해하기 너무 어렵다. 두겹 이상의 중첩된 if 문과 플래그(True/False)를 확인하는 코드는 쓰레기다.

작게 만들어라

함수를 만드는 첫번째 규칙. 작게!

함수를 만드는 두번째 규칙. 더 작게!

근거는 제시하기 어렵지만 경험과 시행착오를 바탕으로 작은 함수가 좋다고 확신한다.

블록과 들여쓰기

if - else , while 문에 들어가는 블록은 한줄이어야 한다. 즉, 대게 함수를 호출해야 한다.

if (testPage(pageData))
	includeSetUpAndTeardownPages(PageData, isSuite)
return pageData.getHtml()

이렇게 하므로서 바깥을 감싸는 함수도 작아지고, 블록 내 호출하는 함수의 이름만 잘지으면 코드를 이해하기도 쉬워진다. 또한 이말은 중첩구조가 생길정도로 함수가 길어져서는 안된다는 뜻이다.

함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다. 즉 한번의 if 문이 있다면 2단으므로 중첩은 NO! NO!

한가지만 해라

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

 

이때 "한 가지"가 의미하는 게 뭘까?

-> 지정된 함수 이름 아래에서 추상화 수준이 하나라면 하나이다.

 

단순히 다른 이름으로서의 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

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

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

val html = getHtml()
val pagePathName: String = PathParser.render(pagepath)
xxx.append("\n)

위 3줄 코드들은 전부다 다른 추상화 수준을 갖고 있다. 맨 윗줄 부터 가장 추상화가 높고, 중간, 낮은 순이다.

 

한 함수 내에 추상화 수준이 섞이면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려워지기 때문이다. 또한 깨진 창문마냥 추후 수정자가 세부사항을 더 추가하면서 추상화 수준이 더욱 깨지게된다.

내려가기 규칙

한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

이렇게 작성 시, 프로그램을 위에서 아래로 읽으면 추상화 수준이 한 번에 한 단계 씩 내려가게 된다. 이것을 내려가기 규칙이라고 한다. 

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

Switch 문

switch 문은 작게 만들기 어렵다. 본질적으로 N가지의 분기를 처리한다. 이를 완전히 피할 방법은 없지만 저차원 클래스에 숨기고 절대 반복하지 않도록 하는 방법은 있다. 이때 다형성 (polymorphism)을 활용한다.

 

다형적 객체를 생성하는 코드에서만 단 한 번만 switch문을 사용하고 그 외에는 절대 노출하지 않고 사용하지 않는다.

abstract class Employee {
    abstract isPayday(): Boolean
    abstract calculatePay: Money
    abstract deliveryPay(money: Money)
}
---
interface EmployeeFactory {
    fun makeEmployee(r: EmployeeRecord)
}
---
class EmployeeeFactoryImpl : EmployeeFactory {
    fun makeEmployee(r: EmployeeRecord) {
        when (r.type) {
            COMMISSIONED -> CommissiendEmployee(r)
            HOURLY -> ...
            else -> ...
        }
    }
}
---
class CommissiendEmployee : Employee {
	// override
}

이렇게 다형성을 활용하면 직급이 다른 Employee들에 대해 caculatePay(), assignHoliday() 등 직급에 따라 다른 로직을 필요로 하는 함수들을 수행 시 switch 없이 override로 처리할 수 있다.

서술적인 이름을 사용하라

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.

작은 함수에 좋은 이름을 붙인다면 이미 절반은 성공이다. 또한 함수가 작고 단순할 수록 서술적인 이름을 고르기도 쉬워진다.

 

이름을 붙일 때는 일관성이 있어야 한다. 모둘 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

fun includeSetupAndTeardownPages(), fun includeSetupPages(), fun includeSetupPage(), ...

그다음 어떤 함수를 어떤 이름으로 작성할 것인지 바로 예상되지 않나? 이렇게 문체가 비슷하면 이야기를 순차적으로 풀기도 쉬워진다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개(무항)이다.
  • 그다음은 1개(단항)이다.
  • 다음은 2개(이항)이다
  • 3개(삼항)은 가능한 피해야 한다.
  • 4개는 특별한 이유가 필요하다, 하지만 특별한 이유가 있더라도 사용하면 안된다.
단항 함수

단항은 대부분 2가지 목적이다.

  1. 인수에 질문을 던지는 경우
  2. 인수를 뭔가로 변환하여 결과를 반환하는 경우

함수 이름을 지을 때 두 경우를 분명히 구분하자. 또한 일반적으로 두 형식을 사용한다.

 

드물게 있는 단항 함수는 이벤트 함수이다. 입력 인수만 있고 출력 인수는 없다. 함수 호출을 이벤트로 해석하여 입력 인수로 시스템 상태를 변경한다. `passwordAttemptFailedNTimes(attempts: Int): Unit` 가 좋은 예이다. 이때 이벤트라는 사실이 코드에 명확히 나타나야 한다.

 

이 외 단항 함수는 최대한 지양하자. 입력 인수를 변환했다면 변환 결과는 반환값으로 돌려주도록 하자.

플래그 인수

플래그 인수는 추하다. 함수로 boolean값을 넘기는 행위는 정말 끔찍하다. 대놓고 여러 역할을 하겠다고 선언하는 행위이기 때문이다. (SRP 위반!)

이항 함수

인수가 2개인 함수가 1개인 함수보다 이해하기 어렵다.

assertEquals(expected, actual)

이 함수를 사용하면서 expected에 actual을 넣는 실수를 얼마나 자주 했는가. 우리는 항상 expected가 첫 번째 인수임을 외우고 인지해서 사용해야 한다.

삼항 함수

인수가 3개인 함수는 2개인 함수보다 훨씬 더 이해하기 어렵다.

순서, 주춤(행동), 무시로 야기되는 문제가 2배 이상 늘어난다.

assertEquals(message, expected, actual)

이 함수에서 첫번째가 expected가 아닌지 예상하지는 않았는지? 멈칫, 주춤 하지 않았는가? 이 함수를 쓸 때마다 그래야 한다는 건 음험하다.

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수이름이 필수이다.

 

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를 들면

fun write(name)

좀 더 나은 이름은

fun writeField(name)

이러면 namefield라는 사실이 분명하게 나타난다.

 

함수에 키워드를 추가하는 형식으로 좋은 함수 이름을 작성할 수 있다.

assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 이를 통해 굳이 인수 순서를 외우지 않아도 된다.

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

부수 효과는 예상치 못하게 클래스 변수를 수정하거나 남몰래 다른 행위를 한다.

이러한 것들은 시간적인 결합(temporal coupling) 또는 순서 종속성(order dependency)를 초래한다.

 

userName과 password를 확인해서 올바르면 true 아니면 false를 반환하는 함수를 봐보자.

class UserValidator {
    fun checkPassword(usrname: String, password: String): Boolean {
        val user: User = UserGateway.findByName(username)
        if (user != User.NULL) {
            // 검증 코드
            if (검증됨) {
                Session.initialize() // <- 부수효과 !
                return true
            }
        }
        return false
    }
}

Session.initialize()는 부수효과이다. 이로인해 시간적인 결합을 초래한다. Session.initialize()로 인해 checkPassword가 단순 패스워드를 확인하는 함수가 아니라 세션을 초기화해도 괜찮은 경우에만 호출이 가능해지는 시간적인 결합이 발생하는 것이다.

명령과 조회를 분리하라!

객체 상태를 변경하거나 or 객체 정보를 반환하거나. 둘 중 하나다.

 

이름을 바꾸고 성공 시 true를 반환하는 함수 `set(attribute: String, value: String): boolean`을 봐보자. 이런 함수는 아래 같은 괴상한 코드를 만든다.

if (set("username", "구름")) ...

이 코드를 보고 set이 머하는 건지 알 수가 없다. 애초에 명령과 조회를 분리해야 혼란을 피할 수 있다.

if (attributeExistes("username")) {
    setAttribute("username", "구름")
}

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

오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.

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

 

try-catch 블록 뽑아내기

try/catch 블록은 추하다. 코드 구조에 혼란을 일으키고, 정상 동작과 오류 처리 동작이 뒤섞인다.

따라서 try/catch 블록을 별도 함수로 뽑아내는게 좋다.

fun delete(page: Page) {
    try {
    	deletePageAndAllRefrences(page)
    } catch (e: Exception) {
    	logError(e)
    }
}

이렇게 delete가 모든 오류를 처리하게 한다. 이렇게 하므로서 코드를 이해하기 쉬워진다.

실제 페이지 제거를 수행하는 `deletePageAndAllRefrences` 함수는 예외를 처리하지 않는다. 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드의 이해와 수정이 쉬워진다.

반복하지 마라!

중복은 소프트웨어에서 모든 악의 근원이다. AOP, 구조적 프로그래밍, COP 모두 중복 제거 전략이다.

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 글짓기와 비슷하다. 초안은 대게 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.

 

함수를 짤 때도 마찬가지이다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 이름은 즉흥적이며 코드는 중복된다. 하지만 그 코드에 대해 빠짐없이 테스트하는 단위 테스트 케이스도 만들어준다.

 

이후 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고, 순서를 바꾼다. 심지어 전체 클래스를 쪼개기도 한다. 동시에 코드에 대한 단위 테스트는 항상 통과한다.

 

처음부터 탁 짜내지 않는다. 그게 가능한 사람은 없으리라.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday