[Clean Code]를 읽으며 느낀 점 및 정리

Falco·2023년 2월 6일
1

책 리뷰

목록 보기
1/6

클린코드라는 책을 읽으며 기억할만한 어절이나 개념들을 간단하게 정리해 보았다.
실제로 읽으면서 어려워서 상당부분을 빼놓았지만, 깨끗한 코드를 작성하기 위한 내가 할 수 있는 팁들은 모두 적어 놓았다.

잊어버리기 쉬운 책의 내용을 이 글을 읽으면서 5분만에 간단하게 되짚어보자.


1장 깨끗한 코드 🧹

나쁜 코드는 개발 속도를 크게 떨어뜨린다. 프로젝트 초반에는 번개처럼 나가다가 1, 2년 만에 굼뱅이처럼 프로젝트 속도가 느려질 것이다.

빠르게 개발하기 위해 어쩔 수 없이 코드를 작성하는가?

나쁜 코드를 작성하는 이유로 일정이 촉박해 제대로 할 시간이 없었다는 것은 변명에 불과하다. 다음 예를 보자

자신이 의사라 가정하자. 어느 환자가 수술 전에 자기가 너무 아파서 빠르게 치료를 받고 싶기 때문에 손을 씻지 말라고 요구한다. 환자는 고객이고 상사이다. 하지만 의사는 이를 단호하게 거부해야한다. 왜? 감염과 질병의 우험은 환자보다 의사가 더 잘 아니까. 환자 말을 그대로 따르는 행동은 전문가답지 못하다.

나는 개발의 전문가가 될 것이며, 이런 나쁜 코드의 위험을 이해하지 못하는 것은 전문가 답지 못하다. 기한을 맞추기 위해 나쁜 코드를 양산하면 결국 마감일을 맞추지 못한다. 빨리가는 이율힌 방법은 언제나 코드를 최대한 깨끗하게 유지하는 습관이다.


한번 망가진 거리는 복구되기 어렵다.

비야네 스트롭스트룹

나는 우아하고 효율적인 코드를 좋아한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유호겡 빠지지 않는다. 꺠끗한 코드는 한 가지를 제대로 한다.

우아한 형제들이 왜 우아하다라는 단어를 사용하게 됬는지 알 수 있는 말이다. 한번 망가지고 더럽혀진 거리는 깨끗해지기 어렵다. 아니 오히려 누구도 상관하지 않는다는 인상을 풍기기 때문에 낙서를 하고, 쓰레기가 쌓이며 창문이 깨지는 쇠퇴하는 과정이 반복될 것이다.

코드에서 깨끗한 코드는 한가지에 집중한다. 각 함수와 클래스와 모듈은 주변 상황에 현혹되거나 오염되지 않은 채 한길만을 걸어야 할 것이다.

이 책에서 이야기하고 있는 깨끗한 코드의 정의는 다음과 같다.

1. 코드가 잘 쓴 문장처럼 읽혀야 한다.

반지의 제왕을 읽을 때 처럼 해결할 문제에 대한 긴장을 명확히 들어내야 한다.

2. 다른 사람이 고치기 쉬운 코드여야 한다.

읽기 쉬운 코드와 깨끗한 코드는 엄연히 다르다. 깨끗한 코드는 테스트 케이스가 있어야하며 이는 작을수록 좋다.

3. 깨끗한 코드는 주의 깊게 작성한 코드이다.

고치려고 아무리 살펴봐도 딱히 손 댈 곳이 없으면 그것은 깨끗한 코드이다. 
(작성자가 이미 모든 사항을 고려해 코드를 작성했기 때문)

4. 중복을 피하라, 한기능만 수행라라, 제대로 표현해라, 작게 추상화해라

추상화를 위해 추상 클래스와 추상 메서드를 작성하는 것은 아주 좋다. 이를 작성하는 과정에서 `진짜`문제에 다가갈 수 있기 때문이다.

우리는 새로운 코드를 작성하며 끈임없이 기존 코드를 읽는다. 코드를 읽는 시간 대 코드를 짜는 시간 비율이 10:1정도라고 하기 때문에 읽기 쉬운 코드가 매우 중요하다.

결론

어렵게 말했지만 우리가 깨끗한 코드를 작성하기 위해 많은 시간과 노력을 투자해 코드를 정리할 필요는 없다. 그저 평소에 코드를 작성할 때 변수 이름에 신경쓰고, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하고, 복잡한 if문 하나를 정리하는 것 만으로 충분하다.


2장 의미있는 이름 📛

변수, 클래스, 인터페이스, jar파일, 디렉토리 등 수많은 이름을 짓게 될 것이다. 이런 이름을 잘 짓는 간단한 규칙 몇가지를 알아보자.

의도를 분명히 밝혀라

좋은 이름을 지으려면 시간이 걸리지만 더 나은 이름으로 절약하는 시간이 더 많다. IDE에서 이름 바꾸는 기능을 제공하기 때문에 이름을 주의깊게 살펴 더 나은 이름이 떠오르면 바로 개선하기 바란다.

이름을 작성하기 위해서는 존재 이유, 수행 기능, 사용 방법등의 주석이 필요하다면 이는 의도를 분명히 드러내지 못했다는 말이다.

lateinit var d: Int // 경과 시간(단위: 날짜)

lateinit var daysSinceCreation: Int 

그릇된 정보를 피하라

개발자가 보기에 이름에 잘못 파악될 수 있는 그릇된 단서를 남기면 안된다. 예를 들어 hp라는 변수를 만들었을 때 이가 health percentage(체력의 양)인지, hypotenuse(직각삼각형의 빗변)인지 단숨에 파악할 수 없기 때문이다.

의미 있게 구분하라

연속적인 숫자를 덧붙인 이름(a1, a2, a3)은 최악의 네이밍이다. 이런 이름은 그릇된 정보를 제공하는 이름도 아니며, 아무런 정보를 제공하지도 못한다.

또한 변수명에 자료형을 포함하지 말아야 한다. accountsList: List<Account>는 이미 리스트로 선언되어 있으며 컴파일로 이를 알고 사용을 제한한다. 굳이 중복된 이름을 써가며 사용하기 보단 accounts, accountsGroup등의 이름을 사용하자.

