티스토리 뷰

Programming

Clean Code 책을 읽고

junojuno 2022. 11. 1. 07:10

아래 글은 로버트 C. 마틴의 Clean Code라는 책을 읽고 스스로 정리하며 학습한 목록입니다.

특히 우아한 테크코스 - 프리코스 1 주차를 진행하며 학습이 필요하다 스스로 판단한 부분을 집중적으로 다루었습니다.

1. 깨끗한 코드

나쁘 코드로 치르는 대가

나쁜 코드는 개발 속도를 크게 떨어뜨린다. 

나쁜 코드가 쌓일수록 팀 생산성은 떨어진다. 

기한을 맞추려면 나쁜 코드를 양산할 수밖에 없다고 느끼지만 실제로는 나쁜 코드를 양산하면 기한을 맞추지 못한다.

오히려 엉망진창인 상태로 인해 속도가 곧바로 늦어지고 결국 기한을 놓친다.

기한을 맞추는 유일한 방법은, 빨리 가는 유일한 방법은 언제나 코드를 최대한 깨끗하게 유지하는 습관이다.

비야네: 깨끗한 코드 == 보기에 즐거운 코드. == 세세한 사항까지 꼼꼼하게 처리하는 코드

깨끗한 코드란 한 가지를 잘한다. 한 가지에 집중.

그래디 부치: 깨끗한 코드 == 가독성이 좋은 코드. 

데이브 토마스: 깨끗한 코드 == 다른 사람이 고치기 쉬운 코드

아무리 가독성이 높아도 테스트 케이스가 없으면 깨끗하지 않음. 작을수록 좋다.

마이클 페더스: 깨끗한 코드 == 주의 깊게 시간을 들여 깔끔하고 단정하게 정리한 코드. 세세한 사항까지 꼼꼼하게 신경 쓴 코드.

론 제프리스: 중복을 피하라. 한 기능만 수행하라. 제대로 표현하라. 작게 추상화하라.

워드 커닝햄: 코드를 읽으면서 놀랄 일이 없어야 깨끗한 코드. 읽으면서 짐작하는 대로 돌아가는 코드. 명백하고 단순해 마음이 끌리는 코드. 코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드.

 

2. 의미 있는 이름

의도를 분명히 밝혀라.

변수나 함수, 클래스 이름은 다음과 같은 질문에 답해야 한다.

변수(혹은 함수, 클래스)의 존재 이유는? 수행 기능은? 사용 방법은? ( 따로 주석이 필요하면 의도를 분명히 드러내지 못했음)

그릇된 정보를 피하라

예를 들어 여러 계정을 그룹으로 묶을 때, 실제 List가 아니면 accountList라 명명하지 않는다.

Accounts, accountGroup, bunchoOfAccounts 같이 명명한다.

서로 흡사한 이름을 사용하지 않도록 주의한다.

의미 있게 구분하라

읽는 사람이 차이를 알도록 이름을 지어야 한다. 예를 들어 moneyAmount와 money는 구분이 안된다. customer와 customerInfo, accountData와 account, theMessage와 meessage는 구분이 안된다. 

컴파일러를 통과할 지라도 연속된 숫자를 붙이거나 의미 없는 단어를 붙이지 마라.

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

예) genymdhms (generate year, moth, day, hour minute, second)라는 이름 대신에 generationTimeStamp 같은 이름을 사용하자.

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

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다는 문제점이 있다.

// case 1
int[] t = new int[34];
int s = 0;
for (int j = 0; j < 34; j++) {
    s += (t[j] * 4) / 5;
}

// case 2
int realDayPerIdealDay = 4;
int WORK_DAYS_PER_WEEK = 5;
int NUMBER_OF_TASKS = 34;
int[] taskEstimate = new int[NUMBER_OF_TASKS];
int sum = 0;
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
    int realTaskDays = taskEstimate[i] * realDayPerIdealDay;
    int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
    sum += realTaskWeeks;
}

case 1과 case 2를 보면 그냥 5를 사용 시 5가 들어가는 이름을 모두 찾아 의미를 분석해 원하는 상수를 가려내야 하는 문제점이 있기 때문에 아래와 같이 작성하는 것이 좋다.

 

인코딩을 피하라

 * 헝가리식 표기법

 : 자바 프로그래머는 변수 이름에  타입을 인코딩할 필요가 없다. 헝가리식 표기법이나 기타 인코딩 방식이 오히려 방해가 되며 변수, 함수, 클래스 이름이나 타입을 바꾸기가 어려워지고 읽기도 어려워진다. 

 * 멤버 변수 접두어

 : 멤버 변수에 m_이라는 접두어 붙일 필요도 없다. 클래스와 함수는 접두어가 필요 없을 정도로 작아야 마땅하다.

또한 멤버 변수를 다른 색상으로 표시하더나 눈에 띄게 보여주는 IDE를 사용해야 마땅하다.

* 인터페이스 클래스와 구현 클래스

 : 인터페이스 클래스 이름과 구현 클래스 이름 중 하나를 인코딩해야 한다면 구현 클래스 이름을 택한다.

예 ) 도형을 생성하는 abstract factory 생성 시 인터페이스 이름과 구현 클래스 이름을 정할 때, 인터페이스는 ShapeFactory로, 구현 클래스는 ShapeFactoryImpl 같이 정한다. 인터페이스에 IShapeFactory 같이 접두어를 붙이지 않는 편이 좋다.

