1 ~ 3 : 깨끗한 코드, 의미 있는 이름, 함수

1장 : 깨끗한 코드

“사소한 곳에서 발휘하는 정직은 사소하지 않다.”

품질은 하늘에서 뚝 떨어진 위대한 방법론이 아니라 사심 없이 기울이는 무수한 관심에서 얻어진다.

장인 정신을 익히는 과정은 두 단계로 나뉘는데, 바로 이론과 실전이다.

  • 첫째, 장인에게 필요한 원칙, 패턴, 기법, 경험이라는 지식을 습득해야 한다.

  • 둘째, 열심히 일하고 연습해 지식을 몸과 마음으로 체득해야 한다.

코드가 존재하리라

  • 기계가 실행할 정도로 상세하게 요구사항을 명시하는 작업이 프로그래밍이고, 이렇게 명시한 결과가 바로 코드다. 궁극적으로 코드는 요구사항을 표현하는 언어이다.

나쁜 코드

  • ✨ 르브랑의 법칙: 나중은 결코 오지 않는다.

나쁜 코드로 치르는 대가

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

  • 원대한 재설계의 꿈

    • 시간을 들여 깨끗한 코드를 만드는 노력이 비용을 절감하는 방법일 뿐만 아니라 전문가로서 살아남는 길이다.

  • 태도

    • 좋은 코드를 사수하는 일은 프로그래머들의 책임다.

  • 원초적 난제

    • 빨리 가는 방법은, 코드를 최대한 깨끗하게 유지하는 습관이다.

  • 깨끗한 코드라는 예술?

    • ‘코드 감각’이 있는 프로그래머는 나쁜 모듈을 보면 좋은 모듈로 개선할 방안을 떠올린다.

  • 깨끗한 코드란 ?

바야네 스트롭스트룹

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

그래디 부치

깨끗한 코드는 단순하고 직접적인다. 깨끗한 코드는 잘 쓴 문장처럼 읽힌다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.

‘big’ 데이브 토마스

깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 하나만 제공한다. 의존성은 최소이며 각 의존성을 명확히 정의한다. API는 명확하며 최소로 줄였다.

마이클 페더스

깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 손 댈 곳이 없다.

론 제프리스
  • 모든 테스트를 통과한다.

  • 중복이 없다.

  • 시스템 내 모든 설계 아이디어를 표현한다.

  • 클래스, 메서드, 함수 등을 최대한 줄인다.

워드 커닝햄

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. 코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다.

우리들 생각

깨끗한 변수 이름, 깨끗한 함수, 깨끗한 클래스를 만드는 방법을 소개한다.

우리는 저자다

코드를 읽는 시간 대 코드를 짜는 시간 비율이 10대 1이 넘는다. 그러므로 읽기 쉬운 코드는 매우 중요하다.

보이스카우트 규칙

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라

지속적인 개선이야말로 전문가 정신의 본질이다.

2장 : 의미 있는 이름

의도를 분명히 밝혀라

의도가 분명하게 이름을 지으라.

코드 맥락이 코드 자체에 명시적으로 드러나야 한다.

그릇된 정보를 피하라

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

유사한 개념은 유사한 표기법을 사용한다.

의미 있게 구분하라

읽는 사람이 차이를 알도록 이름을 지어라.

  • ex. moneyAmount & money, ProductInfo & ProductData

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

프로그래밍은 사회 활동이기 때문에, 발음하기 쉬운 이름은 중요하다.

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

문자 하나를 사용하는 이름과 상수는 쉽게 눈에 띄지 않는다. 변수나 상수를 코드 여러 곳에서 사용한다면 검색하기 쉬운 이름이 바람직하다. 이름 길이는 범위 크기에 비례해야 한다.

  • ex. WORK_DAYS_PER_WEEK vs 5

인코딩

  • 헝가리식 표기법

    • 예전에는 컴파일러가 타입을 점검하지 않아 타입을 기억할 단서로 헝가리식 표기법을 사용했다. 그러나 지금은 변수 이름에 타입을 인코딩할 필요가 없다.

  • 멤버 변수 접두어

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

클래스 이름

클래스 이름과 객체 이름은 명사나 명사구가 적합하다.

메서드 이름

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

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

생성자(Constructor)를 중복정의(overload)할 때는 정적 팩토리 메서드를 사용한다. 생성자 사용을 제한하려면 해당 생성자를 private로 선언한다.

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

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다. 일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.

example

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

말장난을 하지 마라

한 단어를 두 가지 목적으로 사용하지 마라. 프로그래머는 코드를 최대한 이해하기 쉽게 짜야 한다.

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

기술 개념에는 기술 이름이 가장 적합한 선택이다.

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

적절한 ‘프로그래머 용어’가 없다면 문제 영역에서 이름을 가져온다.

해법 영역과 문제 영역을 구분할 줄 알아야 한다.

의미 있는 맥락을 추가하라