불용어를 제한하라. ProductInfo 혹은 ProductData에서 InfoData는 의미가 불분명한 불용어이며 아무런 정보의 제공도 하지 않는다.

발음하기 쉬운 이름을 사용하라

발음하기 쉬운 단어가 읽기도 쉽다.

private Date genymdhms;

private Date generationTimestamp;

변수 젠 야 무다 힘즈의 사용에 오류가 생겼어 보단 제너레이션타임스탬프가 작동을 안해가 더 듣기 편하다.

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

const val MAX_USER_NICKNAME_LENGTH = 12

문자를 사용하는 이름과 상수는 텍스트 코드에서 눈에 잘 뛰지 않는다. 예를 들어 12라는 값을 IDE에서 검색하였을 때 내가 원하는 부분을 찾기는 정말 힘들 것이다.

상수는 추출하여 따로 저장하여 사용하되, 자주 사용되는 상수는 긴 이름보다 짧은 이름으로 사용하자.

멤버변수에 "m"을 붙일 이유는 없다.

클래스와 함수는 접두어가 필요없을 정도로 작아야 마땅하다. 이런 변경가능한 멤버변수는 IDE에서 다른 색으로 보여주는 것만으로 충분하다.

기억력을 자랑하지 말라

i, j, k등의 이터레이터나 a, b, test와 같은 단어는 금지다. 특히 코트린에서는 람다식을 작성할 때 it보다는 커스텀 접근자를 사용하자.

한 개념에 한가지 단어를 사용하라.

controller, manager, driver등의 개념은 동일한 역할을 수행한다. DeviceManagerDeviceController의 차이는 명확하지 않기 때문에 한 개념에는 한가지 단어를 사용하라

프로그래머에게 익숙한 기술 이름을 사용하라

코드를 읽는 사람도 프로그래머다. 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용해도 괜찮다.

의미 있는 맥락을 추가하여 이름을 작성하라

firstName, lastName, addLastName, modifyAddr, printUserStatistics등의 이름은 읽기만 하여도 어떤 변수인지, 어떤 행동을 하는 함수인지 알아낼 수 있다. 주석을 추가하여 해당 함수에 대해 설명을 적는 것 보다는 이름이 길어져도 해당 함수가 어떤 행동을 하는지 맥락을 추가하여 이름을 작성하라.

결론

사람들이 이름을 바꾸지 않으려는 이유 하나는 다른 개발자가 반대할까 두려워서이다. 실상을 다르다. 오히려 좋은이름으로 바꿔주면 반갑고 고맙다.

우리들 대다수는 자신이 짠 클래스 이름과 메서드 이름을 모두 암기하지 못한다. 그렇기에 우리는 문장이나 문단처럼 읽히는 코드를 짜는 데만 집중해야 마땅하다. 이름 역시 나름대로 바꿧다가는 누군가 질책할지도 모른다. 그렇다고 코드를 개선하려는 노력을 중단해서는 안된다.


3장 함수 📚

깨끗한 함수는 무엇인가.

함수는 작아야 한다.

함수를 만드는 첫째 규칙은 작게!이다. 그리고 두번째 규칙은 더 작게!다. 함수가 10줄을 넘어가면 과제충이며 15줄이 넘어가면 이는 고도비만이다.

depth도 동일하다. 함수의 depth가 3이상이면 이는 굴러갈 정도로 배가 많이 나온 사람이라고 생각하고 코드를 작성하자.

한 가지 일만 수행하라.

다음은 지난 30여년 동안 여러 가지 다양한 표현으로 프로그래머에게 주어진 충고다.

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

이런 한 가지는 추상화의 수준에 따라서 다르게 된다. 예를 들어

1. 유저의 성별을 체크한다.
2. 여자면 드레스를 추천한다.
3. 남자면 정장을 추천한다.

해당 추상화에서 함수는 3가지일을 한다. 하지만 이를 수정하면

1. 유저의 성별에 맞는 옷을 추천한다.

로 추상화의 수준을 높일 수 있다.

따라서 저자는 함수가 한 가지만 하는지 판단하는 방법을 소개한다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

내려가기 규칙 : 우리는 책을 위에서 아래로 읽는다.

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와야한다. 이렇게 아래로 갈 수록 추상화의 수준이 낮아지는 것이 좋다. 저자는 이를 내려가기규칙 이라고 부른다.

Switch문은 저차원 클래스에 숨기고 절대로 반복하지 말라

코틀린에서는 When을 사용함으로 예제 코드를 약간 바꾸어 적겠다.

fun makeEmployee(record: EmployeeRecord) : Employee {
	when(record.type) {
    	COMMISSIONED -> { /* ... */ }
    	HOURLY -> { /* ... */ }
    	SALARIED -> { /* ... */ }
    	else -> { throw IllegalStateException() }
    }
} 

오늘날 코틀린읜 When문을 효율적으로 사용하기 위한 Enum Class, Sealed Class를 제공하기에 이를 활용하자.

서술적인 이름을 사용하라!

코드를 읽으면서 짐작했던 기능을 그대로 수행하면 깨끗한 코드라고 불러도 될 것이다.

inclueOneOrMoreTags, sumEmployeesSalary등 길고 서술적인 이름이 하는 일을 더 잘 표현함으로 더 좋은 이름이다. 서술적인 주석보다 명명법을 사용한 서술적인 함수 이름이 더 효율적이다.

이름을 정하느라 시간을 들여도 괜찮다. 이런저런 이름을 넣어 코드를 읽어보면 더 좋다. 이름 바꾸기는 어짜피 F6버튼만 눌르면 IDE가 바꿔주니까!

함수 인수는 적어야 한다.

이상적인 인수의 개수는 0개(무항)이다. 다음은 1개(단항)이고 그다음은 2개(이항)이다. 4개 이상의 다항은 특별한 이유가 있어도 사용하면 안된다. 인수는 개념을 이해하기 어렵게 만든다. setupPage(pageContent, writer)보다 setupPage()이 더 이해하기 쉽다.

또한 테스트의 관점에서 다항은 최악이다. 테스트 인수가 많아질 수록 많은 테스트를 만들어야 하며 테스트 구성이 부담스러워진다.


블로그에 글을 적으며 필자가 말한 함수 인수가 적어야 한다가 Kolin에도 적용되는가에 대해 나의 생각을 적자면 반은 찬성이다.