주의를 흐트리고 과도한 정보를 제공하기 때문이다.

 

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

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.

문자 하나만 사용하는 변수 이름은 문제가 있다. 루프에서 반복 횟수를 세는 변수 i, j k (l은 안됨)은 루프 범위가 아주 작고 다른 이름과 충돌하지 않을 때에만 괜찮다. 

명료함이 최고이다. 남들이 이해하기 쉬운 코드를 작성하자.

* 클래스/객체 이름 : 명사나 명사구. Customer, WikiPage, Account 등이 좋은 예이고, Manager, Processor, Data, Info 같은 단어는 피하고 동사는 사용하지 않는다

* 메서드 이름 : 동사나 동사구. postPayment, deletePage, save 등이 좋은 예이며 접근자, 변경자, 조건자는 javabean 표준에 따라 앞에 set, get, is를 붙인다.

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

예 ) 정적 팩토리 메서드를 사용하는 위 코드가 아래 코드보다 좋다. 생성자 사용을 제한하려면 해당 생성자를 private로 선언한다.

// Good
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
// Bad
Complex fulcrumPoint = new Complex(23.0);

기발한 이름은 피하라.

특정 문화에서만 사용하는 농담은 피하는 편이 좋다. 의도를 분명하고 솔직하게 표현하라.

 

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

추상적인 개념 하나에 단어 하나를 선택해 이를 고수해라.

예를 들어 똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.

또한 동일 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스럽다. DeviceManager와 ProtocolController는 근본적으로 다른 점이 없다. 일관성 있는 어휘를 사용하라.

 

말장난을 하지 마라

: 한 단어를 두 가지 목적으로 사용하지 마라. 다른 개념에 같은 단어를 사용한다면 그것은 말장난에 불과하다.

예를 들어 지금 까지 구현안 add 메서드는 모두가 기존 값 두 개를 더하거나 이어 새로운 값을 만든다 가정할 때, 새로 작성하는 메서드는 집합에 값 하나를 추가할 때, 기존 add 메서드와 맥락이 다르므로 insert나 append라는 이름이 적당하다. 

새 메서드를 add라 부르면 이는 말장난이다.

코드를 최대한 이해하기 쉽게 짜야한다. 대충 훑어봐도 이해할 코드 작성이 목표이다. 

의미를 해독할 책임이 독자에게 있는 것이 아니라 의도를 밝힐 책임이 저자에게 있는 것이 바람직하다.

 

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

: 코드를 읽을 사람도 프로그래머이다. 그러므로 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용해도 괜찮다. 모든 이름을 해법 영역에서 가져오는 정책은 현명하지 못하다. 

예를 들어 VISITOR 패턴에 친숙한 프로그래머는 AccountVisitor라는 이름을 금방 이해한다. JobQueue 또한 마찬가지이다.

 

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

: 먼저 해법 영역에서 이름을 가져 오려한다. 하지만 적절한 프로그래머 용어가 없다면 문제 영역에서 이름을 가져온다.

의미 있는 맥락을 추가하라

스스로 의미가 명확한 이름도 있지만 대다수 이름은 그렇지 못하다. 그래서 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두어(prefix)를 붙인다.

예를 들어 firstName, lastName, street, houseNumber, city, state, zipcode라는 변수가 있을 때, 문맥상 주소라는 사실을 쉽게 알 수 있지만, state 변수 하나만 사용하면 주소 일부라는 사실을 알아 채기 힘들다. addr라는 prefix를 추가하면 맥락이 좀 더 분명해진다. Address라는 클래스를 생성하면 컴파일러에게도 분명해 지기 때문에 더 좋다.

아래 코드를 살펴보자.

// Bad
private void printGuessStatistics(char candidate, int count) {
    String number;
    String verb;
    String pluralModifier;
    if (count == 0) {  
        number = "no";  
        verb = "are";  
        pluralModifier = "s";  
    }  else if (count == 1) {
        number = "1";  
        verb = "is";  
        pluralModifier = "";  
    }  else {
        number = Integer.toString(count);  
        verb = "are";  
        pluralModifier = "s";  
    }
    String guessMessage = String.format("There %s %s %s%s", verb, number, candidate, pluralModifier );

    print(guessMessage);
}

위 Bad 코드를 끝까지 읽어 보고 나서야 number, verb, pluralModifier라는 변수 세 개가 guess statistics 메시지에 사용된다는 사실이 드러난다. 독자가 맥락을 유추해야 하는 것이 문제이다. 메서드만 보고는 세 변수 의미가 불분명하다.

또한 위 Bad 코드의 문제점은 일단 함수 가 좀 길며 세 변수를 함수 전반에서 사용한다.

함수를 작은 조각으로 쪼개고자 GuessStatisticsMessage라는 클래스를 만든 후 세 변수를 클래스에 넣는다.

// Good
public class GuessStatisticsMessage {
    private String number;
    private String verb;
    private String pluralModifier;

    public String make(char candidate, int count) {
        createPluralDependentMessageParts(count);
        return String.format("There %s %s %s%s", verb, number, candidate, pluralModifier );
    }

    private void createPluralDependentMessageParts(int count) {
        if (count == 0) {
            thereAreNoLetters();
        } else if (count == 1) {
            thereIsOneLetter();
        } else {
            thereAreManyLetters(count);
        }
    }

    private void thereAreManyLetters(int count) {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }

    private void thereIsOneLetter() {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }

