부제 : 프로그래머의 코드 품질 개선법
운좋게 리뷰에 당첨되어 받았는데, 생각보다(?) 두꺼웠다 ㅋㅋㅋ....대략 400 페이지 정도 된다.
한달에 한권을 읽어야 하지만 한참을 넘기고 말았다
소프트웨어 전반적인 범위내에서 좋은 코드를 만들기 위한 개념들을 알려주고, 설명해준다.
오브젝트, 클린코드, 이펙티브 시리즈 등과 겹치는 부분들도 존재한다. 또한 책에서는 테스트를 굉장히(?) 중요하게 설명하고 있다.
토이프로젝트나, 현업에서 간단한 비즈니스 로직, API 통신 정도에만 테스트를 해봐서 그런지 매우(x100) 찔렸다.
특징으로는 책에 포함된 예시들이 굉장히 구체적이라서 좋았다. 아마 일반 이론 서적 읽으면서 잘 이해 가지 않았던 개념들을 이 책을 읽어면 이해하는데 성공할 수도 있다.
간단 정리로는
1장 코드 품질 요약
* 좋은 소프트웨어를 만드려면 당연하게도, 고품질의 코드를 작성해야 한다.
* 실제 서비스 환경에서 실행되는 소프트웨어가 되기 전에 코드는 일반적으로 여러 단계의 검사와 테스트를 통과해야 한다(때로는 수동, 때로는 자동화를 통해서)
* 버그나 제대로 동작하지 않는 기능이 사용자에게 제공되거나 비즈니스에 중요한 시스템에서 실행되는 것을 수동, 자동 검사를 통해 막을 수 있다.
* 테스트는 코드를 작성하는 모든 단계에서 고려되면 좋다. 코드를 작성하고 난 후에 고려하는 것이 아니다.
* 고품질 코드를 작성하면 처음에는 시간이 오래 걸리지만, 중장기적으로는 개발 시간이 단축되는 경향을 보인다.
2장 추상화 계층
* 코드를 깨끗하고 뚜렷한 추상화 계층으로 세분화하면 가독성, 모듈화, 재사용, 일반화 및 테스트 용이성이 향상된다.
* 특정 언어에 국한된 기능뿐만 아니라, 함수, 클래스 및 인터페이스를 사용하여 코드를 추상화 계층으로 나눌 수 있다.
* 코드를 추상화 계층으로 분류하는 방법을 결정하려면 해결 중인 문제에 대한 판단과 지식을 사용해야 한다. (도메인 지식을 정확하게 이해하지 않으면 제대로 설계하기 어려울 수 있다)
* 너무 비대한 계층 때문에 발생하는 문제는 너무 얇은 계층 때문에 발생하는 문제보다 더 심각하다. 확실하지 않은 경우에는 남용의 위험에도 불구하고 계층을 얇게 만드는 것이 좋다(구현체와 인터페이스를 분리하는 소위 "느슨하게 만드는 것"이 나누지 않는 것보다 유리하다고 이야기 함)
* 상위 개념(고수준)은 하위 개념(저수준)에 대해 영향 받지 않아야 한다.
고수준/저수준에 관련한 기억나는 하나의 예시
서버에 메세지를 하나 보낸다고 생각하면,
HttpConnection connection = HttpConnection.connect("https://api-endpoint.com/.../...");
connection.send("Hello world");
connection.close();
정도면 간단하게 표현할 수 있다.
여기서 서버에 메세지를 보내는 행위는 상위 수준의 문제이다. 그리고 이를 위해 해결한 하위 문제들은 사실 굉장히 많다
1. 전송할 수 있는 형식으로 문자열 직렬화
2. HTTPS 프로토콜 계층의 모든 복잡한 동작
3. TCP 연결
4. 사용자의 장치가 네트워크에 연결되어 있는지 여부 확인
5. 데이터를 시그널로 변조
6. 링크를 통해 데이터 전송 등
하지만 다른 멋진 개발자들이 이 모든 하위 문제를 이미 해결했을 뿐만 아니라, 그것들을 인식할 필요도 없도록 만들어뒀다.
우리가 알아야할 하위 문제 정도는 Http 연결 열기, Http 연결 닫기, 문자열 보내기 정도이다.
나머지는 우리가 알아야할 필요가 없거나, 무언가 변경해야할 필요는 없다(영향 받지 않음)
3장 다른 개발자와 코드 계약
* 코드베이스는 계속 변하고 일반적으로 여러 개발자에 의해 변경된다.
* 다른 개발자가 어떻게 코드를 해석하고 오용할 수 있을지 생각해보고, 이러한 가능성을 최소화하거나 오용이 불가능하게 만드는 방식으로 코드를 작성하는 것이 유용하다.
* 코드를 작성할 때 일종의 코드 계약이 항상 만들어진다.(여기에는 명백한 항목이나 세부 조항과 같은 내용이 포함될 수 있다.)
* 일반적으로 컴파일러를 사용하여 계약을 확인하는 것이 가장 신뢰할 수 있는 방법이다. 이것이 가능하지 않을 때, 체크나 Assertion 을 사용하여 런타임에서 확인할 수 있다. (Assertion 은 성능 향상과, 코드 오류 발생률을 낮추기 위해 보통 컴파일에서 제외된다)
4장 오류
* 오류에는 크게 2가지 종류가 있다.
* 시스템이 복구할 수 있는 오류/시스템이 복구할 수 없는 오류
* 해당 코드에 의해 생성된 오류로부터 복구 할 수 있는지 여부는 해당 코드를 호출하는 쪽에서만 알 수 있는 경우가 많다.
* 에러가 발생하면 신속하게 실패(컴파일 타임에서 수정할 수 있도록 함)하는 것이 좋고, 에러를 복구할 수 없는 경우에는 요란하게 실패하는 것(에러가 발생했다는 것을 알려서 최악의 경우 프로그램이 비정상 종료 되도록 함)이 바람직하다.
* 오류를 숨기는 것(다양한 에러를 Exception 으로 받아 복구 가능한 에러를 무시한다거나, catch 처리하고 아무런 처리하지 않는 것)은 바람직하지 않을 때가 많으며, 오류가 발생했다는 신호를 보내는 것이 바람직하다.
* 오류 전달 기법은 두 가지 범주로 나눌 수 있다.
* 명시적 방법 : 호출하는 쪽에서는 오류가 발생할 수 있음을 인지하게 처리 하는 방식 (Throw, checkReturnValue annotation 등을 활용하여 상위에서 오류를 처리하지 않으면 컴파일 되지 않도록 처리한다 등)
* 암시적 방법 : 코드에 오류에 대한 설명이 제공되거나, 설명이 아예 없을 수도 있다. 호출하는 쪽에서 알 수 없기 때문에 처리하지 못할 수도 있다.
* 복구 할 수 없는 오류에 대해서는 암시적 오류 전달 기법을 사용하는 것이 유리하다.
* 잠재적으로 복구할 수 있는 오류에 대해서는
* 명시적 혹은 암시적 기법 중 어느 것을 사요할지에 대해서는 개발자들 사이에서도 일치되는 의견이 없다.
* 글쓴이 의견으로는 명시적인 기법이 유리할 것이라 이야기함
* 컴파일러 경고는 종종 코드에 문제가 있을 때 이에 대해 표현해주고, 주의에 귀를 기울이는 것이 바람직하다.
5장 가독성 높은 코드를 작성하라
* 코드의 가독성이 떨어져서 이해하기 어려울 때 다음과 같은 문제가 발생할 수 있다.
* 다른 개발자가 코드를 이해하느라 시간을 허비함
* 버그를 유발하는 오해
* 잘 작동하던 코드를 다른 개발자가 수정한 뒤에 동작하지 않음
* 코드의 가독성을 높이다 보면 때로는 코드가 장황하게 되고 더 많은 줄을 작성해야 할 수도 있다. 이것은 종종 가치 있는 절충이다.
* 코드의 가독성을 높이려면 다른 개발자의 입장을 공감하고, 그들이 코드를 읽을 때 어떻게 혼란스러워할지를 상상해보는 것이 필요하다. (강력한 공감 - 개발할 때 작성하는 "나"를 타인처럼 생각한다)
* 실제 시나리오는 다양하며 보통 그 상황에 해당하는 어려움이 있다. 가독성이 좋은 코드를 작성하려면 언제나 상식을 적용하고 판단력을 사용해야 한다.
6장 예측 가능한 코드를 작성하라
* 다른 개발자가 작성하는 코드는 자주 우리가 작성하는 코드에 의존한다.
* 다른 개발자가 우리가 작성한 코드의 기능을 잘못 해석하거나 처리해야 하는 특수한 경우를 발견하지 못하면, 우리가 작성한 코드에 기반한 코드에서 버그가 발생할 가능성이 크다.
* 코드를 호출하는 쪽에서 예상한대로 동작하기 위한 좋은 방법 중 하나는 중요한 세부 사항이 코드 계약의 명백한 부분에 포함되도록 하는 것이다.
* 우리가 사용하는 코드에 대해 허술하게 가정을 하면 예상을 벗어나는 또 다른 결과를 초래할 수 있다.
* 예를 들어 열거형에 추가되는 새 값을 예상하지 못하는 경우
* 사용 중인 코드가 가정을 벗어날 경우, 코드 컴파일을 중지하거나 테스트가 실패하도록 작성하는 것이 중요하다.(대수 타입 활용 등)
* 테스트만으로는 예측을 벗어나는 코드의 문제를 해결할 수 없다. 다른 개발자가 코드를 잘못 해석하면 테스트해야 할 시나리오도 잘못 이해할 수 있다.
7장 코드를 오용하기 어렵게 만들자
* 코드가 오용되기 쉽게 작성되고 나면 어느 시점에선가는 오용될 가능성이 크고 이것은 버그로 이어질 수 있다.
* 코드가 오용되는 몇가지 일반적인 사례는 아래와 같다.
* 호출하는 쪽에서 잘못된 입력을 제공
* 다른 코드에서 일어나는 부수 효과
* 함수 호출 시점이 잘못되거나 올발느 순서로 호출되지 않은 경우
* 기존 코드에 연관된 코드를 수정할 때 기존 코드가 내포한 가정과 어긋나게 수정하는 경우
* 오용이 어렵거나 불가능하도록 코드를 설계하고 구조화하는 것이 종종 가능하다. 이를 통해 버그 발생 가능성이 크게 줄어들고 중장기적 개발자의 시간을 많이 절약할 수 있다.
예시로 나오는 예금/인출/잔액 3개의 속성중에 파생 속성이 있는데 예금 - 인출 = 잔액으로 잔액을 의미하는데 굳이 잔액 속성을 두는 것이 아니라 필요할 때 예금과 인출을 이용하여 잔액을 계산해내는 식의 truth of source (진실의 원천)을 구분하여 처리해야하는 부분에 큰 공감이 갔다.
8장 코드를 모듈화 하자
* 코드가 모듈화되어 있으면 변경된 요구사항을 적용하기 위한 코드를 작성하기가 쉽다.
* 모듈화의 주요 목표중 하나는 요구사항의 변경이 해당 요구사항과 직접 관련된 코드에만 영향이 미치도록 하는 것이다.
* 코드를 모듈식으로 만드는 것은 간결한 추상화 계층을 만드는 것과 깊은 관련이 있다.
* 다음의 기술을 사용하여 코드를 모듈화 할 수 있다.
* 의존성 주입(책에서는 메뉴얼 주입하는 경우 부가적인 코드를 많이 작성하게 되어 불편할 수 있으니 의존성 프레임워크를 추천)
* 구체적인 클래스가 아닌 인터페이스에 의존 (하위 구현은 상위 레벨에 의존)
* 클래스 상속 대신 인터페이스 및 구성(컴포지션)의 활용 (이펙티브 시리즈에서 항상 나오는 주제!)
* 클래스는 자신의 기능만 처리
* 관련된 데이터의 캡슐화
* 반환 유형 및 예외 처리 시 구현 세부 정보 유출 방지(서비스 입장에서는 로그인이 실패했다는 에러를 알고 싶은 것이지, 어떤 암호화 처리중 어떤 문제가 생겼는지 구체적인 하위 구현의 내용을 상위에 유출할 필요는 없음)
9장 코드를 재사용하고 일반화 할 수 있도록 하자
* 동일한 하위 문제가 자주 발생할 수 있기 때문에 코드를 재사용할 수 있도록 작성하면 미래의 자신과 팀 동료의 시간과 노력을 절약할 수 있다.
* 다른 개발자가 여러분이 해결하려는 문제와는 다른 상위 수준의 문제를 해결하더라도 특정 하위 문제에 대해서는 여러분이 작성한 해결책을 재사용할 수 있도록 근본적인 하위 문제를 식별하고 코드를 구성하도록 노력해야 한다.
* 간결한 추상화 계층을 만들고 코드를 모듈식으로 만들면 코드를 재사용하고 일반화하기가 훨씬 쉽고 안전해진다.
* 가정을 하게 되면 코드는 종종 취약해지고 재사용하기 어렵다는 측면에서 비용이 발생한다.
* 가정을 하는 경우 이점이 비용보다 큰지 확인하자
* 가정을 해야 할 경우 그 가정이 코드의 적절한 계층에 대해 이루어지는 것인지 확인하고 가능하다면 가정을 강제적으로 적용하자.
* 전역 상태를 사용하면 비용이 많이 발생하는 가정을 하는 것이 되고, 재사용하기에 안전하지 않은 코드가 된다. 대부분의 경우 전역 상태를 피하는 것이 가장 바람직하다.
10장 단위 테스트의 원칙
* 코드 베이스에 제출된 거의 모든 "실제 코드"(서비스에 활용되는 코드)는 그에 해당하는 단위 테스트가 동반 되어야 한다.
* "실제 코드"가 보여주는 모든 동작에 대해 이를 실행해보고 결과를 확인하는 테스트 케이스가 작성되어야 한다. 아주 간단한 테스트 케이스가 아니라면 각 테스트 케이스 코드는 준비, 실행 및 단언(Given-When-Then, gwt 기법)의 세가지 부분으로 나누는 것이 일반적이다.
* 바람직한 단위 테스트의 주요 특징은 아래와 같다.
* 문제가 생긴 코드의 정확한 탐지
* 구현 세부 정보에 구애받지 않음
* 실패가 잘 설명됨
* 이해하기 쉬운 테스트 코드
* 쉽고 바르게 실행
* 테스트 더블은 의존성을 실제로 사용하는 것이 불가능하거나 현실적으로 어려울 때 단위 테스트에 사용할 수 있다. 테스트 더블의 몇가지 예는 다음과 같다.
* 목 : 클래스나 인터페이스를 시뮬레이션 하는데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다. 함수가 호출될 때 인수에 제공되는 값을 기록한다.
* 스텁 : 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션 한다. 이를 통해 테스트 대상 코드는 특정 멤버 함수를 호출하고 특정 한 값을 반환하도록 의존성에 시뮬레이션 할 수 있다.
* 페이크 : 클래스(혹은 인터페이스)의 대체 구현체로 테스트에서 안전하게 사용할 수 있다. 페이크는 실제 의존성의 공개 API 를 정확하게 시뮬레이션 하지만 구현은 일반적으로 단순한데, 외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다 스텁과는 달리 구현을 해야하기 때문에 PRD(제품 요구 문서) 를 봐야하는 경우가 많고 이때 도메인 지식을 습득하게 되어 실수를 더 방지하는 효과가 생긴다.
* 목과 스텁사이에는 차이가 있지만, 개발자들이 일상적으로 목이라고 말할 때는 목 + 스텁을 합쳐서 지칭하는 경우가 많다.
* 목 및 스텁을 사용한 테스트 코드는 비현실적이고 구현 세부 정보에 밀접하게 연결 되어 이후 확장성에 어려움이 있을 수 있다.
* 목과 스텁의 사용에 대한 여러 의견이 있다. 글쓴이는 가능한 한 실제 의존성이 테스트에 사용되어야 한다고 이야기 한다. 그리고 차선책으로는 페이크 그 이후에 목과 스텁이 최후의 수단으로 사용되는 것을 지향한다. (우선순위 : 실제 의존성 -> 페이크 -> 목 -> 스텁)
11장 단위 테스트의 실제
* 각각 함수 단위에서 테스트하는 것에 집중하다 보면 중요한 테스트에 대해 충분히 테스트 하지 못하는 경우가 있다. 보통은 모든 중요한 행동을 먼저 파악하고 각각의 테스트 케이스를 작성하는 것이 더 효과적이다.
* 결과적으로 중요한 동작을 테스트 해야 한다. 프라이빗 함수를 테스트 하는 것은 거의 대부분 결과적으로 중요한 사항을 테스트 하는 것이 아니다.
* 한번에 한가지만 테스트를 하면 테스트 실패의 이유를 더 잘 알 수 있고, 테스트 코드를 이해하기가 쉽다.
* 테스트 설정 공유(@BeforeEach, @BeforeAll, @AfterEach, @AfterAll 류를 지칭한다)는 양날의 검이 될 수 있다. 코드 반복이나 비용이 큰 설정을 피할 수 있지만 부적절하게 사용할 경우 효과적이지 못하거나 신뢰할 수 없는 결과를 초래할 수 있다.
* 의존성 주입을 사용하면 코드의 테스트 용이성이 상당히 향상될 수 있다.
* 단위 테스트는 개발자들이 가장 자주 다루는 테스트 수준이지만 이것만이 유일한 테스트는 아니다. 높은 품질의 소프트웨어를 작성하고 유지하려면 여러가지 테스트 기술을 함께 사용해야 할 때가 많다.
위의 요약되어 있는 부분들이 책에서는 자세하게 설명되어 있으니 여유가 읽는 것을 추천한다.
'한달에 교양 책 한권' 카테고리의 다른 글
헤드 퍼스트 디자인 패턴 with ChatGPT (0) | 2023.04.30 |
---|---|
돈의 속성 (1) | 2023.04.23 |
부의 확장 (0) | 2023.04.08 |
주린이가 가장 알고 싶은 최다 질문 (0) | 2023.03.25 |
부자 아빠 가난한 아빠 (0) | 2023.03.19 |