과거 자바에는 인자의 갯수에 따라 다른 함수가 오버로딩되며 실행되고, 인자의 형태에 따라 다른 함수가 오버라이드되며 실행된다. 그렇기에 initView(content, user, condition)등의 인자 순서가 하나라도 바뀌면 예상한 함수가 실행되지 않을 수 있다.

하지만 코틀린에서는 이름있는 아규먼트를 제공하며, 기본 인자를 제공하기 때문에 인자의 개수가 많아져도 함수의 동작을 이해하기 어렵지 않다.

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

다음은 안드로이드 Compose에서 사용하는 Column함수의 예이다. 4개의 인자를 받는 다항함수지만,

Column(modifier = modifier) {
	// Contents
}

와 같이 간단하게 사용할 수 있으며 이해하기도 쉽다.

또한 다음 예를 보자.

assertEquals(expected, actual)

자바에서 expected 인수에 actual값을 집어넣는 실수를 얼마나 많이 했던가. 두 인수는 자연적인 순서가 없다. 따라 이를 의식적으로 기억했어야만 했다. 하지만 코틀린에서는 다르다.

assertEquals(expected = "", actual = "")

이 얼마나 이해하기 쉽고 아름다운가.

코틀린에서 인자의 갯수는 많아도 상관없다?

하지만 이말에 반만 찬성이라고 한 이유가 있다. 이는 함수는 테스트를 작성할 때 함수의 인자가 많다면 그만큼 테스트할 조합이 많아지기 때문이다. (애초에 인자가 여러개인 함수는 하는 일이 그만큼 많기 때문에 테스트에 비효율적이다.) 상황에 맞추어 자신에게 맞는 인자의 개수를 선택하도록 하자

플래그 인수는 추하다

함수로 Boolean값을 넘기는 관례는 정말로 끔찍하다. 왜냐고? 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까.

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

SideEffect 즉 부수효과는 함수에서 내가 모르는 다른 일이 실행되는 것이다. 이를 의도하는 사람은 거의 없고, 예상치 못하게 작동된다.

public boolean checkPassword(String userName, String password) {
	User user = UserGateWay.findByName(userName);
    if (user != User.NULL) {
    	if(user.getPhraseEncodedByPassword() == password.encode()) {
        	Session.initialized()
            return true
        }
    }
    return false
}

다음 함수의 부수효과는 Session.initialized()이다. checkPassword는 비밀번호가 맞는지 확인하는 기능을 수행한다고 했지, 세션을 초기화한다는 사실은 드러나지 않는다. 그래서 함수 이름만 보고 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버린다.
이를 방지하기 위해서는 checkPasswordAndInitializeSession으로 변경하는 것이 훨씬 좋다.

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

if (deletePage(page) == E_OK)

위 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다. 이런 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.