    private void thereAreNoLetters() {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

그러면 세 변수는 맥락이 분명해진다. 

이렇게 맥락을 개선하자.

 

불필요한 맥락을 없애자

일반적으로 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서 이다. 이름에 불필요한 맥락을 추가하지 않도록 주의하자.

예를 들어 Address 클래스의 인스턴스로 accountAddress, customerAddress는 좋은 이름으로 적합하지 않다.

 

다른 사람이 짠 코드를 손 보면 리팩터링 도구를 사용해 문제 해결 목적으로 이름을 개선하라.

단기적 효과는 물론 장기적 이익도 보장한다.

 

3. 함수

작게 만들어라!

얼마나 작게 만들어야 하는가? : if 문/ else 문/ while 문 등에 들어가는 블록은 한 줄 이어야 한다.

대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수가 작아질 뿐 아니라, 블록 안 호출하는 함수 이름을 적절히 지으면 코드를 이해하기도 쉬워진다.

중첩 구조가 생길 만큼 함수가 커져서는 안 된다.

함수에서 들여 쓰기 기준은 1단이나 2단을 넘어서는 안된다.

그래야 함수는 읽고 이해하기 쉬워진다.

 

한 가지만 해라!

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

한 가지를 하는 게 무엇인가? 추상화 수준이 하나인 단계만 수행하면 그 함수는 한 가지 작업만 하는 것이다.

또 다른 방법은, 단순히 다른 표현이 아닌 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것이다.

 

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

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

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 

 

그러면 어떻게 함수를 작성해야 하는가?

: 내려가기 규칙으로 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와서 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아지도록 작성한다.

핵심은 짧으면서 '한 가지'만 하는 함수로 위에서 아래로 읽어내듯 코드 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.

 

switch 문

switch문은 작게 만들기 어렵다. 본질적으로 switch 문은 n가지를 처리한다.

불행하게도 switch문을 완전히 피할 방법은 없지만 다형성을 이용해 저 차원 클래스에 숨기고 절대 반복하지 않는 방법을 사용할 수 있다.

    public Money calculatePay(Employee e) throws InvalidEmployeeType {
        switch (e.type) {
            case COMMISSIONED:
                return calculateCommissionedPay(e);
            case HOURLY:
                return calculateHourlyPay(e);
            case SALARIED:
                return calculateSalariedPay(e);
            default:
                throw new InvalidEmployeeType(e.type);
        }
    }

위 함수의 문제점은 

1. 함수가 길고, 2. '한 가지' 작업만 수행하지 않으며 3. 코드 변경할 이유가 여럿이기에 SRP 위해된다. 4. 새 직원 유형 추가할 때마다 코드 변경해야 하므로 OCP 위배한다.

 

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r) ;
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

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

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

이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다. 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그다음 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.

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

includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, 등이 좋은 예이다.

 

함수 인수(argument)

: 함수에서 이상적인 인수 개수는 0개이고, 다음은 1개, 다음은 2개이다. 3개 이상은 가능한 피하는 편이 좋다.

4개 이상은 특별한 이유가 필요하며 특별한 이유가 있어도 사용하면 안 된다.

최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우이다.

 

* 많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지이다.

1. 인수에 질문을 던지는 경우

boolean fileExists("MyFile") 경우

2. 인수를 뭔가로 변환해 결과를 변환하는 경우. 예) InputStream fileOpen("MyFile")

다소 드물게 사용하지만 아주 유용한 단항 함수 형식이 이벤트이다.

이벤트 함수는 입력 인수만 있고 출력할 수 없는데, 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.  하지만 이벤트 함수는 조심해서 사용해야 하며, 이벤트라는 사실이 코드에 명확히 드러나야 한다. 

그러므로 이름과 문맥을 주의해서 선택해야 한다.

위 2가지 (3가지)를 제외하면 단항 함수는 가급적 피한다.

입력 인수를 변환하는 함수라면 변환 결과는 반환 값으로 돌려준다.

// Bad
StringBuffer transform(StringBuffer in)

// Better
void transform(StringBuffer out)

위 코드에서 볼 수 있듯, 입력 인수를 그대로 돌려주는 함수라 할지라도 변환 함수 형식을 따르는 편이 좋다.

적어도 변환 형태는 유지하기 때문이다.

 

* 플래그 인수 (boolean을 parameter로 받을 경우): 

함수로 boolean 값을 넘기는 플래그 인수를 사용하는 관례는 추하고 끔찍하다. 

함수가 한 번에 여러 가지를 처리한다고 대놓고 공표하는 것이기 때문에 true와 false로 나누어서 render(true)라는 코드는 renderForSuite()와 renderForSingleTest()로 나눠야 마땅하다.

 

* 이항 함수:

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

인수가 1개인 함수를 사용하는 것이 더 쉽게 읽히고 빨리 이해되기 때문이다. 

이항 함수가 무조건 나쁜 것은 아니다. 불가피한 경우도 생기기 때문이다. (좌표를 넘길 때처럼 적절한 경우도 있다) 하지만 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 해야 한다.

 

* 삼항 함수: 인수가 3개인 함수는 인수가 2개인 함수보다도 더 이해하기 어렵다. 따라서 만들 때 신중히 고려해야 한다.

 

* 인수 객체 

: 넘겨야 하는 argument가 2~3개 필요하면 독자적인 클래스 변수로 선언할 수 있다.

Circle makeCircle(double x, double y, double radius);

Circle makeCircle(Point center, double radius);

 

 객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질 수도 있지만, 그렇지 않은 것이 변수를 묶어 넘기려면 이름을 붙여야 하므로 개념을 표현하게 되기 때문이다.

 

