코드 품질에 대해 정의하고 좋은 코드를 짜는 방법, 나쁜 코드를 구별하는 방법에 대한 지침서
- 고품질의 코드: 신뢰할 수 있고, 유지보수가 쉬운 소프트웨어
- 그러나 코드를 고/저 품질로 정의하는 것은 본질적으로 주관적이고 성급한 것이다
- 코드의 목표
- 작동해야 하고, 작동이 멈춰서는 안된다
- 변화하는 요구사항에 적응해야 한다
- 이미 존재하는 기능을 다시 구현해서는 안된다
- 코드 품질의 핵심 요소
- 가독성: 읽기 쉬워야 한다
- 예측 가능성: 동작 예측이 가능해야 한다
- 쉬운 사용성: 사용이 쉬워야 하며 잘못 사용하기 어렵다
- 모듈성: 인터페이스가 잘 정의되어 서로 다른 구성 요소 간 상호작용 지점이 최소화된다
- 재 사용 가능성(일반화)
- 재 사용성: 어떤 문제를 해결하기 위한 무언가가 다른 상황에서도 사용 가능할 때
- 일반화성: 개념적으로 유사하나 미묘한 다른 문제를 해결 할 수 있을 때
- 테스트 용이성: 해당 코드가 얼마나 테스트하기 용이한가?
- 추상화 계층
- 코드를 잘 구성한다는 것은 간결한 추상화 계층을 만드는 것으로 귀결될 때가 많다
- 상위 수준의 문제를 해결하기 위해 최소한의 개념만을 필요하게 만들어 준다
- 코드를 적절한 크기의 클래스로 세분화하는 것은 추상화를 위한 효과적 도구이다
- 두 가지의 이상의 다른 방식으로 구현하거나 구현될 것이 예상되는 경우 인터페이스를 이용하자
- 계층이 비대한 것보다 너무 얇아서 생기는 문제가 더 심각하다 → layer bypass, 복잡도 문제 생김
- 코드 계약
- 명시적(명백) 계약: 함수 명, 클래스, 열거형, 매개변수, 반환 값 유형
- 암시적(세부) 계약: 주석문, 문서 → 실제로 잘 읽지 않는다
- 개발자는 암시적 계약을 잘 읽지 않으므로 명시적 계약에 주로 의도를 나타내야 한다
- 상태/ 가변성을 클래스 외부로 노출하지 말아야 암시적 계약의 필요성이 줄어든다
- 오류
- 오류는 숨기는 것이 아니다!!
- 기본 값 반환/ 널 객체 패턴과 같은 경우는 특수한 경우만 사용해야 한다
- 가능한 오류에 대해 문서화 해야 한다
- 빠른 실패를 통해 알려져야 한다
- 오류의 발생 지점 가장 가까운 곳에서 해당 오류를 실패 처리 해야 한다 → 디버깅 용이성
- 복구 가능한 오류
- 일반적으로 시스템 외부 요소에 의한 오류는 시스템 전체가 표 나지 않게 처리해야 한다
- 호출하는 쪽에서 오류를 복구하기를 기대하고 오류에 대해 인지 할 수 있도록 해야 한다
- 복구할 수 없는 오류
- 오류의 발생을 빠르게 인지 할 수 있도록 요란하게 실패 해야 한다 → 모니터링
- 오류 전달 방법
- 저자는 호출하는 쪽에서 복구를 원한다면 명시적 방식이 좋다고 생각한다
- 명시적
- Checked Exception: 자바 한정
- Null 값이 가능한 유형 반환
- Result 객체 반환: 성공 값 과 실패 원인을 가진 객체를 반환한다
- 아웃컴 반환: 성공/실패 시 boolean 값을 반환한다 → java는 @CheckReturnValue 어노테이션을 이용하자
- 암시적
- Unchecked Exception
- 프로미스/퓨처를 이용한 방법
- 매직 값 반환: 특별한 의미를 부여한 값을 반환 → 쓰지 말자…
- 가독성이 높은 코드
- 서술적인(명시적인) 이름을 사용하자
- 큰 단위의 코드에서는 주석문이 유용할 수 있다
- 성능을 위해서가 아니라면 간결하더라도 이해하기 어려운 코드를 피해라
- 너무 많은 중첩(코드 블록)을 피하라
- 중첩 블록에 반환문이 없다면 함수가 너무 많은 일을 하고 있다는 증거
- 설명되지 않은 하드코딩 값을 피하라
- 간단한 로직에서는 익명 함수(람다)를 이용하라
- 예측 가능한 코드
- 매직 값을 반환하지 말아야 한다 → Null, 옵셔널, 오류를 반환하라
- Null 객체 패턴(ex: 빈 컬렉션 )은 필요시에만 사용해야 한다
- 분명하고 의도적인 부수 효과를 사용하도록 하라
- 입력 매개변수를 수정하지 마라 → 복사 후 사용 또는 함수 명에 명시하라
- 열거형에서 기본(default) 케이스 처리를 주의하라 → 알 수 없다면 쓰지 마라
- 새로운 값이 추가되었을 때 버그를 만들어 낼 수 있다
- 오/남용이 어려운 코드
- 불변 객체를 고려하라
- 가변 객체는 추론하기 어렵고 다중 스레드에서 문제가 발생할 수 있다
- 이 경우 얕은 복사(swallow copy) 대신 깊은 복사를 통해 방어적으로 객체를 만들어야 한다
- 지나치게 일반적인 기본 타입의 사용을 피하라
- 페어,트은 유형의 사용에 주의하라
- 시간 처리
- 정수를 사용하지 말자 → 양인지 어떤 순간인지를 구분하기 어렵다
- java의 경우 Instant(순간), Duration(양) 객체가 라이브러리로 제공된다
- 데이터/로직에 대한 진실의 원천(source of truth)는 하나만 가져야 한다
- 파생 데이터를 따로 저장하지 말고 원천(기본) 데이터로부터 계산하는 편이 좋다
- 중복 로직은 결국엔 진실의 원천이 여러 개인 것과 동일하다
- 모듈화된 코드
- 요구 사항이 어떻게 바뀔지 정확히 예측하는 것은 시간 낭비
- 의존성 주입을 통해 하위 문제를 바꾸는 방식으로 요구 사항 변경에 대응해야 한다
- 클래스 의존보다는 인터페이스 의존이 더 바람직하다
- 상속보다는 합성을 사용해야 한다
- 상속은 반드시 is-a 관계 일 때만 사용해야 한다
- 불필요한 기능 노출이 발생 할 수 있다
- 부모 클래스 변경에 취약해 질 수 있다
- 클래스는 자신의 기능에만 집중해야 한다 → 다른 클래스의 세부사항에 의존하면 안된다
- 클래스는 최대한 캡슐화 하라
- 반환 유형에서 구현 세부 사항이 유출되어서는 안된다
- 내부 오류는 한 번 감싸서 전달되어야 한다 → 세부 오류 구현이 바깥으로 유출된다
- 재 사용/일반화된 코드
- 저자는 특정한 목적이 아니라면 코드 베이스는 짧을 수록 좋다고 여긴다
- 기능 변경과 리팩토링을 같이 하지 마라!!
- 저자는 목/스텁을 사용하는 테스트보다는 실제 의존성이나 페이크를 사용하는 편이 낫다고 여긴다.