대다수의 이름은 스스로 의미가 분명하지 않으므로 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 맥락을 개선하면 함수를 쪼객기가 쉬워지므로 알고리즘도 더 명확해진다.

불필요한 맥락을 없애라

이름에 불필요한 맥락을 추가하지 않도록 주의한다.

ex. GSDAccountAddress, accountAddress, customerAddress → Address

3장 : 함수

작게 만들어라

함수를 만드는 첫째 규칙은 ‘작게!’다. 함수를 만드는 둘째 규칙은 ‘더 작게!’다.

  • 블록과 들여쓰기

    • if문 / else 문 / while 문 등에 들어가는 블록은 한 줄이어야 한다.

한 가지만 해라 !

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

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

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

함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

✔️ 위에서 아래로 코드 읽기 : 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 즉, 위에서 아래로 프로그램을 읽으면 함ㅅ 추상화 수준이 한 번에 한 단계씩 낮아진다. 이것이 내려가기 규칙이다.

Switch 문

switch 문을 피할 수 없는 경우 다형성을 이용하여 각 witch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.

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

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

길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

이름 붙일 때는 일관성이 있어야 한다.

함수 인수

함수에서 이상적인 인수 개수는 0개다. 다음은 1개고, 다음은 2개다. 3개는 가능한 피하는 편이 좋다. 4개 이상은 특별한 이유가 필요하다. 4개 이상은 특별한 이유가 있어도 사용하면 안 된다.

많이 쓰는 단항 형식 - 함수에 인수 1개를 넘기는 이유

  • 인수에 질문을 던지는 경우

  • 인수를 뭔가로 변환해 결과를 반환하는 경우

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

플래그 인수

  • 함수로 부울 값을 넘기는 관계는 정말로 끔찍한데, 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하기 때문이다.

이항 함수

삼항 함수

  • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 순거, 주춤, 무시로 야기되는 문제가 두 배 이상 늘어난다.

인수 객체

  • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.

인수 목록

  • 때로는 인수 개수가 가변적인 함수도 필요하다. 가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있는데, 이런 논리로 따져보면 사실상 이항 함수다.

public String format(String format, Object... args)

동사와 키워드

  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.

    • ex. writeField(name)

  • 함수 이름에 키워드를 추가한다.

    • ex. assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다.

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

  • 객체 지향 프로그램이이 나오기 전에는 출력 인수가 불가피한 경우도 있었지만 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문이다.

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

명령과 조회를 분리하라 !

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다.

public boolean set(String attribute, String value);

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

함수를 구현한 개발자는 "set"을 동사로 의도했다. 하지만 if 문에 넣고 보면 형용사로 느껴진다. set이라는 이름을 setAndCheckIfExists라고 바꾸는 방법도 있지만 if문에 넣고 보면 여전히 어색하다. 진짜 해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 방법이다.

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if 문에서 명령을 표현식으로 사용하기 쉬운 탓이다. 아래 전 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.

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;
}

Try/Catch 블록 뽑아내기

try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 trt/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

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

ivate 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가 있다면 함수는 try 문으로 시작해 catxh/finally 문으로 끝나야 한다는 말이다.

Error.java 의존성 자석

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

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

아래와 같은 클래스는 의존성 자석(magnet)이다. 다른 클래스에서 Error enum을 import해 사용해야 하므로, 즉 Error enum이 변한다면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 한다. 이는 Error 클래스의 변경을 어렵게 한다.

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

반복하지 마라 !

중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 각각 손봐야 하므로 문제가 된다.

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

  • E.F.커드는 자료에서 중복을 제거할 목적으로 관계형 데이터베이스에 정규 형식을 만들었따.

  • 객체 지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다.

  • 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming) 모두 어떤 면에서 중복 제거 전략이다.

구조적 프로그래밍

  • 데이크스트라는 모든 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다. 즉, 함수는 return 문이 하나여야 한다는 말이다. 루프 안에서 break나 continue를 사용해선 안 되며 goto는 절대로, 절대로 안 된다.

  • 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 떄로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다. 반면, goto 문은 큰 함수에서만 의미가 있으므로, 작은 함수에서는 피해야만 한다.

함수를 어떻게 짜죠 ?

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

  • 함수를 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 저자는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다.

  • 그런 다음코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전테 클래스를 쪼개기도 한다.✨ 이 와중에도 코드는 항상 단위 테스트를 통과한다.

💫 최종적으로는 이 장에서 설명한 규칙을 따르는 함수가 얻어진다. 처음부터 탁 짜내지 않는다. 그게 가능한 사람은 없으리라.

결론

  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어(Domain Specific Language, DSL)로 만들어진다. 프로그래밍의 기술은 언제나 언어 설계의 기술이다.

  • 대가 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다. 여기서 설명한 규칙을 따르면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수가 나온다. 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다.

https://weheartit.com/entry/300652990

Last updated