* 인수 목록

: 때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메서드가 그 예이다.

public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

가변 함수 인수를 전부 동등히 취급하면 List 형 인수 하나로 취급할 수 있기 때문에 String.format은 사실상 이항 함수이다.

 

* 동사와 키워드

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

함수와 인수가 동사/명사 쌍을 이뤄야 한다.

예를 들어 write(name)보다 writeField(name)이 더 나은 이름이다.

또한 함수 이름이 키워드를 추가하는 형식이다.

예를 들어, assertEquals 보다 assertExpectedEqualsActual(expected, actual)이 더 좋다.

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

부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고 남몰래 다른 짓도 하기 때문이다.

public class UserValidator {
    private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) {
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codedPhrase, password);
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}

위 코드에서는 Session.initialize()라는 부수 효과를 일으킨다.

이는 이름 그대로 함수를 확인하고, 이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다.

따라서 함수 이름만 보고 함수 호출하는 사용자는 사용자를 인증하며 기존 세션 정보를 지워버릴 위험에 처한다.

물론 함수가 '한 가지'만 한다는 규칙을 위반하지만 checkPasswordAndInitializeSession이라는 이름이 훨씬 좋다.

 

* 출력 인수

일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

예를 들어 appendFooter(s); 의 경우 무언가에 s를 바닥글로 첨부하는지, 아니면 s에 바닥글을 첨부하는지, 인수 s는 입력인지 출력인지 모르기 때문에 report.appendFooter()와 같이 호출하는 방식이 좋다.

 

명령과 조회를 분리하라

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

객체 상태를 변경하거나 아니면 객체 정보를 반환하거나.

둘 다 하면 혼란을 초래한다.

public boolean set(String attribute, String value);

위 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 T, 실패 시 F를 반환한다.

그래서 다음과 같이 이상한 코드가 나온다.

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

함수 호출 코드만 보면 의미가 모호하다. username 속성이 unclebob으로 설정되면 이라고 읽히지 설정하는 데 성공하면..이라고 읽히지 않는다. 그래서 다음과 같이 명령과 조회를 분리해 혼란을 애초에 뿌리 뽑아야 한다.

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

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

명령 함수에서 오류 코드를 반환하는 방식은 명령 / 조회 분리 규칙을 미묘하게 위반한다. 

자칫하면 if문에서 명령을 표현식으로 사용하기 쉽기 때문이다.

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){
            logger.log("page deleted");
        }else{
            logger.log("configKey not deleted");
        }
    }else{
        logger.log("deleteReference from registry failed");
    }
}else{
    logger.log("delete failed");return E_ERROR;
}

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

    public void delete(Page page) {
        try {
            deletePageAndAllReferences(page);
        } catch (Exception e) {
            logError(e);
        }
    }

    private void deletePageAndAllReferences(Page page) throws Exception {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    }

    private void logError(Exception e) {
        logger.log(e.getMessage());
    }

* Try/Catch 블록 뽑아내기

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

그러므로 위와 같이 별도 함수로 try/catch 블록을 뽑아내는 편이 좋다.

 

*오류 처리도 한 가지 작업이다.

함수는 '한 가지' 작업만 해야 하는데, 오류 처리도 '한 가지' 작업에 속한다.

따라서 위의 delete 메서드처럼 오류를 처리하는 함수는 오류만 처리해야 한다. 

 

* Error.java 의존성 자석

오류 코드를 반환한다는 이야기는 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻이다.

public enum Error { 
	OK,
	INVALID,
	NO_SUCH,
	LOCKED,
	OUT_OF_RESOURCES, 	
	WAITING_FOR_EVENT;
}

다른 클래스에서 Error enum을 import 해 사용해야만 하므로, 위와 같은 클래스는 의존성 자석이다.

즉 위 enum이 변하면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 한다.

그래서 Error 클래스 변경이 어려워진다.

반면 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

 

반복하지 마라!

중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제거할 목적으로 나왔다.

구조적 프로그래밍

함수를 작게 만들면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.

오히려 때로는 single entry-exit rule보다 의도를 표현하기가 쉬워진다. (데이크스트라에 의한 함수는 return문이 하나여야 하며 루프 안에서 break나 continue를 사용하지 말며 goto를 사용하지 말아야 한다는 것)

하지만 goto 문은 작은 함수에서는 피해야만 한다.

-함수를 짜는 방법

글짓기와 비슷하다. 처음에는 길고 복잡하다. 들여 쓰기 단계도 많고 중복된 루프도 많고 인수 목록도 아주 길다.

하지만 단위 테스트 케이스를 만들어 코드를 다듬고, 함수를 만들고, 이름을 바꾸고 중복을 제거하며 메서드를 줄이고 순서를 바꾼다.

때로는 클래스를 쪼개기도 하지만 단위 테스트는 통과해야 한다.

 

4. 주석

주석은 사실상 기껏해야 필요악이다.

주석은 언제나 실패를 의미한다. 프로그래밍 언어 자체가 표현력이 풍부하면 , 프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면 주석은 거의 필요가 없다. 아니, 전혀 필요가 없다.

표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.

 

5. 형식 맞추기

프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야한다.

코드 형식은 중요하다. 코드의 가독성은 앞으로 바뀔 코드 품질에 지대한 영향을 미치기 때문이다.

적절한 행 길이를 유지하라.

500줄을 넘어가지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다.

 