if (deletePage(page) == E_OK) {
	if(registry.deleteReference(page.name) == E_OK) {
    	if(configKeys.deleteKey(page.name.makeKey() == E_OK) {
        	// ...
        }
    }
}

예제 코드를 작성하는 것도 끔찍하다. 이렇게 사용하기 보다 try - catch문과 예외를 사용해 이를 처리하라

try {
	deletePage(page);
    registry.deleteReference(page.name);
} catch(Exception e){
	logger.error(e.getMessage());
}

이 코드는 이해하기 쉽다. 이러한 오류 처리도 한 가지작업만을 수행해야 한다.

반복하지 마라 👺

같은 코드를 두번 째 적고 있다면 무엇인가 실수하고 있는 것이다. 중복은 소프트웨어에서 모든 악의 근원이다. 데이터베이스에서는 중복을 제거할 목적으로 정규 형식을 만들었고, 객체지향에서는 코드를 부모 클래스로 몰아 중복을 없앤다.AOP, OOP모두 중복을 제거하기 위한 전략이다.

함수를 작성하는 팁 및 결론

여느 글짓기와 비슷하다. 논문이나 기사를 작성할 때 먼서 생각을 주저리주저리 작성하고 보기좋게 다듬는다. 초안은 대게 서투르고 어수선하기에 원하는 대로 읽힐 때까지 말을 다듬도 정리하는 과정을 거친다.

함수를 작성할 때도 동일하다. 작동하는 함수를 만들고, 테스트케이스를 작성하고, 이름을 바꾸고, 중복을 제거하고, 메소드를 줄이며 순서를 바꾼다. 이렇게 바뀐 함수들도 테스트케이스를 만족해야 한다.

함수는 동사이며, 클래스는 명사다. 이러한 주인공들이 좀 더 풍부하고 좀 더 표현력 있게 스토리를 만들어 가는 것이 프로그래머가 할 일이라 생각한다. 여러분이 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기 쉬워진다는 사실을 기억하기 바란다.

주석 🐚

나쁜 코드에 주석을 달지 마라, 새로 짜라.

잘 달린 주석은 그 어떤 정보보다 유용하다. 경솔하고 근거 없는 주석은 해악을 미친다.

코드로 의도를 표현하지 못해, 실패를 만회하기 위해 주석을 사용한다. 주석은 언제나 실패를 의미한다. 주석은 반겨 맞을 손님이 아니다. 그러므로 주석이 필요한 상황에 처하면 곰곰이 생각해보길 바란다. 상황을 역전해 코드로 의도를 표현할 방법은 없는가? 코드로 의도를 표현할 때마다 스스로를 칭찬하자.

저자는 왜 이렇게 주석을 폄하하는가?

주석은 고의는 아니지만 너무 많이 거짓말을 한다. 아니 거짓말을 하게 된다. 코드가 오래될 수록 주석도 바뀌어야 하는데 프로그래머들이 주석을 유지보수하기는 현실적으로 어렵기 때문이다. 코드가 분할되고 합쳐지면서 진화한다. 주석은 과연 그럴 수 있는가? 주석이 코드에서 분리되어 점점 더 부정확한 고아로 변하는 사례가 너무 흔하다.

주석은 나쁜 코드를 보완하지 못한다.

주석을 추가하는 이유는 실패 때문이라고 했다. 내가 코드를 잘못 짰기에, 코드 품질이 낮기에 주석을 작성한다. 자신이 저지른 난장판을 주석으로 설명하려고 애쓰기 보다 그 난장판을 깨끗이 치우는데 시간을 보내라.

그러기 위해서

코드로 의도를 표현하라

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if((employee.flags && HOURLY_FLAG) && employee.age > 65)

if(employee.isEligibleForFullBenefits())

몇 초만 더 생각하여 코드로 의도를 표현하라. 많은 경우는 주석으로 달려는 설명을 함수로 만들어 표현하는 것만으로 충분하다.

좋은 주석

정말로 좋은 주석은 주석이 필요없는 구조와 네이밍이다.

하지만 꼭 필요하거나 좋은 주석의 예도 몇 개 보고 가겠다.

정보를 제공하는 주석

// kk:mm:ss EEE, MMM dd, yyyy 형식의 정규식
val timeRegex = """\\d*:\\d*:\\d* \\w*, \\w*. \\d*. \\d*"""

어떤 형식의 정규식이라는 것을 변수이름에 넣기는 쉽지않다. 이런 설명은 주석으로 표현해도 좋다. (이왕이면 obejct나 class로 추출하여 사용하면 더 좋다.)

의도를 설명하는 주석

var lambda = {a:Int,b:Int-> when{
        a<b -> 1 	// a가 더 작을 시 1을 반환하여 Comparator가 해당 수를 앞으로 가게 함
        a>b -> -1
        else -> 0
    }}
var pq = PriorityQueue(Comparator<Pair<Int,Int>>{a,b ->
        when{
            a.first !=b.first -> lambda(a.first,b.first)
            else -> lambda(a.second, b.second)
    }})

해당 네이밍은 적절치 못하더라도 왜 저러한 람다식을 사용했는지는 알 수 있다.

TODO 주석

앞으로의 할일을 //TODO주석으로 남겨놓으면 편하다. IDE에서는 이러한 주석을 전부 찾아주는 기능도 제공하여 잊어버릴 일이 없다.

이 외에도 중요성을 강조하거나, 설명이 잘 된 공개 API에 대한 주석 등 훌륭한 주석이 있다.

나쁜 주석

대다수 주석이 이 범주에 속한다. 대부분의 나쁜 주석은 엉성한 코드를 변명하거나, 미숙한 결정을 합리화하는 프로그래머의 독백에서 크게 벗어나지 못한다.

주절거리는 주석

try {
	val context = LocalContext.current
	// do something ...
    
} catch(e:Exception) {
	// 여기에 도착한다면 context가 잘못 설정된 것이다.
}

해당 주석은 아무런 도움이 되지 않는다. context가 왜 잘못 불러졌는가? 어떻게하면 이를 방지할 수 있는가?에 대한 해결책은 다른 코드를 뒤져보는 수 밖에 없다. 이는 바이트 낭비 그 자체이다.

같은 이야기를 하는 주석

// 화면에 보이는 리뷰를 담고있는 변수이다.
private val _reviews:MutableLiveData<List<Review>> = MutableLiveData(emptyList())
val reviews:Livedata<List<Review>> get() = _reviews

// 레포지토리로부터 리뷰를 불러온다.
fun getReviews() = viewModelScope.launch {
	repository.getReviews().collect()
}

주석을 읽는 시간과 소스를 읽는 시간이 거의 동일하다. 이는 코드를 지저분하고 정신 없게 만들 뿐이다.

주석으로 처리한 코드는 없애라

// val is = formatter.getResultStream()
// val reader = StreamReader(resutsStream)

해당 소스는 뭔가 의미가 있어보이고 다른 사람이 지우기를 주저한다. 질 나쁜 노봉방주가 쌓이듯 쓸모 없는 코드가 점차 쌓여간다.

주석은 해당하는 부분에만 달아라

/** 
 * 기본 적합성 테스트가 동작하는 포트는 8080이다.
 */
fun setFitnessPort() {

}

만약 기본값 포트가 바뀐다고 할 때 이 주석을 바꿀 생각을 하겠는가?

책에서 필자는 오해할 여지가 있는 주석, 이력을 기록하는 주석, 닫는 괄호에 다는 주석, 의무적으로 다는 주석, 있으나 마나 한 주석 등을 필요없는 나쁜 주석이라고 말하고 있다.

함수나 변수로 표현할 수 있다면 주석을 달지 말 것이며 주석을 달더라도 의미있는 주석을 달아라

형식 맞추기

프로젝트를 열었을 때 정돈된 코드가 보이면 기분이 매우 좋을 것이다. 또한 한번 정돈된 코드는 앞으로 코드를 작성할 때도 정돈되게 작성해야할 강박감이 들 수 있다.

팀으로 일한다면 규칙을 정하고 모두가 그 규칙을 따라야 한다. 필요하다면 규칙을 자동으로 규칙하는 도구를 활용할 수 도 있다.

그래도 몇가지 대중적으로 사용되면 좋을 형식들이 있다. 그런 형식을 알아보자.

신문기사처럼 작성하라.

모든나라에서 신문기사나 에세이를 위에서 아래로 읽는다. 중요한 내용이 대부분 위에 있으며,, 아래는 세세한 부분이 들어난다. 클래스는 신문의 주제이고, 함수는 해당 주제에 대한 설명이다. 날짜, 이름, 사실을 무작위로 뒤섞은 기사는 아무도 읽지 않을 것이다.

개념은 빈 행으로 분리하라

package, import, class, fun사이에는 빈 행이 있어야 한다.

pakcage com.example.example

import java.util.*

class SomeClass() {
	
    fun someFunction() {}
}

세로 밀집도

서로 밀접한 코드 행은 세로로 가까이 놓여야 한다.

private val userName = ""

private val _liveData :MutableLiveData<String> = MutableLiveData("")
private val liveData :LiveData<String> get() = _liveData

함수 연관 관계와 동작 방식을 이해하려고 이 함수 저 함수 오가며 소스를 위아래도 뒤지는 뺑뺑이를 한번씩은 돌아봤을 것이다. 이런 경험을 방지하기 위해 밀접한 개념은 세로간격이 가깝게 유지하자.

변수는 사용하는 위치에 최대한 가깝게 선언하라

fun updateData() {
	var count = 0
    // ...
    // ...
    try {
    	while {
    		// doSomething
	        count +=1
        }
    } catch(e:Exception) {
    
    }
}

이는 그렇게 긴 함수는 아니지만 만약 함수가 길어지면 count를 찾기위해 스크롤을 올려야 할 수도 있다.

인스턴스 변수는 클래스 맨 처음에 선언한다.

인스턴스 변수는 대부분 클래스 맨 처음에 선언하며 세로로 거리를 두지 않는다. 잘 설계한 클래스는 많은 클래스 메소드에서 인스턴스 변수를 사용하기 때문이다.

코틀린에서는 인자로 오는 인스턴스 변수를 생성자에서 바로선언하여 사용할 수 있다.

class NetworkHelper(
	private val url: String,
    private val interceptor: Interceptor,
)

종속 함수, 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치할 것

fun validateUser() {
	repository.validateUser(
	    userName = userName,
    	onSuccess = { getDetailContent() }
    )     
}

fun getDetailContent() {
	// do something
}

바로 아래에 배치된 함수를 호출하면 가독성이 좋다.

개념적으로 친화된 코드는 가까이 배치한다.

함수 내부에 종속되지 않아도, 개념적으로 종속된 함수는 가까이 배치한다.

fun addPlan() {}

fun removePlan() {}

fun modifyPlan() {}

가로의 길이에 대하여

대부분의 개발자는 짧은 행을 원한다. 코드를 읽는 것은 개발자이다. 그럼으로 짧은 행으로 코드를 작성하는 것이 바람직하다.

객체와 자료 구조 ⛑

변수를 private으로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서이다. 하지만 많은 프로그래머가 gettersetter를 활용해 비공개변수를 공개적으로 외부로 노출한다.

우리는 이들과는 차별되게 소스를 작성해야 한다.

그러기 위해서 우리는 자료구조객체의 차이가 무엇인지 알아야할 필요가 있다.

객체는

추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.

interface Vehicle {
	fun getPercentFuelRemaining() : Double	
}

자동차 연료 상태를 백분율로 반환하는 추상적인 함수는 내부의 정보를 전혀 공개하지 않는다.

자료구조는

자료를 그대로 공개하며 별 다른 함수는 제공하지 않는다.

data class Point(
	val x : Int,
    val y : Int
)

  • 자료 구조를 사용하는 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
  • 객체 지향 코드(객체를 사용하는 코드)는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

이의 반대도 역시 옳은 말이다.

  • 절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.
  • 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.

디미터 법칙

디미터 법칙은 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.

이를 잘설명한 것이 우아한형제들 기술블로그 - 생각하라, 객체지향처럼을 보면 알 수 있다.

val outputDir = ctxt.options.scratchDir.absolutePath

다음과 같은 코드가 있다고 하자. 유저는 무엇을 위해 .을 3번이나 써가면서 absolutePath를 사용하고자 하는가?

val fileOutputStream = FileOutputStream(outputDir)
val bufferedOutputStream = BufferedOutputStream(fileOutputStream)

해당 파일의 버퍼를 읽기 위함이였다. 그렇다면 이러한 일을 객체가 하도록 시키는 것이 어떤가??

val bufferedOutputStream = ctxt.getBufferedOutputStream()

절대 경로를 구할필요도 없이 객체가 일을하며 해당 파일의 버퍼를 읽을 수 있다.

결론을 요약하면 "객체가 일을하게 하자" 이다.

잡종 구조

하지만 모든 클래스를 자료 구조, 객체의 형태로 뚜렷하기 나누기는 쉽지 않다. 객체를 만들면서도 getter()를 활용해 비공개 변수를 노출하고픈 유혹에 빠지기 쉽상이다.

이렇게 절반은 객체, 절반은 자료 구조인 클래스를 잡종 구조라고 한다. 이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다.

코틀린에서는 Data class를 제공하여 자료구조로 사용하라고 권장하고 있다.

흔히 DTO라고 일컷는 자료 전달 객체는 자바에서부터 사용되던 유용한 구조체이다.

public class Address {
	private String street;
    private String streetExtra;
    
    public Address(String street, String streetExtra) {
    	this.street = street;
        this.streetExtra = streetExtra;
    }
    
    public String getStreet() {
    	return street;
    }
    
    public String getStreetExtra() {
    	return streetExtra;
    }
}

해당 구조는 Bean구조라고 하기도 하며 비공개 변수를 조회/설정 함수로 조작한다. 이는 사이비 캡슐화로, 일부 순수주의자나 만족시킬 뿐 별다른 이익을 제공하지 않는다.

하지만 코틀린의 data class는 다르다.

data class Address(
	val street: String,
    val streetExtra: String
)

자바의 코드와 달리 getter, setter를 자동으로 만들어줌과 동시에 equals, toString, hashCode 함수를 자동으로 만들어줌으로 데이터 클래스끼리의 비교함수를 따로 만들 필요가 없다.

Address("A","B") == Address("A","B") // true

결론

객체는 동작을 공개하고 자료를 숨긴다. 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.

자료구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료구조를 추가하기는 어렵다.

오류 처리 🛑

깨끗한 코드와 오류 처리는 확실한 연관성이 있다. 상당수 코드 기반은 전적으로 오류 처리 코드에 좌우된다.

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

if (handle != DeviceHandle.INVALID) {
	// ERROR!
} else {
	// DO SOMETHING!
    if (res != ResultWrapper.ERROR) {
    	// ERROR!
    } else {
    	// DO SOMETHING!
    }
}

위의 코드와 같이 예제코드를 활용해 예외를 처리한다면 코드가 중첩되며 더러워질 것이다.

요즘 언어는 대부분 try-catch를 제공함으로 이런 일이 존재하지 않는다.

Try-Catch-Finally 문 부터 작성하라

이는 TDD에서 언급되는 내용과 동일하다. 강제로 예외를 일으키는 테스트 케이스를 작성한 후 그 테스트를 통과하게 코드를 작성한다. (단 다른 오류는 생각하지 않는다.) 코드가 작성되면 새로운 테스트 케이스를 추가하여 코드를 수정한다.

이렇게 코드를 작성하면 모든 테스트케이스를 검증할 수 있는 유지보수 용이한 코드를 작성할 수 있다. 또한 자연스럽게 try, catch의 트랜잭션 범위부터 구현하게 되므로 트랜잭션의 본질을 유지하기 쉬워진다.

예외에 의미를 제공하라

예외를 던질 때는 전후 상황에 대한 설명을 덧붙여라. 오류 메시지에 실패한 연산 이름과 실패 유형도 언급한다. 팀이 있다면 팀 내에서 에러 메시지에대한 컨벤션을 설정하여 에러를 던져라. 이러한 에러는 로깅하며 오류를 쉽게 찾을 수 있도록 도움을 준다.

[ERROR] NullPointException : $RouteDTO Street속성은 15글자 이하여야 합니다. 

null을 반환하지 마라

null 확인을 누구 한명이라도 빼먹는다면 이는 바로 널익셉션오류가 날 것 이다. 외부 라이브러리에서 null을 반환한다면 해당 API의 감싸기 메서드를 구현해 예외를 던지거나 특수 사례를 반환하라.

코틀린은 null을 허용하지 않는 언어이며 이를 다루기 위한 여러 도구도 제공한다. (엘비스 연산자, 논널 어설션, 세이프콜 등등) 그럼으로 널에 대한 대처가 컴파일전에 대부분 가능하다.

하지만 플랫폼 타입의 널 확인에 대해 크게 신경써야 한다.[kotlin] 안정성을 위해 - 최대한 플랫폼 타입을 사용하지 말라 특히 자바와 협엽할 때는 @Nonnull, @Nullable등과 같은 어노테이션을 활용하여 응답값을 합의하도록 하자.

결론

깨끗한 코드는 읽기도 좋아야 하지만, 안정성도 높아야 한다. 이 둘은 서로 공생하는 목표이다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

경계

외부 라이브러리, 인터페이스를 사용할 때는 이 코드를 프로젝트에 맞게 깔끔하게 통합해야한다. 이런 소프트웨어의 경계를 깔끔하게 처리하는 방법이 무엇일까?

외부 코드 사용하기

한 예로 java.util.Map을 보자 Map은 굉장히 다양한 인터페이스로 수많은 기능을 제공한다. Map이 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험도 크다.

예를 들어 프로그램에서 Map객체를 여기저기 넘긴다고 가정하자. 여러군대 필요하니까.. 하지만 어느 쪽에서 Map내용을 수정하거나 삭제한다고 생각해보자. clear()Map객체를 가지고 있으면 누구든지 실행이 가능하니까 말이다. 순식간에 프로그램에서 에러를 뱉어낼 것이다.

또한 Map객체에 특정 유형만 저장하기로 구두 약속을 했다고 가정하자. 입사한지 일주일 된 개발자가 모든 유형이 들어갈 수 있게 해당 객체를 조정하고 있다면 어떻게 할 것인가??

책에서 필자는 경계 인터페이스를 활용하여 Map을 객체 내부로 숨기고 있다.

public class Sensors {
	private Map sensors = new HashMap();
    
    public Sensor getById(String id) {
    	return (Sensor) sensors.get(id);
    }
    
    // ...
}

해당 경계 인터페이스를 활용하면 유형을 제한할 수도 있고, Map에 대한 삭제나 수정도 제한할 수 있다. 이런 Map과 같은 경계 인터페이스를 활용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않게 조심하라.

코틀린에서는

코틀린에서는 이미 해당 문제를 언어 자체에서 해결하고 있다.

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> 

자바의 콜렉션을 상속받아 수정이 가능한 MutableCollectionImutableCollection으로 나누어 콜렉션을 제공하고 있고, 더해 제네릭을 활용해 맵에 들어갈 유형도 제한하고 있다. mapOf<String, Int>

경계 살피고 익히기

외부 경계에 있는 소스를 안전하고 알맞게 사용하기 위해서 학습테스트를 만들 수 있다.

학습 테스트란 가상으로 테스트케이스를 만들어 해당 인터페이스를 돌려보는 테스트이다. 학습 테스트에 드는 비용은 없으며, 어쨌든 API는 배워야함으로 노력보다 얻는 성과가 더 크다.

결론

이런 경계에서는 알수없는 일이 많이 벌어진다. 또한 오픈소스가 업데이트 되거나, 소프트웨어 수정이 필요할 때 이러한 경계가 수많이 있다면 유지보수가 쉽지 않을 것이다.

따라서 경계에 위치하는 코드는 깔끔히 분리하되, 외부 패키지에 의존하는 코드의 량도 줄여라.

단위 테스트 ⚠️

TDD 법칙 세가지

TDD가 실제 코드를 작성하지 전에 단위 테스트부터 짜라고 요구한다는 것은 누구나 알 것이다. 이에 더해 몇가지 규칙을 더 알아보자.

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

위의 규칙을 따르며 개발하면 매일 수십 개, 매달 수백 개, 매년 수천 개에 달하는 테스트 케이스가 나오게 된다. 이러면 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.


필자는 다음과 같은 경험을 했다고 한다.

  1. 지저분해도 빨리 개발해라는 목적으로 단위 테스트 규칙을 깨고 개발을 진행헀다.
  2. 하지만 테스트는 진행해야 함으로 지저분한 테스트 코드라도 작성하여 현재 코드가 돌아가는지 확인한다.
  3. 실제 코드를 수정할 일이 생기면 해당된 모든 테스트 코드를 수정해야 한다.
  4. 실제 코드를 짜는 시간보다 테스트코드를 수정하는 시간이 더 길어진다.
  5. 새 버전을 출시함에 따라 테스트 케이스를 유지하고 보수하는 비용도 늘어난다. 이 때 오래된 테스트 케이스는 골칫거리가 된다.

그들이 테스트 코드에 쏟아 부은 노력은 나쁘지 않았다. 나쁜 것은 테스트 코드를 막 짜도 좋다고 허용한 결정이었다.

결론을 말하자면

깨끗한 테스트 코드는 실제 코드 못지 않게 중요하다.

테스트 코드는 깨끗하게 유지하지 않으면 결국은 잃어버린다. 그리고 테스트 케이스가 없으면 시제 코드를 유연하게 만드는 버팀목도 사라진다.

깨끗한 테스트코드란?

깨끗한 테스트 코드를 만들려면 세 가지가 필요하다.

  1. 가독성
  2. 가독성
  3. 가독성

가독성은 어쩌면 실제 코드보다 테스트코드에 더 필요하다. 이런 가독성을 높일려면? 명료성, 단순성, 풍부한 표현력이 필요하다.

테스트 코드는 정말 필요한 자료 유형과 함수만을 사용하여 간단하게 표현해야 한다.

테스트 당 ssert 하나

JUnit으로 작성하는 테스트 코드를 잘 때는 함수마다 assert문을 단 하나만 작성해야 한다라고 말하는 학파가 있다. 이는 가혹해 보이지만 확실히 장점이 있다. 결론이 하나뿐이라 코드를 이해하기 쉽기 때문이다.

given-when-tehn 관례 사용하기

테스트 당 개념 하나

F.I.R.S.T.

Fast : 빠르게
Independent : 독립적으로 (테스트가 테스트에 의존해서는 안된다.)
Repeatable : 반복가능하게 (테스트는 어떤 환경에서도 돌아가야한다.)
Self-Validating : 자가검증하는 (테스트는 Boolean값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알려고 로그 파일을 읽게 해서는 안된다.)
Timely : 적시에 작성해야한다. (단위 테스트는 테스트하려는 실제 코드 구현 전에 작성할 것)

결론

테스트 코드는 어쩌면 실제코드 보다 더 중요할지도 모르다. 따라서 테스트 API를 구현해 DSL을 만들어 활용하는 것도 좋다. 테스트 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 위해서 노력하라

클래스

클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야한다.
두 번째 규칙도 크기다. 클래스는 더 작아야 한다. 함수와 마찬가지로, '작게'가 기본 원칙이다.

클래스 이름은 해당 클래스의 책임을 기술해야 한다. 간결한 이름이 떠오르지 않는다면 클래스의 책임이 너무 큰건 아닌지 고민해보라.
(클래스의 설명은 if, and, or, but을 쓰지 않고 25단어 내외로 끝어야 한다.)

단일 책임 원칙

Single Responsibility Principle(단일 책임 원칙)은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.

getMyProfileInfoUseCase라는 클래스를 변경할 이유는 내 프로필이미지를 가져올 때 밖에 없다.

큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이루어진 시스템이 더 바람직하다. 작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

응집도

클래스는 인스턴스 변수 수가 적어야 한다.
일반적으로 우리는 응집도가 높은 클래스를 선호한다. 응집도가 높다는 말은 클래스에 속한 메소드와 변수가 서로 의존하며 묶인다는 의미이다.

하지만 응집도가 높은 클래스는 가능하지도, 바람직하지도 않다.

큰 함수를 작은 함수 여럿으로 나누어 클래스를 늘려라.
클래스를 늘리고 인스턴스 변수를 각각의 클래스에서만 사용하라.

변경하기 쉬운 클래스

이렇게 나누어진 클래스는 변경하기 쉽다. 또한 새 기능을 확장하기엔 쉽지만 수정하기엔 어려운 클래스를 만들어야 한다. 기존 기능에 수정을 가하더라도 건드릴 코드가 최소인 시스템구조가 바람직하다. 이상적인 시스템이라면 새 시스템을 확장할 뿐 기존 코드를 변경하지는 않는다.

시스템

도시는 교통 관리, 수도 관리, 전력 관리 등의 팀으로 나누어져있어 각각의 사람들의 자신의 관심사(일)에 집중하여 돌아간다.

어플리케이션에도 중요한 것이 이런 관심사 분리이다.

if (service == null) {
	servce = new MyServiceImpl();
}
return service;

이것은 자바에서의 지연초기화를 위한 방법이다. 하지만 이런 소스는 일반 런타임 로직에 객체 생성 로직을 섞어놓은 탓에 단일 책임의 원칙을 벗어난다.

Main 분리

시스템 생성과 시스템 사용을 분리하는 한 가지 방법으로, 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.

메인 함수에서 시스템에 필요한 객체를 생성한 후 이를 어플리케이션에 넘긴다. 어플리케이션은 그저 객체를 사용할 뿐이다.

따라서 의존성 화살표의 방향이 모두 메인 쪽에서 어플리케이션으로 향한다. 즉 어플리케이션은 메인이나 객체갓 ㅐㅇ성되는 과정을 모른다는 뜻이다.

팩토리

때로 객체가 생성되는 시점을 어플리케이션이 결정할 필요도 생긴다.

팩토리 함수를 메인에서 제공하며 어플리케이션이 인스턴스가 필요할 때 팩토리에서 찍어내어 배포한다.

여기서도 모든 의존성이 메인에서 어플리케이션으로 향한다. 어플리케이션은 LineItem이 생성되는 방법을 모른다.

의존성 주입

사용과 제작을 분리하는 강력한 메커니즘 하나가 의존성 주입(DI)이다. 의존성 주입은 제어 역전(Inversion of Control)기법을 의존성 관리에 적용한 메커니즘이다. 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.

새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임원칙을 지키게 된다.

결론

꺠끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다. 도메인 논리가 흐려지면 제품 품질이 떨어진다. 버그가 숨어들기 쉬워지고, 스토리를 구현하기 어려워지는 탓이다. 기민성이 떨어지면 생산성이 낮아져 TDD가 제공하는 장점이 사라진다.

창발성

코드 구조와 설계를 파악하기 쉬워지는, 우수한 설계를 만들기 위한 간단한 네 가지 규칙이 있다.

켄트 백이 제시한 다음 네가지 설계 규칙은 소프트웨어 설계품질을 크게 높인다고 한다.

  1. 모든 테스트를 실행한다.
  2. 중복을 없앤다.
  3. 프로그래머의 의도를 표현한다.
  4. 클래스와 메서드 수를 최소로 줄인다.

위 순서는 중요도 순이다.

1. 모든 테스트를 실행한다.

결합도가 높으면 테스트 케이스를 작성하기 어렵다. 테스트를 많이 작성할 수록 개발자는 DI, 인터페이스, 추상화 같은 도구를 활용해 결합도를 낮춘다.

테스트 케이스를 만들고 계속 돌려라라는 간단하고 단순한 규칙을 따르면 시스템은 낮은 겨랍도와 높은 응집력이라는, 객체 지향 방법론이 지향하는 목푤를 저절로 달성한다.

리팩터링

테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리하라. 응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스의 크기를 줄이고, 더 나은 이름을 선택하라.

나쁜 요구사항은 다시정의하면된다. 나쁜 팀 역학은 복구하면 된다. 나쁜 코드는 썩어 문드러진다. 점점 무게가 늘어나 팀의 발목을 잡는다.

아침에 엉망으로 만든 코드를 오후에 정리하기는 어렵지 않다. 더욱이 5분전에 엉망으로 만든 코드는 지금 당장 정리하기 쉽다. 그러므로 코드는 언제나 최대한 빨리 깔끔하고 단순하게 정리하자. 절대로 썩어가게 방치하면 안 된다.

2. 중복을 없애라

우수한 설계에서 중복은 커다란 적이다. 2번 이상 반복되는 코드가 없이 작성하라 동일한 기능을 하는 여러 다른 함수는 인터페이스로 추출하라.

3. 의도를 제대로 표현하라

가장 비용이 많이 들어 가는 것은 장기적인 유지보수 비용이다. 개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다.

그러기 위해서
1. 좋은 이름을 선택하라
2. 함수와 클래스 크기를 줄여라.
3. 표준 평칭을 사용하라.
4. 단위 테스트 케이스를 꼼꼼히 작성하라.

나중에 읽을 사람을 고려해 좆금이라도 읽기 쉽게 만들려는 충분한 고민을 하자.

결론

경험을 대신할 단순한 개발 기법은 없다. 지금 위에서 말한 것들은 수십년 동안 쌓은 경험의 징수이다. 4가지 규칙을 지키기만 한다면 우수한 기법과 원칙을 지킨 시스템 구축이 가능해진다.


냄새와 휴리스틱

Refactoring에서 마틴 파울러는 "코드 냄새"라는 것을 거론한다. 다음 목록은 마틴이 맡은 냄새에 클린코드의 저자가 맡은 냄새를 추가한 것이다.

주석

C1 : 부적절한 정보

주석은 코드와 설계의 기술적인 설명을 부연하는 것이다.

C2 : 쓸모없는 정보

오래된 주석, 엉뚱한 주석, 잘못된 주석

C3 : 중복된 주석

C4 : 성의 없는 주석

주절대지 마라. 당연한 소리를 하지 마라

C5 : 주석 처리된 코드

주석 처리된 코드를 발견하면 바로 지워라. 필요하다면 과거 커밋에서 가져다 써라

환경

E1 : 여러 단계로 빌드

빌드는 간단히 한 단계로 끝나야 한다. 한 명령으로 전체를 체크하라

E2 : 여러 단계로 테스트

모든 단위 테스트는 한 명령으로 돌려야 한다.

함수

F1 : 너무 많은 인수

함수에서 인수 갯수는 작을 수록 좋다. 없으면 더좋다.

F2 : 출력 인수

함수에서 뭔가 상태를 변경해야 한다면 출력인수로 내뱉지 말고 객체의 상태를 변경하라.

F3 : 플래그 인수

boolean인수는 함수가 여러 기능을 수행한다는 증거이다. 이는 피해야 한다.

F4 : 죽은 함수

아무도 호출하지 않는 함수는 바로 삭제하라. 필요하다면 과거 커밋을 뒤져보라

일반

G1 : 한 소스에 여러 언어를 사용하는 것

G2 : 당연한 동작을 구현하는 것

fun weekToDay(weeks: Int) : Int = weeks * 7

G3 : 경계를 올바로 처리하지 않는 것

모든 경계 조건을 테스트하는 테스트 케이스를 작성하라

G4 : 안전 절차 무시

실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 신용카드가 공짜 돈이라는 생각과 동일하다.

G5 : 중복

코드에서의 중복은 추상화의 기회이다.

G6 : 추상화 수준이 올바르지 못한 것

세부 구현과 관련된 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안된다. 기초 클래스는 구현 정보에 무지해야 한다.

G7 : 기초 클래스가 파생 클래스에 의존한다.

개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서이다.

G8 : 과도한 정보

잘 정의된 모듈은 인터페이스가 작다. 작은 인터페이스만으로 많은 일을 한다. 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다.

자료를 숨겨라, 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라. 하위 클래스에서 필요하다는 이유로 protected변수나 함수를 마구 생성하지 마라. 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라.

G9 : 죽은 코드

호출하지 않는 유틸리티 함수, 불가능한 if문 등 죽은 코드를 발견하면 장례식을 치뤄주라.

G10 : 수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다. 비공개 함수는 처음으로 호출되는 위치 바로 아래 위치해야 한다.

G11 : 일관성 부족

최소 놀람의 원칙(The Principle of Least Surprise)에 따라 일관성있게 변수 이름을 작성하라. 어떤 개념을 구현할 떄도 특정 방식으로 구현했다면, 유사항 개념도 같은 방식으로 구현하라.

G12 : 잡동사니

사용되지 않는 건 다 지워

G13 : 인위적 결합

무관한 개념을 인위적으로 엮지 마라.

G14 : 기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 최선을 다하여야 한다. 다른 객체의 참조하여 변경을 하거나, 객체 내용을 조작한다면 그 객체 클래스를 욕심내는 것이다.

클래스 즉 객체가 자기 자신만의 일을 하도록 만들어라.

G15 : 선택자 인수

함수 인자의 끝에 달리는 boolean인자는 대체의 경우 필요가 없다. 이는 함수 분할을 하지 않은채 함수를 유지하려는 것이다.

G19 : 서술적 변수, 함수를 활용하라

이름이 길어져도 상관없다.

G28 : 조건을 캡슐화 하라

if (shouldBeDeleted(timer))

라는 코드보다 다음 코드가 좋다.

if (timer.hasExpirted() && !timer.isRecurrernt())

G29 : 부정조건은 피하라

G30 : 함수는 한 가지만 수행해야 한다.

함수를 만들면서 생각해야할 것 첫번째는 함수는 최소 기능만 수행할 것 이다.

G33 : 경계 조건을 캡슐화 하라

int netLevel = level + 1 // 경계 조건

if (nextLevel < tags.length) {
	// ...
}

G34 : 함수는 추상화 수준을 한 단계만 내려가야한다.

더 깊은 기능을 수행하고자 한다면 함수를 추가하라.

G36 : 추이적 탐색을 피하라

한 모듈은 주변 모듈을 모를수록 좋다.

A가 B를 사용하고 B가 C를 사용할 때 A는 C를 몰라야 한다.
profile
강단있는 개발자가 되기위하여

0개의 댓글