* 신문기사처럼 작성하라. 

이름은 간단하면서도 설명이 가능하게 짓는다. 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경 써서 짓는다. 

소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명하고 아래로 내려갈수록 의도를 세세하게 묘사한다.

마지막에는 가장 저 차원의 함수와 세부 내역이 나온다.

 

* 개념은 빈 행으로 분리하라

생각 사이에는 빈 행을 넣어 분리하듯, 패키지 선언부, import 문, 각 함수 사이에 빈 행이 들어간다.

빈 행은 새로운 개념을 시작한다는 시각적 단서이다.

// 빈 행을 넣지 않을 경우
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
	public static final String REGEXP = "'''.+?'''";
	private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
		Pattern.MULTILINE + Pattern.DOTALL);
	public BoldWidget(ParentWidget parent, String text) throws Exception {
		super(parent);
		Matcher match = pattern.matcher(text); match.find(); 
		addChildWidgets(match.group(1));}
	public String render() throws Exception { 
		StringBuffer html = new StringBuffer("<b>"); 		
		html.append(childHtml()).append("</b>"); 
		return html.toString();
	} 
}
// 빈 행을 넣을 경우
package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
	public static final String REGEXP = "'''.+?'''";
	private static final Pattern pattern = Pattern.compile("'''(.+?)'''", 
		Pattern.MULTILINE + Pattern.DOTALL
	);
	
	public BoldWidget(ParentWidget parent, String text) throws Exception { 
		super(parent);
		Matcher match = pattern.matcher(text);
		match.find();
		addChildWidgets(match.group(1)); 
	}
	
	public String render() throws Exception { 
		StringBuffer html = new StringBuffer("<b>"); 
		html.append(childHtml()).append("</b>"); 
		return html.toString();
	} 
}

* 세로 밀집도

줄 바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다.

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

// 의미없는 주석으로 변수를 떨어뜨려 놓아서 한눈에 파악이 잘 안된다.

public class ReporterConfig {
	/**
	* The class name of the reporter listener 
	*/
	private String m_className;
	
	/**
	* The properties of the reporter listener 
	*/
	private List<Property> m_properties = new ArrayList<Property>();
	public void addProperty(Property property) { 
		m_properties.add(property);
	}
// 의미 없는 주석을 제거함으로써 코드가 한눈에 들어온다.
// 변수 2개에 메소드가 1개인 클래스라는 사실이 드러난다.

public class ReporterConfig {
	private String m_className;
	private List<Property> m_properties = new ArrayList<Property>();
	
	public void addProperty(Property property) { 
		m_properties.add(property);
	}

* 수직거리

함수 연관 관계와 동작 방식을 이해하려고 조각조각을 어디에 있는지 찾고 기억하느라 시간과 노력이 소모한다.

서로 밀접한 개념은 세로로 가까이 놔둬야 한다.

물론 두 개념이 서로 다른 파일에 속하면 규칙이 통하지 않지만, 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. protected변수를 피해야 하는 이유 중 하나가 이것이다.

같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다.

여기서 연관성이란 한 개념을 이해하는 데 다른 개념이 중요한 정도이다.

연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다.

 

변수 선언 시 변수는 사용하는 위치에 최대한 가까이 선언한다.

루프를 제어하는 변수는 흔히 루프 문 내부에 선언한다.

 

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

변수 간에 세로 거리를 두지 않는다. 잘 설계한 클래스는 클래스의 많은(혹은 대다수) 메서드가 인스턴스 변수를 사용하기 때문이다.

 

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

또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
그러면 프로그램이 자연스럽게 읽힌다.

 

개념의 유사성. 어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다.

친화도가 높을수록 코드를 가까이 배치한다.

친화도가 높은 요인은 여러 가지인데, 앞서 보았듯 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예이다.

변수와 그 변수를 사용하는 함수도 한 예다.

또한 비슷한 동작을 수행하는 일군의 함수가 좋은 예이다.

// 같은 assert 관련된 동작들을 수행하며, 명명법이 똑같고 기본 기능이 유사한 함수들로써 개념적 친화도가 높다.
// 이런 경우에는 종속성은 오히려 부차적 요인이므로, 종속적인 관계가 없더라도 가까이 배치하면 좋다.

public class Assert {
	static public void assertTrue(String message, boolean condition) {
		if (!condition) 
			fail(message);
	}

	static public void assertTrue(boolean condition) { 
		assertTrue(null, condition);
	}

	static public void assertFalse(String message, boolean condition) { 
		assertTrue(message, !condition);
	}
	
	static public void assertFalse(boolean condition) { 
		assertFalse(null, condition);
	}

위 함수들은 개념적인 친화도가 매우 높다. 명명법이 같고 기본 기능이 유사하고 간단하기 때문이다.

종속적인 관계가 없더라도 가까이 배치할 함수들이다.

 

* 세로 순서

일반적으로 함수 호출 종속성은 아래 방향으로 유지한다.

호출되는 함수를 호출하는 함수보다 나중에 배치한다.

그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.

 

가로 형식 맞추기

한 행은 가로로 얼마나 길어야 적당할까?

짧은 행이 바람직하다. 100자나 120자에 달해도 나쁘지 않다. 하지만 그 이상은 솔직히 주의 부족이다.

개인적으로 120자 정도로 행 길이를 제한한다.

 

* 가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

private void measureLine(String line) { 
	lineCount++;
	
	// 흔히 볼 수 있는 코드인데, 할당 연산자 좌우로 공백을 주어 왼쪽,오른쪽 요소가 확실하게 구분된다.
	int lineSize = line.length();
	totalChars += lineSize; 
	
	// 반면 함수이름과 괄호 사이에는 공백을 없앰으로써 함수와 인수의 밀접함을 보여준다
	// 괄호 안의 인수끼리는 쉼표 뒤의 공백을 통해 인수가 별개라는 사실을 보여준다.
	lineWidthHistogram.addLine(lineSize, lineCount);
	recordWidestLine(lineSize);
}

또한 연산자 우선순위를 강조하기 위해서도 공백을 사용한다.

private static double determinant(double a, double b, double c) {
    return b * b - 4 * a * c;
}

위 코드보다 아래 코드가 읽기가 편하다. (도구에서 없애는 경우가 흔하지만)

private static double determinant(double a, double b, double c) {
    return b*b - 4*a*c;
}

* 가로 정렬

public class FitNesseExpediter implements ResponseSender {
	private		Socket		  socket;
	private 	InputStream 	  input;
	private 	OutputStream 	  output;
	private 	Reques		  request; 		
	private 	Response 	  response;	
	private 	FitNesseContex	  context; 
	protected 	long		  requestParsingTimeLimit;
	private 	long		  requestProgress;
	private 	long		  requestParsingDeadline;
	private 	boolean		  hasError;

위 코드는 선언부를 읽다 보면 변수 유형은 무시하고 변수 이름부터 읽게 된다.

위와 같이 정렬하지 않으면 오히려 중대한 결함을 찾기 쉽다. 

정렬이 필요할 정도로 목록이 길면 문제는 목록 길이지 정렬 부족이 아니다.

따라서 아래 코드처럼 작성해야 한다.

public class FitNesseExpediter implements ResponseSender {
    private Socket socket;
    private InputStream input;
    private OutputStream output;
    private Reques request;
    private Response response;
    private FitNesseContex context;
    protected long requestParsingTimeLimit;
    private long requestProgress;
    private long requestParsingDeadline;
    private boolean hasError;
}

* 들여 쓰기

우리는 범위로 이뤄진 계층을 표현하기 위해 코드를 들여 쓴다.

들여 쓰는 정도는 계층에서 코드가 자리 잡은 수준에 비례한다.

클래스 정의처럼 파일 수준인 문장은 들여 쓰지 않는다.

클래스 내 메서드는 클래스보다 한 수준 들여 쓴다.

메서드 코드는 메서드 선언보다 한 수준 들여 쓴다.

블록 코드는 블록을 포함하는 코드보다 한 수준 들여 쓴다.

이런 코드가 속하는 범위를 시각적으로 표현함으로써 범위별 이동이 쉬워진다.

 

때로 간간한 if문, 짧은 while문, 짧은 함수에서 들여 쓰기 규칙을 무시하고픈 유혹이 생기지만 들여 쓰기를 넣는다

 

*가짜 범위

때로는 빈 while문이나 for문을 접한다. 가능한 피하려 애쓰지만 피하지 못할 경우 올바로 들여 쓰고 괄호로 감싼다.

세미콜론은 새 행에다 제대로 들여 써서 넣어준다.

 

팀 규칙

팀에 속한다면 자신이 선호해야 할 규칙은 팀 규칙이다.

팀은 한 가지 규칙에 합의해야 하며 모든 팀원은 그 규칙을 따라야 한다.

그래야 소프트웨어가 일관적인 스타일을 보인다.

 

6. 객체와 자료구조

변수를 비공개 private로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶기 때문이다. 

충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서이다.

그렇다면 어째서 수많은 프로그래머가 get함수와 set 함수를 당연하게 public으로 공개해 private 변수를 외부에 노출할까

 

자료 추상화

구체적인 Point 클래스 : 

public class Point { 
  public double x; 
  public double y;
}

추상적인 Point 클래스 :

public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y); 
  double getR();
  double getTheta();
  void setPolar(double r, double theta); 
}

위 구체적인 Point 클래스와 추상적인 Point 클래스의 차이점은, 추상적인 Point 클래스는 자료구조를 명확히 한다는 점이다. 추상적인 Point 클래스는 극좌표(polar coordinate)인지 직교좌표(cartesian coodrinate)인지 알 방법이 없다.

자료구조를 명확히 할 뿐만 아니라 클래스 메서드가 접근 정책을 강제한다. 좌표 설정 시 두 값을 한 번에 설정해야 하고 읽을 때는 개별적으로 읽어야 하기 때문이다. 

반면 구체적인 Point 클래스는 확실히 직교좌표(cartesian coordinate)를 사용하며 개별적 좌표값을 읽고 설정하게 강제한다. 구현을 노출하며 private로 변수를 선언하더라도 getter와 setter를 제공하면 구현을 외부로 노출하는 셈이다.

추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.

따라서 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다. 인터페이스나 조회/설정 함수만으로는 추상화가 이루어지지 않는다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 하며 아무 생각 없이 get, set 함수를 추가하는 방법이 가장 나쁘다.

 

자료/객체 비대칭

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

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

객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉽고, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.

복잡한 시스템을 짜다보면 새로운 함수가 아닌 새로운 자료 타입이 필요한 경우가 생기는데, 이때는 클래스와 객체 지향 기법이 가장 적합하지만 새로운 자료 타입이 아닌 새로운 함수가 필요한 경우도 있는데, 이때는 절차적인 코드와 자료구조가 좀 더 적합하다.

때로는 단순한 자료구조와 절차적인 코드가 가장 적합한 상황도 있다.

 

디미터 법칙

디미터 법칙은 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다. 위에서 봤듯, 객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안 되는 의미다.

좀 더 정확히 표현하자면, 디미터 법칙은 "클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다"라고 주장한다.

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

하지만 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다.

다시 말해, 낯선 사람은 경계하고 친구랑만 놀라는 의미이다.

 

중요한 것은 자료구조와 객체로 개념을 분리시켜야 한다는 것이다.

객체는 동작을 공개하고 자료를 숨긴다.

그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.

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

시스템을 구현 시, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료구조와 절차적인 코드가 더 적합하다.

우수한 소프트웨어 개발자는 편견 없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.

 

7. 오류 처리

오류 처리는 중요하지만 오류 처리 코드로 인해 로직을 이해하기 어려워지면 클린 코드라 부르기 어렵다.

 

오류코드보다 예외를 사용하라는 것은 앞에서 다루었다.

Unchecked Exception을 사용하라

Unchecked Exception은 Runtime Exception 하위 클래스들을 의미한다.

checked exception을 사용하면 throws 경로에 위치하는 모두 함수가 최하위 함수에서 던지를 예외를 알아야 하므로 캡슐화가 깨진다.

 

예외에 의미를 제공하라

오류 메세지에 정보를 담아 예외와 함께 전지며 실패한 연산 이름과 실패 유형도 언급한다.

애플리케이션이 로깅 기능을 사용하면 catch 블록에서 오류를 기억하도록 충분한 정보를 넘겨준다.

 

Null은 전달하지도 반환하지도 말라.

 

9. 단위 테스트

깨끗한 테스트 코드 유지하기

테스트 코드는 실제 코드 못지않게 중요하다. 테스트 코드는 사과와 설계와 주의가 필요하다.

실제 코드 못지않게 깨끗하게 작성해야 한다.

 

* 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다.

테스트 케이스가 있으면 변경이 두렵지 않기 때문이고 공포가 사실상 사라지기 때문이다.

따라서 실제 코드를 점검하는 자동화된 단위 테스트 suite는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠이다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다. 테스트 케이스가 있으면 변경이 쉬워지기 때문이다.

 

그럼 어떻게 깨끗한 테스트 코드를 만들까

가독성이 핵심이다.

어쩌면 가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다.

테스트 코드에서 가독성을 높이려면 여느 코드와 마찬가지로 명료성, 단순성, 풍부한 표현력이 필요하다.

깨끗한 테스트 코드에는 BUILD-OPERATE-CHECK 패턴이 테스트 구조에 적합하다.

첫 부분은 테스트 자료를 만든다. 두 번째 부분은 테스트 자료를 조작하며, 세 번째 부분은 조작한 결과가 올바른지 확인한다.

잡다하고 세세한 코드는 거의 다 없애며 테스트 코드 본론에 돌입해 진짜 필요한 자료 유형과 함수만 사용한다.

그러므로 코드를 읽는 사람은 온갖 잡다하고 세세한 코드에 헷갈리지 않고 코드가 수행하는 기능을 재빨리 이해한다.

 

* 이중 표준

@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
  hw.setTemp(WAY_TOO_COLD); 
  controller.tic(); 
  assertTrue(hw.heaterState());   
  assertTrue(hw.blowerState()); 
  assertFalse(hw.coolerState()); 
  assertFalse(hw.hiTempAlarm());       
  assertTrue(hw.loTempAlarm());
}

위 코드는 세사한 사항이 아주 많다. 이것의 가독성을 다음과 같이 증가시킬 수 있다.

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
  wayTooCold();
  assertEquals("HBchL", hw.getState()); 
}

이상한 tic함수는 wayTooCold라는 함수로 만들어 숨겼고 assertEquals 함수 내 문자열로 대문자/소문자로 on/off로 구분해 테스트 코드를 쉽게 이해할 수 있다.

 

테스트 당 assert 하나

Junit으로 테스트 코드를 짤 때는 함수마다 assert문을 단 하나만 사용해야 한다고 주장하는 학파가 있지만, 확실히 장점이 있다. assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.

public void testGetPageHierarchyAsXml() throws Exception { 
  givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
  
  whenRequestIsIssued("root", "type:pages");
  
  thenResponseShouldBeXML(); 
}

public void testGetPageHierarchyHasRightTags() throws Exception { 
  givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
  
  whenRequestIsIssued("root", "type:pages");
  
  thenResponseShouldContain(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
  ); 
}

위 함수 이름을 바꿔 given-when-then 이라는 관례를 사용하면 테스트 코드를 읽기가 쉬워진다.

하지만 테스트를 분리하면 중복되는 코드가 많아지는데, 이는 TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다. given/when 부분을 부모 클래스에 두고 then 부분은 자식 클래스에 두면 된다.

아니면 @Before 함수에 given/when을 넣고 @Test 함수에 then을 넣어도 된다. 

하지만 배보다 배꼽이 더 커질 수도 있고 이것저것 감안해 보면 여러 assert문을 사용하는 것이 나을 수도 있다.

assert 문 개수는 최대한 줄여야 좋지만 때로는 함수 하나에 여러 assert문을 넣기도 한다.

 

* 테스트 당 개념 하나

어쩌면 테스트 함수마다 한 개념만 테스트하라는 규칙이 낫다.

잡다한 개념을 연속으로 테스트하는 함수는 피한다. 

따라서 가장 좋은 규칙은 "개념 당 assert 문 수를 최소로 줄여라", "테스트 함수 하나는 개념 하나만 테스트하라"

 

깨끗한 테스트의 다섯 가지 규칙 F.I.R.S.T

  • Fast. 테스트는 빨리 돌아야 한다. 느리면 자주 못 돌리고 자주 못 돌리면 초반 문제를 찾아내지 못한다.
  • Independent. 각 테스트는 서로 의존하면 안 된다. 각 테스트는 독립적이고 어떤 순서로 실행해도 괜찮아야 한다.
  • Repeatable. 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA 환경, 네트워크 연결 없이도 실행할 수 있어야 한다. 
  • Self-Validating. 테스트는 boolean 값으로 결과를 내야 한다. 실패 아니면 성공. 통과 여부를 알리려고 로그파일을 읽게 해서는 안된다.
  • Timely. 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

 

10. 클래스

클래스 체계

클래스를 정의하는 표준 자바 관례에 따르면, 가장 먼저 변수 목록이 나온다.

static public 상수가 있다면 맨 처음에 나온다. 다음 static private 변수가 나오며 이어서 private 인스턴스 변수가 나온다.

public 변수가 필요한 경우는 거의 없다.

변수 목록 다음에는 공개 함수가 나온다. 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다.

즉, 추상화 단계가 순차적으로 내려간다. 그래서 프로그램은 신문 기사처럼 읽힌다.

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다.

때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다.

하지만 private 상태를 유지할 온갖 방법을 강구한다. 캡슐화를 풀어주는 결정은 언제나 최후 수단이다.

클래스는 작아야 한다.

클래스를 첫째 규칙은 크기이며 두 번째 규칙도 크기이다. 더 작아야 한다.

그러면 얼마나 작아야 하는가?

함수는 물리적인 행 수로 크기를 측정했지만 클래스는 맡은 책임을 센다.

클래스 이름은 해당 클래스 책임을 기술해야 한다. 실제로 작명은 클래스 크기를 줄이는 첫 번째 관문이다.

클래스 설명은 if, and, or, but을 사용하지 않고 25 단어 내외로 가능해야 한다.

 

* 단일 책임 원칙(SRP)

클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.

SRP는 '책임'이라는 개념을 정의하며 적절한 클래스 크기를 제시한다.

클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미이다.

책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.

SRP는 객체 지향 설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이기도 하지만 SRP는 클래스 설계자가 가장 무시하는 규칙 중 하나이다.

왜냐하면 깨끗하고 체계적인 소프트웨어보다 돌아가는 소프트웨어에 초점을 맞추기 때문이다.

관심사를 분리하는 작업은 프로그램만이 아니라 프로그래밍 활동에서도 마찬가지로 중요하다.

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

하지만 큰 다목적 클래스 몇 개로 이루어진 시스템은 변경을 가할 때 당장 알 필요가 없는 사실까지 들이밀어 독자를 방해한다.

큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.

작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

 

* 응집도

클래스는 인스턴스 변수 수가 적어야 한다.

각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다. 일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다. 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 더 높다.

일반적으로 이처럼 응집도가 가장 높은 클래스는 가능하지도, 바람직하지도 않지만 그래도 우리는 응집도가 높은 클래스를 선호한다.

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

'함수를 작게, 매개변수 목록을 짧게'라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.

응집도가 높아질수록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.

 

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

큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.

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

전혀 아니다. 만약 네 변수를 클래스 인스턴스 변수로 승격시키면 새 함수는 인수가 전혀 필요 없다.

그만큼 함수를 쪼개기 쉬워진다.

하지만 이렇게 하면 클래스가 응집력을 잃는다. 몇몇 함수만 사용하는 인스턴스 변수가 점점 늘어나기 때문이다.

그런데 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리해도 된다. 

따라서 클래스가 응집력을 잃으면 쪼개라!

그래서 큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다. 그러면서 프로그램에 점점 더 체계가 잡히고 구조가 투명해진다.

 

 

의도를 명확히 표현하려면 조건문을 캡슐화해야 한다! 즉 조건문을 메서드로 뽑아내 적절한 이름을 붙인다.

 

자바)

J1. 긴 import 목록을 피하고 와일드카드를 사용하라

긴 import 목록은 읽기에 부담스럽다. 

J2. 상수는 상속하지 않는다.

상수를 상속을 통해 숨기지 말고 static import를 사용하라.

J3. 상수 대 Enum

public static final int 대신 enum을 사용해라. 훨씬 더 유연하고 서술적인 강력한 도구이다.

 

Reference : 

클린코드 애자일 소프트웨어 장인정신 (인사이트)

https://github.com/Yooii-Studios/Clean-Code

 

GitHub - Yooii-Studios/Clean-Code: Clean Code 스터디의 결과물 정리를 위해 만든 저장소입니다.

Clean Code 스터디의 결과물 정리를 위해 만든 저장소입니다. . Contribute to Yooii-Studios/Clean-Code development by creating an account on GitHub.

github.com

 

'Programming' 카테고리의 다른 글

스트랭글러 패턴  (0) 2023.05.31
Clean Code - 냄새와 휴리스틱 (일반)  (0) 2022.11.01
질문을 잘 하는 개발자가 되자  (2) 2022.10.28
마크다운 연습  (0) 2022.01.03
parameter와 argument의 차이  (0) 2021.09.30
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday