Search

좋은 코드, 나쁜 코드

 Chapter 1. 코드 품질

소프트웨어는 코드로 만들어진다.
코드가 의도한 대로 작동하고 기존의 기능이 여전히 잘 작동한다고 확신하기 위한 다양한 과정과 점검이 이루어진다.
이러한 과정을 종종 소프트웨어 개발 및 배포 프로세스라고 부른다.
1.
개발자가 코드베이스의 로컬 복사본을 가지고 작업하면서 코드를 변경한다
2.
작업이 끝나면 코드 검토를 위해 변경된 코드를 가지고 병합 요청을 한다
3.
다른 개발자가 코드를 검토하고 변경을 제안할 수 있다
4.
작성자와 검토자가 모두 동의하면 코드가 코드베이스에 병합된다
5.
배포는 코드베이스를 가지고 주기적으로 일어난다.
6.
테스트에 실패하거나 코드가 컴파일되지 않으면 코드베이스에 병합되는 것을 막거나 코드가 배포되는 것을 막는다.
고품질 코드는 일반적으로 좀 더 신뢰할 수 있고 유지보수가 쉬우며, 버그가 적은 소프트웨어를 생산한다.
코드 품질을 높이는 것에 관한 많은 원칙들은 소프트웨어가 처음에 만들어지는 방식을 보장하는 것뿐만 아니라.
이후에 요구 사항이 진화하고 새로운 상황이 등장하더라도 그 방식을 계속 유지할 수 있는 것에 관심을 둔다.
코드 품질을 높이기 위한 네 가지 목표가 존재한다.
1.
코드는 작동해야 한다.
우리가 코드를 작성하는 이유는 문제를 해결하기 위함이다. 코드는 우리가 해결하려고 하는 문제를 해결해야 한다.
이것은 또한 버그가 없다는 것을 의미하고 모든 요구 사항을 충족하는 것을 의미한다.
2.
코드는 작동이 멈추면 안 된다.
당장 돌아가는 코드를 만들기는 쉽지만, 변화하는 환경과 요구 사항에도 불구하고 계속 작동하는 코드를 만드는 것은 훨씬 더 어렵다.
코드가 계속 작동하도록 보장하는 것은 소프트웨어 엔지니어가 직면하는 큰 과제 중 하나이며, 코딩의 모든 단계에서 고려해야 할 사항이다.
3.
코드는 변경된 요구 사항에 적응할 수 있어야 한다.
코드나 소프트웨어가 시간이 지남에 따라 어떻게 변할지 완벽하고 정확하게 예측하는 것은 불가능하다.
하지만 어떻게 변할지 정확히 알지 못한다고 해서 변한다는 사실 자체를 완전히 무시해야 하는 것은 아니다.
4.
코드는 이미 존재하는 기능을 중복 구현해서는 안 된다.
이미 있는 코드는 다시 작성하지 않는다는 개념은 양방향으로 적용된다.
어떤 하위 수준의 문제를 해결하기 위해 다른 개발자가 이미 코드를 작성했다면, 그것을 해결하기 위해 자신의 코드를 작성하기 보다는 그들의 코드를 사용해야 한다.
마찬가지로 어떤 하위 문제를 해결하기 위해 자신이 이미 코드를 작성했다면, 다른 개발자들이 동일한 문제를 해결하기 위해 자신만의 코드를 다시 작성하지 않도록 쉽게 재사용할 수 있는 방식으로 코드를 구성해야 한다.
위의 코드 품질을 높이기 위한 네 가지 목표에 부합하기 위한 코드 품질의 여섯 가지 핵심 요소는 다음과 같다.
1.
코드는 읽기 쉬워야 한다.
코드를 작성하고 난 후 어느 시점이 되면 다른 개발자가 그 코드를 읽고 이해해야 하는 상황이 반드시 온다.
코드의 가독성이 떨어진다면, 다른 개발자가 그 코드를 이해하는 데 많은 시간을 들여야 한다.
또한, 코드의 기능에 대해 잘못 이해하거나 몇 가지 중요한 세부 사항을 놓칠 가능성 역시 크다.
2.
코드는 예측 가능해야 한다.
도움이 되거나 똑똑한 일을 수행하는 코드를 작성할 때, 아무리 좋은 의도를 가지고 잇더라도 예상을 벗어난 동작을 수행하는 위험이 있을 수 있다.
코드가 예상에서 벗어나는 일을 한다면, 그 코드를 사용하는 개발자는 그 상황을 알지 못하거나 그 상황에 대처할 생각을 하지 못할 것이다.
이로 인해 문제의 코드와는 전혀 상관없어 보이는 부분에서 명백하게 이상한 일이 발견되기 전까지 시스템은 계속 비정상적으로 작동한다.
3.
코드를 오용하기 어렵게 만들라.
자신이 작성하는 코드는 종종 다른 코드에 의해 호출되는데, 그 코드가 잘못 사용된다면 중요한 일이 수행되지 않거나 이상하게 동작하지만 눈에 띄지 않는다는 것을 의미할 수도 있다.
코드를 오용하기 어렵거나 불가능하게 하면 코드가 작동할 뿐만 아니라 계속해서 잘 작동할 가능성을 극대화할 수 있다.
4.
코드를 모듈화하라.
코드를 외부에 의존하지 않고 실행할 수 있는 모듈로 나누는 것은 변화하는 요구 사항에 더 쉽게 적응할 수 있는 코드를 작성하는데 도움이 된다.
모듈화된 시스템은 일반적으로 이해하기 쉽고 추론하기 쉬운데, 기능이 관리 가능한 단위로 나누어지고 기능 단위 간 상호작용이 잘 정의되고 문서화되기 때문이다.
코드가 모듈화되어 작성되면 처음에 작동이 시작되고 그 후에도 계속해서 잘 작동할 가능성이 커진다.
5.
코드를 재사용 가능하고 일반화할 수 있게 작성하라.
코드가 재사용할 수 있고 일반화되어 있으면 다른 사람들은 그 코드를 코드베이스의 여러 부분에서, 그리고 하나 이상의 상황에서 사용할 수 있고, 여러 가지 문제를 해결할 수 있다.
이런 코드는 시간과 노력을 절약해주고 더 신뢰할 수 있는데, 그 이유는 실제 서비스 환경에서 이미 시도되고 테스트된 논리를 재사용하기 때문이다.
즉, 어떤 버그라 할지라도 이미 발견되고 해결되었을 가능성이 크다는 것을 의미한다.
6.
테스트가 용이한 코드를 작성하고, 제대로 테스트하라.
이 항목은 제목에 ‘테스트가 용이한 코드를 작성하라’와 ‘제대로 테스트하라’라는 두 가지 중요한 개념이 포함되어 있다.
테스트로 코드의 기능을 견고하게 만들지 않으면 복잡해진 코드베이스에서 실수하는 문제가 발생할 수 있다.
테스트 용이성은 테스트 대상이 되는 코드를 가리키며 해당 코드가 얼마나 테스트하기 적합한지를 나타내는데 코드의 테스트 용이성이 낮으면 제대로 테스트하는 것이 불가능할 수 있다.
이러한 핵심 요소들을 다 갖춰 네 가지 목표를 이룬 고품질 코드의 작성은 일정을 지연시키는가?
이 질문에 대한 답은 단기적으로는 고품질 코드를 작성하는 데 시간이 더 걸릴 수 있다는 것이다.
하지만 한번 사용하고 버릴 사소한 유틸리티성 프로그램이 아닌 좀 더 중요한 소프트웨어를 개발하고 있다면, 일반적으로 고품질 코드를 작성하는 것이 중장기적으로는 개발 시간을 단축해준다.

 Chapter 2. 추상화 계층

코드 작성의 목적은 문제 해결이다.
문제를 어떻게 해결하는가도 중요하지만 그것들을 해결하는 코드를 어떻게 구성하는가도 중요하다.
코드를 구성하는 방법은 코드 품질의 기본적인 측면 중 하나이며, 코드를 잘 구성한다는 것은 간결한 추상화 계층을 만드는 것으로 귀결될 때가 많다.
코드 작성은 복잡한 문제를 계속해서 더 작은 하위 문제로 세분화하는 작업이다.
어떤 문제를 하위 문제로 계속해서 나누어 내려가면서 추상화 계층을 만든다면, 같은 층위 내에서는 쉽게 이해할 수 있는 몇 개의 개념만을 다루기 때문에 개별 코드는 특별히 복잡해 보이지 않을 것이다.
깨끗하고 뚜렷한 추상화 계층을 구축하면 코드 품질의 네 가지 목표를 달성할 수 있다.
실제로 추상화 계층을 생성하는 방법은 코드를 서로 다른 단위로 분할하여 단위 간의 의존 관계를 보여주는 의존성 그래프를 생성하는 것이다.
추상화 계층을 명확하게 구분하기 위해 코드를 작성하며 고려해야 할 측면이 두 가지 있다.
1.
코드를 호출할 때 볼 수 있는 내용
a.
퍼블릭 클래스, 인터페이스 및 함수(메서드)
b.
이름, 입력 매개변수 및 반환 유형이 표현하고자 하는 개념
c.
코드 호출 시 코드를 올바르게 사용하기 위해 알아야 하는 추가 정보
2.
코드를 호출할 때 볼 수 없는 내용
a.
구현 세부 사항
대부분의 프로그래밍 언어는 코드를 다른 단위로 나누기 위해 함수, 클래스, 인터페이스, 패키지의 언어 요소를 자유롭게 사용할 수 있다.
함수
어떤 로직을 새로운 함수로 구현하면 대부분 유익하다.
각 함수에 포함된 코드가 하나의 잘 써진 짧은 문장처럼 읽히면 이상적이다.
함수를 작게 만들고 수행하는 작업을 명확하게 하면 코드의 가독성과 재사용성이 높아진다.
클래스
여러 개발자들은 단일 클래스의 이상적인 크기의 기준으로 줄 수, 응집력, 관심사의 분리를 제시한다.
대부분 300줄보다 긴 클래스는 너무 많은 개념을 다루므로 분리해야 한다고 경고한다.
응집력과 관심사의 분리에 대해 생각할 때는 서로 관련된 여러 가지 사항을 하나의 사항으로 간주하는 것을 어느 수준에서 해야 유용할지 결정해야 한다.
너무 많은 일을 하는 거대한 클래스를 만들면 코드 품질의 저하로 이어질 때가 많다.
그러므로 클래스 구조를 설계할 때 코드 품질의 네 가지 목표를 충족하는지 신중하게 생각하면 좋다.
인터페이스
계층 사이를 뚜렷이 구분하고 구현 세부 사항이 계층 사이에 유출되지 않도록 하기 위해 사용할 수 있는 한 가지 접근법은 어떤 함수를 외부로 노출할 것인지를 인터페이스를 통해 결정하는 것이다.
하나의 추상화 계층에 대해 두 가지 이상의 다른 방식으로 구현을 하거나 향후 다르게 구현할 것으로 예상되는 경우 인터페이스를 정의하는 것이 좋다.
주어진 추상화 계층에 대해 한 가지 구현만 있고 향후에 다른 구현을 추가할 계획이 없더라도 여전히 인터페이스를 통해 추상화 계층을 표현해야 하는가는 아래와 같은 장,단점이 존재한다.
장점
1.
퍼블릭 API를 매우 명확하게 보여준다.
2.
한 가지 구현만 필요하다는 가정이 틀렸을 때 대응하기 쉽다.
3.
mock 객체나 페이크 객체를 통해 테스트를 쉽게 할 수 있다.
4.
같은 클래스가 두 개 이상의 서로 다른 추상화 계층에 구현을 제공할 수 있다.
단점
1.
인터페이스를 정의하려면 새로운 파일, 더 많은 코드를 작성해야 한다.
2.
다른 개발자가 코드를 이해하고 논리적으로 탐색하기 어려워질 수 있다.
추상화 계층을 너무 많이 생성하여 층이 너무 얇아진다면 장점도 많지만 비용도 늘어나게 된다.
어떤 것이 더 나을지 확실하지 않다면, 너무 많은 계층을 남용하는 결과를 가져오더라도 계층을 여러 개로 나누는 것이 한 계층 안에 모든 코드를 집어넣는 것보다는 낫다.
그렇다면 마이크로서비스는 어떤가?
마이크로서비스는 일반적으로 꽤 간결한 추상화 계층을 제공하는 것이 사실이지만, 대개 크기와 범위를 기준으로 마이크로서비스를 나누기 때문에 여전히 그 내부에서 적절한 추상화 계층을 고려하는 것이 유용하다.
올바른 추상화 및 코드 계층을 만드는 것은 여전히 중요하다.

 Chapter 3. 다른 개발자와 코드 계약

자신이 작성한 코드를 다른 개발자가 작업해야 하고, 반대로 다른 개발자가 작업한 코드를 자신이 작업해야 할 때도 있다.
고품질 코드를 작성할 때 가장 중요한 고려 사항 중 하나는 다른 개발자가 변경하거나 코드와 상호작용할 때 발생할 수 있는 문제는 없는지, 또 발생한다면 그 문제를 어떻게 완화할 수 있는지를 이해하고 선제적으로 조치하는 것이다.
코드를 작성할 때 다음 세 가지를 고려하는 것이 유용하다.
자신에게 분명하다고 해서 다른 사람에게도 분명한 것은 아니다.
자신의 로직에 너무 익숙해서 모든것이 분명해 보이기 때문에 어떤 것이 왜 그런 방식인지, 문제를 왜 그 방식으로 해결하고 있는지에 대해서는 거의 생각하지 않아도 될 것이다.
하지만 어느 시점에 이르면 다른 개발자가 여러분이 작성한 코드와 상호작용하거나, 여러분의 코드를 변경하거나, 여러분의 코드가 의존하고 있는 코드를 변경해야 할 수도 있다는 것을 기억해야 한다.
이것을 항상 고려하고 코드가 어떻게 사용되어야 하는지, 무엇을 하는지, 그리고 왜 그 일을 하고 있는지를 설명하는 것이 유용하다.
다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.
여러분이 작성한 코드는 다른 코드로부터 전혀 영향을 받지 않은 채 독립적으로 있는 것이 아니라, 끊임없이 변화하는 코드 위에 놓여 있고, 여러분의 코드를 기반으로 계속해서 변화하는 코드 역시 끊임없이 작성된다.
다른 개발자의 코드 변경으로 인해 여러분 자신의 코드가 작동하지 않거나 오용하는 결과를 가져온다면, 무언가 문제가 있을 때 코드 컴파일이 중지되거나 테스트가 실패하도록 만들어서 이 문제가 해결되기 전까진 코드베이스에 병합되면 안 된다.
시간이 지나면 자신의 코드를 기억하지 못한다.
자신에게는 분명한데 다른 사람에게는 분명하지 않을 수 있다는 것, 혹은 다른 사람들이 무의식중에 자신의 코드를 작동하지 않게 만드는 것과 관련해 살펴본 모든 내용이 어느 순간에 자신에게 적용된다.
배경지식이 거의 없거나 전혀 없는 사람에게도 자신의 코드가 이해하기 쉬어야 하고, 잘 작동하던 코드에 버그가 발생하는 것이 어려워야 한다.
이렇게 하는 것은 다른 사람에게 호의를 베푸는 것이기도 하지만 미래의 자신에게도 유익한 일이다.
다른 개발자가 여러분의 코드를 사용하거나 여러분의 코드에 의존하는 코드를 수정할 때, 그들은 여러분의 코드를 어떻게 사용해야 하는지 그 코드가 무슨 일을 하는지 파악해야 한다.
여러분이 작성한 코드를 어떻게 사용해야 하는지 알아내기 위해 다른 개발자가 할 수 있는 일은 다음과 같다.
함수, 클래스, 열거형 등의 이름을 살펴본다.
이름을 살펴보는 것은 개발자들이 새로운 코드의 사용 방법을 알아내기 위해 실제로 사용하는 주된 방법 중 하나다.
패키지, 클래스, 함수의 이름을 책의 목차라고 생각할 수 있다.
이 이름들을 살펴보는 것은 하위 문제를 해결할 코드를 찾기 위한 편리하고 빠른 방법이다.
함수와 생성자의 매개변수 유형 또는 반환값의 유형 같은 데이터 유형을 살펴본다.
컴파일이 필요한 정적 유형의 언어에서는 데이터 유형을 인식하고 올바르게 사용해야 한다. 그렇지 않으면 코드가 컴파일되지 않는다.
따라서 유형 시스템을 사용하는 언어로 코드를 작성하는 것은 다른 개발자가 코드를 오용하거나 오작동할 수 없도록 하기 위한 좋은 방법 중 하나다.
함수/클래스 수준의 문서나 주석문을 읽어본다.
자바독과 같은 공식적인 코드 내 문서, 함수 및 클래스 수준의 비공식적인 주석문, README.md와 같은 외부 문서들은 모두 매우 유용하지만 어느정도까지만 신뢰할 수 있다.
다른 개발자가 이 문서들을 읽을 것이라는 보장이 없으며 실제로 읽지 않을 때가 많고
설령 읽더라도 잘못 해석할 수 있으며 문서의 업데이트가 제대로 안 될 수 있기 때문이다.
직접 와서 묻거나 채팅/이메일을 통해 문의한다.
이 접근법 또한 상당히 효과적일 수 있지만 코드의 사용법을 설명하기에는 신뢰하기 어려운 방법이다.
코드를 많이 작성할수록 답하는 데 더 많은 시간을 써야하며 코드 작성자가 휴가를 가거나 회사를 떠나면 코드에 대해 물어볼 사람이 없다.
또한 1년이 지나면 자기 자신도 그 코드를 기억하지 못하기 때문에 어느 정도 제한된 기간 동안만 효과가 있다.
여러분이 작성한 함수와 클래스의 자세한 구현 코드를 읽는다.
코드 사용 방법에 대한 가장 확실한 답을 얻을 수 있는 방법은 코드의 자세한 구현 세부 사항을 살펴보는 것이다.
하지만 이 접근법은 실용적이지도 않고 코드의 양이 많으면 효과를 얻기 힘들다.
코드 계약은 다른 사람들이 어떻게 코드를 사용할지, 그리고 코드가 무엇을 할 것으로 기대할 수 있는지에 대한 원칙이다.
코드를 호출하는 사람에게 무언가를 설정하거나 입력(선결 조건)을 제공해야 할 요건을 부여하고, 호출 결과 일어날 일 혹은 반환될 값(사후 조건)에 대한 기대를 갖게 한다.
코드의 계약을 정의할 때 명확한 부분과 세부 조항이 존재한다.
계약의 명확한 부분
함수와 클래스 이름 : 호출하는 쪽에서 이것을 모르면 코드를 사용할 수 없다.
인자 유형 : 호출하는 쪽에서 인자의 유형을 잘못 사용하면 코드는 컴파일조차 되지 않는다.
반환 유형 : 호출하는 쪽에서 함수의 반환 유형을 알아야 한다.
검사 예외 : 호출하는 코드가 이것을 처리하지 않으면 코드는 컴파일되지 않는다.
세부 조항
주석문과 문서
비검사 예외
주석문과 문서의 형태로 된 세부 조항은 간과하고 넘어갈 때가 많기 때문에 다른 개발자들이 해당 코드를 사용할 때 모든 세부 조항을 다 알지 못할 가능성이 크다.
따라서 세부 조항을 사용하는 것은 신뢰할 만한 방법이 아니고 세부 조항에 너무 많이 의존하면 오용하기 쉬운 취약한 코드가 될 가능성이 크다.
다른 개발자가 코드를 올바르게 사용하기 위해 세부 조항에 의존하기보다 잘못된 일을 하는 것을 처음부터 불가능하게 만드는 것이 좋다.
코드가 어떤 상태에 들어갈 수 있는지 혹은 입력이나 반환으로 어떤 데이터 유형을 취할 수 있는지 신중하게 생각해보면 이렇게 변경하는 것이 가능할 때가 있다.
코드가 오용되거나 잘못 설정되면 컴파일조차 되지 않도록 하는 것이 목표다.
컴파일러를 사용하여 코드 계약을 확인하는 것에 대한 대안으로 런타임 검사를 사용할 수 있다.
체크
코드 계약 조건을 확인하기 위한 일반적인 방법은 체크를 사용하는 것이다.
체크는 전제 조건 검사나 사후 상태 검사가 있으며 코드 계약이 준수되었는지 확인하기 위한 추가적인 로직이다.
가능하다면 처음부터 세부 조항은 피하는 것이 바람직하며 코드에 체크가 많이 있으면 세부 조항을 없애는 것에 대해 고려해봐야 한다.
어서션
어서션은 코드 계약을 준수하도록 강제하기 위한 방법이라는 점에서 체크와 매우 유사하다.
코드가 개발 모드에서 컴파일 되거나 테스트가 실행될 때, 어서션은 체크와 거의 같은 방식으로 동작한다.
조건이 위반되면 오류가 명백하게 보이거나 예외가 발생한다.
어서션과 체크 사이의 주요 차이점은 배포를 위해 빌드할 때 어서션은 보통 컴파일에서 제외된다는 점이다.

 Chapter 4. 오류

코드 실행되는 환경은 불완전하다.
모든 것이 잘못될 수 있고 잘못될 것이기 때문에 오류 사례를 신중하게 생각하지 않고는 견고하고 신뢰성 높은 코드를 작성할 수 없다.
오류에 대해 생각할 때 소프트웨어가 작동을 계속할 수 있는 오류와 작동을 계속할 합리적인 방법이 없는 오류로 구분하는 것이 유용할 때가 많다.

복구 가능성

복구 가능한 오류
일반적으로 시스템 외부의 무언가에 의해 야기되는 오류에 대해서는 대부분 시스템 전체가 표나지 않고 적절하게 처리하기 위해 노력해야 한다.
낮은 층위의 코드에서 오류를 시도하고 복구하는 것은 장점이 별로 없고, 오류 처리 방법을 알고 있는 더 높은 층위의 코드로 오류를 전송해야 하는 경우가 많다.
복구할 수 없는 오류
오류를 복구할 수 있는 방법이 없다면, 유일하게 코드가 할 수 있는 합리적인 방법은 피해를 최소화하고 개발자가 문제를 발견하고 해결할 가능성을 최대화하는 것이다.
이후 살펴볼 신속한 실패라는 개념과 요란한 실패라는 개념은 바로 이 대한 것이다.
호출하는 쪽에서만 오류 복구 가능 여부를 알 때가 많다
대부분의 오류는 한 코드가 다른 코드를 호출할 때 발생하고 코드는 종종 재사용되고 여러 곳에서 호출된다.
간결한 추상화 계층을 만들고자 한다면 일반적으로 코드의 잠재적 호출자에 대한 가정을 가능한 한 하지 않는 것이 좋다.
함수를 작성하거나 수정하는 시점에 오류로부터 복구할 수 있는지 혹은 복구해야 하는지 여부를 항상 알 수 있는 것은 아니기 때문이다.
자신의 코드가 어떻게 사용되어야 하는지에 대해 스스로에게는 명백해 보일 수 있을지라도 다른 사람들에게는 분명하지 않을 수도 있다는 점을 이해해야 한다.
호출하는 쪽에서 오류로부터 복구하기를 원할 것이라고 판단하는 것은 좋은 일이지만, 오류가 발생할 수 있다는 것조차 인식하지 못한다면 그것을 제대로 처리하지 못할 것이다.
호출하는 쪽에서 복구하고자 하는 오류에 대해 인지하도록 하라
다른 코드가 자신의 코드를 호출할 경우, 호출 시 오류가 발생한다는 것을 사전에 알 수 있는 실질적인 방법이 없는 경우가 많다.
따라서 함수 작성자는 이 함수에서 오류가 발생할 수 있다는 가능성을 호출하는 쪽에서 확실하게 인지하도록 해야 한다.
그렇지 않으면 이 함수를 호출하는 개발자가 오류를 처리하는 코드를 작성하지 않은 상태에서 오류가 발생하는 경우 개발자의 예상과는 다른 결과를 초래할 수 있다.

견고성 vs 실패

오류가 발생할 때, 다음 중 하나를 선택해야 한다.
실패, 더 높은 코드 계층이 오류를 처리하게 하거나 전체 프로그램의 작동을 멈추게 한다.
오류를 처리하고 계속 진행한다.
오류가 있더라도 처리하고 계속 진행하면 더 견고한 코드라고 볼 수 있지만, 오류가 감지되지 않고 이상한 일이 발생하기 시작한다는 의미도 될 수 있다.
신속하게 실패하라
신속하게 실패하기는 가능한 한 문제의 실제 발생 지점으로부터 가까운 곳에서 오류를 나타내는 것이다.
복구할 수 있는 오류의 경우 호출하는 쪽에서 오류로부터 훌륭하고 안전하게 복구할 수 있는 기회를 최대한으로 제공하고, 복구할 수 없는 오류의 경우 개발자가 문제를 신속하게 파악하고 해결할 수 있는 기회를 최대한 제공한다.
두 경우 모두 소프트웨어가 의도치 않게 잠재적으로 위험한 상태가 되는 것을 방지한다.
요란하게 실패하라
요란한 실패는 간단히 말하자면 오류가 발생하는데도 불구하고 아무도 모르는 상황을 막고자 하는 것이다.
이를 위한 가장 명백한 방법은 예외(또는 이와 유사한 것)를 발생해 프로그램이 중단되게 하는 것이다.
다른 방법은 오류 메시지를 기록하는 것인데 개발자가 얼마나 부지런하게 로그를 확인하는지, 혹은 로그에 방해되는 다른 메시지가 얼마나 있는지에 따라 오류 메시지가 무시될 수도 있다.
코드가 실패할 때 신속하고, 요란하게 오류를 나타내면 개발 도중이나 테스트하는 동안에 버그가 발견될 가능성이 크다.
그렇지 않더라도 배포된 후에 오류 보고를 보기 시작할 것이고 보고 내용으로부터 버그가 발생한 위치를 정확히 알 수 있는 이점이 있다.
복구 가능성의 범위
일반적으로 소프트웨어를 견고하게 작성하는 것이 좋다.
한 번의 잘못된 요청으로 인해 전체 서버의 동작이 멈추는 것은 바람직하지 않다.
그러나 오류를 알아차리지 못한 채 시스템이 계속 동작하지 않도록 하는 것 또한 중요하기 때문에 코드가 요란하게 실패해야 한다.
이 두 목표는 양립하지 못할 때가 많다.
가장 요란스럽게 실패하는 것은 프로그램이 멈추도록 하는 것이지만, 이것은 분명 소프트웨어를 견고하지 못하게 만든다.
이에 대한 해결책은 프로그래밍 오류가 발견되면 개발자가 이를 알아차릴 수 있도록 프로그래밍 오류를 기록하고 모니터링하는 것이다.
오류를 숨기지 않음
오류를 숨기는 것은 복구할 수 있는 오류와 복구할 수 없는 오류 모두에 문제를 일으킨다.
호출하는 쪽에서 복구하고자 할 수도 있는 오류를 숨기면, 호출하는 쪽에서 오류로부터 복구할 수 있는 기회를 없애는 것이다.
정확하고 의미 있는 오류 메시지를 표시하거나 다른 동작을 하는 대신, 잘못된 일이 일어날 수 있음을 전혀 알지 못하게 되고, 이것은 곧 소프트웨어가 의도한 대로 작동하지 않을 가능성이 크다는 것을 의미한다.
복구할 수 없는 오류를 숨기면 프로그래밍 오류가 감춰진다.
앞에서 신속하게 실패하는 것과 요란하게 실패하는 것에 대해 살펴봤듯이, 이러한 오류들은 개발팀이 알아야만 고칠 수 있다.
그것들은 숨긴다는 것은 개발팀이 그 오류에 대해 전혀 알지 못할 수도 있다는 것을 의미하며, 버그는 꽤 오랫동안 알아차리지 못한 채 그대로 남아 있을 수 있다.
이 두 경우 모두 에러가 발생하면 일반적으로 호출하는 쪽에서 예측한 대로 코드가 실행되지 않는다는 것을 의미한다.
오류를 감추면 호출하는 쪽에서는 모든 것이 잘 작동하고 있다고 가정하지만 실제로 코드는 제대로 동작을 못할 것이다.
잘못된 정보를 출력하거나 일부 데이터를 손상시키거나 마침내 작동이 멈출 수 있다.
기본값 반환
오류가 발생하고 함수가 원하는 값을 반환할 수 없는 경우 기본값을 반환하는 것이 간단하고 쉬운 해결책처럼 보일 때가 있다.
코드에 기본값을 두는 것이 유용한 경우가 있을 수 있지만, 오류를 처리할 때는 대부분의 경우 적합하지 않다.
잘못된 데이터로 시스템이 제대로 작동하지 못하게 만들고 오류가 나중에 이상한 방식으로 나타날 수 있기 때문에 신속한 실패와 요란한 실패의 원리를 위반하는 것이다.
널 객체 패턴
널 객체는 개념적으로 기본값과 유사하지만 이것을 더 확장해서 더 복잡한 객체(클래스 등)를 다룬다.
널 객체는 실제 반환값처럼 보이지만 모든 멤버 함수는 아무것도 하지 않거나 의미 없는 기본값을 반환한다.
디자인 패턴에 관한 한 이것은 양날의 검이다.
널 객체 패턴을 사용하는 것이 꽤 유용한 경우가 있지만, 오류 처리에 사용되는 것은 바람직하지 않다.
아무것도 하지 않음
코드가 무언가를 반환하지 않고 단지 어떤 작업을 수행하는 경우, 문제가 발생할 때 가능한 한 가지 옵션은 오류가 발생했다는 신호를 보내지 않는 것이다.
호출하는 쪽에서는 코드에서 작업이 의도대로 완료되었다고 가정하기 때문에 일반적으로 이렇게 하는 것은 바람직하지 않다.
오류가 발생할 때 이 오류를 기록하면 약간 개선된 것이긴 하지만 여전히 바람직하지 않은 코드다.

오류 전달 방법

오류가 발생하면 일반적으로 더 높은 계층으로 오류를 알려야 한다.
오류로부터 복구할 수 없는 경우 이는 일반적으로 프로그램의 훨씬 더 높은 계층에서 실행을 중지하고, 오류를 기록하거나 전체 프로그램의 실행을 종료하는 것을 의미한다.
오류로부터의 복구가 잠재적으로 가능한 경우, 일반적으로 즉시 호출하는 쪽(또는 호출 체인에서 한두 수준 위의 호출자)에 오류를 알려 정상적으로 처리할 수 있도록 해야 한다.
오류를 알리는 방법은 크게 두 가지 종류로 나뉜다.
명시적 방법
코드를 직접 호출한 쪽에서 오류가 발생할 수 있음을 인지할 수 밖에 없도록 한다.
그것을 처리하든, 이전 호출자에게 전달하든, 아니면 그냥 무시하든 간에 어떻게 처리할지는 호출하는 쪽에 달려 있다.
하지만 무엇을 하든 그것은 적극적인 선택의 결과다.
오류가 발생할 가능성이 코드 계약의 명확한 부분에 나타나 있기 때문에 오류를 모르고 넘어갈 수 있는 방법은 거의 없다.
암시적 방법
코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류를 신경 쓰지 않아도 된다.
오류가 발생할 수 있음을 알기 위해서는 문서나 코드를 읽는 등의 적극적인 노력이 필요하다.
문서에 오류가 언급되어 있다면 코드 계약의 숨겨진 세부 조항이다.
가끔 오류가 여기서조차 언급되지 않을 때도 있는데, 이 경우엔 오류가 계약 내용에 전혀 없는 것이 된다.
명시적 오류 전달 기법
암시적 오류 전달 기법
코드 계약에서의 위치
명확한 부분
세부 조항 혹은 아예 없음
호출하는 쪽에서 오류 발생 가능성에 대해 아는가?
그렇다.
알 수도 있고 모를 수도 있다.
기법의 예
검사 예외 널 반환 유형 (널 안전성의 경우) 옵셔널 반환 유형 리절트 반환 유형 아웃컴 반환 유형 (반환값 확인이 필수인 경우) 스위프트 오류
비검사 예외 매직값 반환 (피해야 한다) 프로미스 또는 퓨처 어서션 체크 (구현에 따라 달라짐) 패닉
요약: 예외
많은 프로그래밍 언어들은 예외라는 개념을 가지고 있다.
이것은 코드에서 오류나 예외적인 상황이 발생한 경우 이를 전달하기 위한 방법으로 고안되었다.
예외가 발생할 때 콜 스택을 거슬러 올라가는데 예외를 처리하는 코드를 만나거나, 더 이상 올라갈 콜 스택이 없을 때까지 그렇게 한다.
더 이상 올라갈 콜 스택이 없는 경우에는 오류 메시지를 출력하고 프로그램이 종료된다.
자바는 검사 예외와 비검사 예외의 개념을 모두 가지고 있다.
예외를 지원하는 대부분의 주요 언어는 비검사 예외만 가지고 있으므로 자바 이외의 거의 모든 언어에서 예외라는 용어는 일반적으로 비검사 예외를 의미한다.
명시적 방법: 검사 예외
컴파일러는 검사 예외에 대해 호출하는 쪽에서 예외를 인지하도록 강제적으로 조치하는데, 호출하는쪽에서는 예외 처리를 위한 코드를 작성하거나 자신의 함수 시그니처에 해당 예외 발생을 선언해야한다.
함수가 예외를 처리하지도 않고, 자신의 함수 시그니처에 선언하지도 않으면 코드는 컴파일되지 않는다.
따라서 검사 예외를 사용하는 것은 오류를 전달하기 위한 명시적인 방법이다.
암시적 방법: 비검사 예외
비검사 예외를 사용하면 다른 개발자들은 코드가 이 예외를 발생시킬 수 있다는 사실을 전혀 모를 수 있다.
이 경우에는 함수에서 어떤 예외를 발생시키는지 문서화하는 것이 바람직하지만 개발자가 문서화하는 것을 잊어버릴 때가 있다.
설사 문서화를 하더라도 이것은 코드 계약의 세부 조항이다.
앞에서 살펴본 것처럼 세부 조항은 코드 계약 내용을 전달하는데 있어 신뢰할 만한 방법이 아닐 때가 많다.
따라서 비검사 예외는 오류가 발생할 수 있다는 것을 호출하는 쪽에서 인지하리라는 보장이 없기 때문에 오류를 암시적으로 알리는 방법이다.
명시적 방법: 널값이 가능한 반환 유형
함수에서 널값을 반환하는 것은 특정값을 계산하거나 얻는 것이 불가능함을 나타내기 위한 효과적이고 간단한 방법이다.
사용 중인 언어가 널 안전성을 지원하는 경우 널값이 반환될 수 있다는 것을 호출하는 쪽에서 강제적으로 인지하고, 그에 따라 처리할 수 밖에 없다.
따라서 (널 안전성을 지원할 때) 널 값이 가능한 반환 유형을 사용하는 것은 오류를 전달하기 위한 명시적인 방법이다.
널 안전성을 지원하지 않는 언어를 사용하는 경우 옵셔널 반환 유형을 사용하는 것이 좋다.
명시적 방법: 리절트 반환 유형
널값이나 옵셔널 타입을 반환할 때의 문제 중 하나는 오류 정보를 전달할 수 없다는 것이다.
호출자에게 값을 얻을 수 없음을 알릴 뿐만 아니라 값을 얻을 수 없는 이유까지 알려주면 유용하다.
이러한 경우에는 리절트 유형을 사용하는 것이 적절할 수 있다.
언어가 리절트 유형을 지원하거나 혹은 (자신만의 리절트 유형을 정의할 때) 다른 개발자들이 그 유형에 익숙하다고 가정하면, 리절트 유형을 반환 유형으로 사용하는 것은 오류가 발생할 수 있다는 점을 분명히 하는 것이 된다.
따라서 리절트 반환 유형을 사용하는 것은 오류를 알리는 명시적인 방법이다.
명시적 방법: 아웃컴 반환 유형
어떤 함수들은 값을 반환하기보다는 단지 무언가를 수행하고 값을 반환하지는 않는다.
어떤 일을 하는 동안 오류가 발생할 수 있고 그것을 호출한 쪽에 알리고자 한다면, 함수가 수행한 동작의 결과를 나타내는 값을 반환하도록 함수를 수정하는 것이 한 가지 방법이 될 수 있다.
아웃컴 반환 유형을 반환할 때 호출하는 쪽에서 반환값을 강제적으로 확인해야 한다면 이것은 오류를 알리는 명백한 방법이다.
암시적 방법: 프로미스 또는 퓨처
비동기적으로 실행하는 코드를 작성할 때 프로미스나 퓨처(혹은 이와 동등한 개념)를 반환하는 함수를 작성하는 것이 일반적이다.
많은 언어에서 프로미스나 퓨처는 오류 상태도 전달할 수 있다.
프로미스나 퓨처를 사용할 때 일반적으로 오류 처리를 강제로 해야 하는 것은 아니고, 해당 함수에 대한 코드 계약의 세부 조항을 잘 알지 못하면 오류 처리 코드를 추가로 작성해야 한다는 것을 모를 수 있다.
따라서 프로미스나 퓨처를 사용한 오류 전달은 암시적인 방법이다.
암시적 방법: 매직값 반환
매직값(또는 오류 코드)은 함수의 정상적인 반환 유형에 적합하지만 특별한 의미를 부여하는 값이다.
매직값이 반환될 수 있다는 것을 알려면 문서나 코드를 읽어야 한다.
따라서 이것은 암시적 오류 전달 기법이다.
매직값을 사용하여 오류를 알리는 일반적인 방법은 -1을 반환하는 것이다.
매직값은 코드 계약의 명백한 부분을 통해 호출하는 쪽에 알릴 수 없어서 예상을 벗어나는 결과를 가져올 수도 있고 버그로 이어질 수 있다.

복구할 수 없는 오류의 전달

현실적으로 복구할 가능성이 없는 오류가 발생하면 아래와 같은 방법으로 신속하게 실패하고, 요란하게 실패하는 것이 최상의 방법이다.
비검사 예외를 발생
프로그램이 패닉이 되도록 (패닉을 지원하는 언어를 사용하는 경우)
체크나 어서션의 사용
이러한 경우 프로그램(또는 복구 불가능한 범위 내에서)이 종료되는데, 이는 개발자들이 뭔가 잘못되었음을 알아차린다는 것을 의미하고, 생성된 오류 메시지는 대개 스택 트레이스나 줄 번호를 제공하여 오류가 발생한 위치를 명확하게 알려준다.
오류를 복구할 방법이 없을 때는 이것이 합리적이다.
왜냐하면 이 경우 자신을 호출한 쪽에 오류를 전달하는 것 외에는 할 수 있는 방법이 없기 때문이다.

호출하는 쪽에서 복구하기를 원할 수도 있는 오류의 전달

호출하는 쪽에서 복구하기를 원할 수도 있는 오류를 전달하고자 할 때, 비검사 예외와 명시적 오류 전달 기법 중 어느 것을 사용해야 하는지에 대한 논쟁이 있다.
그 전에 먼저 기억해야 할 점은 팀이 동의한 철학이 다른 어떤 주장보다도 중요하다는 점이다.
비검사 예외를 사용해야 한다는 주장
코드 구조 개선
대부분의 오류 처리가 코드의 상위 계층에서 이루어질 수 있기 때문에 (명시적인 기술을 사용하는 대신) 비검사 예외를 발생시키면 코드 구조를 개선할 수 있다.
오류가 높은 계층까지 거슬러 올라오면서 전달되고, 그 사이에 있는 코드는 오류 처리를 할 필요가 없다.
중간에 있는 계층은 (특정 작업을 다시 시도하는 등) 원한다면 예외 중 일부를 처리할 수 있지만, 그렇지 않으면 오류가 최상위 오류 처리 계층으로 전달된다.
사용자 응용 프로그램이라면 오류 처리 계층은 오류 메시지를 UI에 표시할 수 있다.
서버나 백엔드 프로세스라면 오류 메시지가 기록될 수 있다.
이 접근법의 핵심 장점은 오류를 처리하는 로직이 코드 전체에 퍼지지 않고 별도로 몇 개의 계층에만 있다는 점이다.
개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함
개발자들이 너무 많은 명시적 오류 전달을 접하면 결국 잘못된 일을 한다.
명시적인 오류 전달 방식을 사용하면 코드의 계층을 올라가면서 오류를 반복적으로 전달하고 이를 처리하는 일련의 작업이 필요한데, 이런 번거로운 작업 대신 개발자는 편의를 도모하고 잘못된 작업을 하고 싶은 마음이 들 수 있다.
예를 들어 예외를 포착하고도 무시한다거나, 널이 가능한 유형을 확인도 하지 않고 널이 불가능한 유형으로 변환을 하는 것이다.
명시적 기법을 사용해야 한다는 주장
매끄러운 오류 처리
비검사 예외를 사용한다면 모든 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기가 어렵다.
예를 들어 사용자 입력이 잘못되면 해당 입력 필드 바로 옆에 오류 메시지를 보여주는 것이 타당하다.
입력을 처리하는 코드를 작성하는 엔지니어가 오류 시나리오를 알지 못하고 더 높은 수준으로 전달되도록 내버려 둔다면, 이는 사용자 친화적이지 않은 오류 메시지를 UI에 표시할 수 있다.
실수로 오류를 무시할 수 없다
어떤 호출자의 경우에는 실제로 오류를 처리해야 하는 경우가 있을 수 있다.
비검사 예외가 사용되면 적극적인 의사 결정이 들어갈 여지는 줄어들고 대신 기본적으로 잘못된 일(오류를 처리하지 않는 일)이 일어나기 쉽다.
좀 더 명확한 오류 전달 방식을 사용하면 잘못된 일이 기본적으로 혹은 실수로 인해 일어나지 않는다.
개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함
비검사 예외가 코드베이스 전반에 걸쳐 제대로 문서화된다는 보장이 없고, 개인적인 경험에 비추어 보면 문서화되지 않는 경우가 많다.
이는 종종 어떤 코드가 어떤 예외를 발생시킬 것인지 확실하게 알지 못한다는 것을 의미하고, 이로 인해 예외를 처리하는 것이 당황스러운 두더지 잡기 게임이 될 수 있다.
비검사 예외를 사용하든 명시적인 오류 전달 처리 방식을 사용하든 어느 경우라도 문제가 있을 수 있다는 것을 알아야 한다.
필자의 의견: 명시적 방식을 사용하라
필자의 경험상 비검사 예외의 사용은 코드베이스 전반에 걸쳐 완전히 문서화되는 경우가 매우 드물며, 이것이 의미하는 바는 해당 함수에 대해 발생 가능한 오류와 이에 대한 처리를 어떻게 해야 하는지 개발자가 확실하게 알기란 거의 불가능하다는 것이다.
더 바람직하지 않은 상황은 팀 내에서 각자 개발자들이 서로 다른 접근 방식을 사용하는 것이다.
따라서 팀원들이 오류 전달에 대한 철학에 동의하고 그것을 따르는 것이 가장 바람직하다.

컴파일러 경고를 무시하지 말라

컴파일러는 오류뿐만 아니라 대부분의 컴파일러는 경고 메시지도 출력한다.
컴파일러 경고는 어떤 식으로든 코드가 의심스러우면 표시를 하는데, 이것은 버그에 대한 조기 경고일 수 있다.
이러한 경고에 주의를 기울이면 코드베이스에 병합되기 훨씬 전에 코드로부터 프로그래밍 오류를 발견하고 제거할 수 있다.

 Chapter 5. 가독성 높은 코드를 작성하라

가독성의 핵심은 개발자가 코드의 기능을 빠르고 정확하게 이해할 수 있도록 하는 것이다.
실제로 이렇게 하려면 다른 사람의 관점에서 보았을 때, 코드가 혼란스럽거나 잘못 해석될 수 있는지를 상상하고 공감해야 할 때가 많다.

서술형 명칭 사용

코드에서 이름을 지을 때 클래스, 함수, 변수와 같은 것들을 고유하게 식별하기 위해 이름이 필요하다.
하지만 이름을 붙이는 것은 그것이 스스로 설명되는 방식으로 언급함으로써 읽기 쉬운 코드 작성을 위한 기회이기도 하다.

주석문의 적절한 사용

서술적이지 않은 이름은 코드를 읽기 어렵게 만들고, 이를 개선할 수 있는 한 가지 방법은 주석문과 문서를 추가하는 것이다.
하지만 여전히 다음과 같은 많은 문제가 있다.
코드가 훨씬 더 복잡해 보인다. 개발자는 코드를 이해하기 위해 파일을 계속해서 위아래로 스크롤해야 한다.
작성자와 다른 개발자는 코드뿐만 아니라 주석문과 문서도 유지보수해야 한다.
서술적인 이름을 사용하면 이해하기 어려운 코드가 갑자기 이해하기 쉬운 코드로 바뀐다.
코드 내에서 주석문이나 문서화는 다음과 같은 다양한 목적을 수행할 수 있다.
코드가 무엇을 하는지 설명
코드가 왜 그 일을 하는지 설명
사용 지침 등 기타 정보 제공
클래스와 같이 큰 단위의 코드가 무엇을 하는지 요약하는 높은 수준에서의 주석문은 유용하다.
서술적인 이름으로 잘 작성된 코드는 그 자체로 줄 단위에서 무엇을 하는지 설명된다.
다음과 같은 경우에는 주석문을 사용해 코드가 존재하는 이유를 설명하면 좋다.
제품 또는 비즈니스 의사 결정
이상하고 명확하지 않은 버그에 대한 해결책
의존하는 코드의 예상을 벗어나는 동작에 대처
코드 기능에 대한 상위 수준에서의 개략적인 문서는 다음과 같은 경우에 유용하다.
클래스가 수행하는 작업 및 다른 개발자가 알고 있어야 할 중요한 세부 사항을 개괄적으로 설명하는 문서
함수에 대한 입력 매개변수 또는 기능을 설명하는 문서
함수의 반환값이 무엇을 나타내는지 설명하는 문서
주석과 문서화는 코드만으로는 전달할 수 없는 세부 사항을 설명하거나 코드가 큰 단위에서 하는 일을 요약하는데 유용하다.
단점으로는 이런 주석문과 문서도 유지 및 보수가 필요하고 내용이 제때 업데이트되지 않으면 코드와 맞지 않게 되고 코드가 지저분해질 수 있다.
따라서 주석과 문서화를 효과적으로 사용하기 위해서는 이런 장단점 사이에서 균형 잡힌 접근법이 필요하다.

코드 줄 수를 고정하지 말라

일반적으로 코드베이스의 줄 수는 적을수록 좋다.
코드는 일반적으로 어느 정도의 지속적인 유지보수를 필요로 하며 코드의 줄이 많다는 것은 코드가 지나치게 복잡하거나, 기존 코드를 재사용하지 않고 있다는 신호일 수 있다.
또한, 코드 줄이 많으면 읽어야 할 코드의 양이 늘어나기 때문에 개발자의 인지 부하가 증가할 수 있다.
그러나 코드 줄 수는 우리가 실제로 신경 쓰는 것들을 간접적으로 측정해줄 뿐이다.
우리가 정말로 신경 쓰는 것은 코드에 대해 다음과 같은 사항들을 확실하게 하는 것이다.
이해하기 쉽다.
오해하기 어렵다.
실수로 작동이 안 되게 만들기가 어렵다.
간결하지만 이해하기 어려운 코드는 피하라
많은 세부 사항과 가정을 매우 간결한 단 한 줄의 코드로 압축할 때 다음과 같은 문제가 있다.
다른 개발자는 이 단 한 줄의 코드에서 이 모든 세부 사항과 가정을 도출하기 위해 많은 노력을 기울여야 한다.
이로 인해 그들의 시간이 낭비되고, 또한 코드를 오해하고 코드를 수정할 때 작동하지 않게 될 가능성이 커진다.
이런 가정은 다른 코드에서 이루어진 가정과 일치해야 한다.
하나의 가정을 로직의 유일한 최종 원천(single source of truth)으로 재사용하는 것이 더 나을 것이다.

일관된 코딩 스타일을 고수하라

일반적인 코딩 스타일 규칙에 의하면 클래스 이름은 파스칼 케이스로, 변수 이름은 카멜 케이스로 작성해야 한다.
이 규약을 따른다면 인스턴스 변수와 클래스를 헷갈리지 않고 분명하게 알 수 있다.
코딩 스타일은 명명법이상의 많은 측면을 다루는데 다음과 같다.
언어의 특정 기능 사용
코드 들여쓰기
패키지 및 디렉토리 구조화
코드 문서화 방법
팀이나 조직 전체가 같은 코딩 스타일을 따르면, 그것은 마치 모두가 같은 언어를 유창하게 말하는 것과 비슷하다.
서로 오해할 위험이 크게 줄어들고, 이로 인해 버그가 줄어들고, 혼란스러운 코드를 이해하는 데 낭비하는 시간이 줄어든다.

깊이 중첩된 코드를 피하라

인간의 눈은 각 코드 라인의 중첩 수준이 정확히 어느 정도인지 추적하는 데 능숙하지 않다.
이로 인해 코드를 읽을 때, 다른 논리가 실행되는 때를 정확히 이해하기 어렵다.
중첩이 깊어지면 가독성이 떨어지기 때문에 중첩을 최소화하도록 코드를 구성하는 것이 바람직하다.
중첩된 모든 블록에 반환문이 있을 때, 중첩을 피하기 위해 논리를 재배치하는 것이 일반적으로 아주 쉽다.
그러나 중첩된 블록에 반환문이 없다면, 그것은 대개 함수가 너무 많은 일을 하고 있다는 신호다.
많은 일을 하는 코드에 중첩마저 많을 때, 이 함수를 쪼개는 것은 두 배로 중요해진다.
왜냐하면 중첩을 제거하기 위해서는 나누는 작업을 먼저 해야 하기 때문이다.

함수 호출도 가독성이 있어야 한다

어떤 함수의 이름이 잘 명명되면 그 함수가 무슨 일을 하는지 분명하지만 이름이 잘 지어졌더라도 함수의 인수가 무엇을 위한 것이고, 무슨 역할을 하는지 명확하지 않다면 함수 호출 자체가 이해되지 않을 수 있다.
명명된 매개변수는 점점 더 많은 언어, 특히 최근에 나온 언어에서 지원되고 있다.
함수 호출에서 명명된 인수를 사용할 때, 인수 목록 내의 위치가 아닌 이름으로 일치하는 매개변수를 찾는다.
명명된 인수를 사용한다면, 함수 정의를 확인하지 않고도 함수에 대한 호출은 쉽게 이해할 수 있다.
안타깝지만 모든 언어가 명명된 매개변수를 지원하는 것이 아니므로, 객체 구조 분해를 사용하면 명명된 매개변수인 것처럼 만들 수 있다.
단일 객체를 매개변수로 허용하게 하고 이 객체는 즉시 매개변수의 속성으로 구조 분해되고 함수 내에서 이 속성을 직접 읽을 수 있다.
사용하는 프로그래밍 언어의 명명된 매개변수 지원 여부와 상관없이 함수를 정의할 때 좀 더 서술적인 유형을 사용하는 것이 바람직하다.
클래스 : 우선순위를 클래스로 표현한다.
열거형 : 정책은 옵션이 있는 열거형을 사용한다.
때로는 함수를 호출하는 라인의 가독성을 높여주는 특별한 방법이 없을 때가 있다.
여기서 최선의 방법은 생성자를 호출할 때 각각의 인수가 무엇인지 설명하기 위해 인라인 주석문을 사용하는 것이다.
인라인 주석문은 분명히 생성자 호출 코드의 가독성을 높이지만 이렇게 코드를 작성할 때 실수를 하지 않아야 하고, 이런 주석문을 최신 상태로 계속 유지해야 하므로 그다지 만족스러운 해결책은 아니다.
세터 함수를 추가하거나 빌더 패턴과 같은 것을 사용하는 것이 대안이 될 수 있지만, 두 방법 모두 값이 누락된 채 인스턴스가 만들어질 수 있기 때문에 코드가 쉽게 오용될 수 있는 단점이 있다.

설명되지 않은 값을 사용하지 말라

하드 코드로 작성된 값이 필요한 경우가 많이 있는데, 몇 가지 일반적인 예는 다음과 같다.
한 수량을 다른 수량으로 변환할 때 사용하는 계수
작업이 실패할 경우 재시도의 최대 횟수와 같이 조정 가능한 파라미터 값
어떤 값이 채워질 수 있는 템플릿을 나타내는 문자열
하드 코드로 작성된 모든 값에는 두 가지 중요한 정보가 있다.
값이 무엇인지: 컴퓨터가 코드를 실행할 때 이 값을 알아야 한다.
값이 무엇을 의미하는지: 개발자가 코드를 이해하려면 값의 의미를 알아야 한다. 이 정보가 없으면 코드를 이해할 수 없다.
설명이 없는 값은 다른 개발자들이 그 값이 왜 거기에 있고, 무슨 일을 하는지 이해하지 못하기 때문에 코드의 가독성을 떨어트린다.
코드에 설명되지 않은 값이 있으면 혼란을 초래하고 이로 인해 버그가 발생할 수 있다.
그 값이 무엇을 의미하는지를 다른 개발자들에게 명확하게 해주는 것이 중요하다.
잘 명명된 상수를 사용하라
값을 설명하기 위해 할 수 있는 한 가지 간단한 방법은 상수를 정의하고 상수 이름을 통해 값을 설명하는 것이다.
코드에서 값을 직접 사용하는 대신 상수를 사용하면 이것은 곧 상수 이름이 코드를 설명한다는 것을 의미한다.
잘 명명된 함수를 사용하라
코드의 가독성을 높이기 위해 함수를 사용할 수 있는 방법은 두 가지가 있다.
상수를 반환하는 공급자 함수
변환을 수행하는 헬퍼 함수
마지막으로 여러분이 정의한 값이나 헬퍼 함수를 다른 개발자들이 재사용할 것인지 고려해볼 만한 가치가 있다.
그럴 가능성이 있다면 현재 사용 중인 클래스보다는 별도의 유틸리티 클래스에 두는 것이 좋다.

익명 함수를 적절하게 사용하라

익명 함수는 이름이 없는 함수이며, 일반적으로 코드 내의 필요한 지점에서 인라인으로 정의된다.
단 하나의 문장이면 충분하고, 해결하려는 문제가 간단하다면 익명 함수를 사용하는 것이 괜찮다.
하지만 명명 함수를 정의하는 것이 코드 재사용성 관점에서 유용하다면 익명 함수보다는 명명 함수를 작성하는 것이 더 나을 수 있다.
익명 함수는 정의상 이름이 없기 때문에 그 익명 함수 코드를 읽는 사람에게 어떠한 것도 제공하지 않는다.
그것이 얼마나 간단한 것이든 간에 익명 함수의 내용이 자명하지 않다면 코드의 가독성은 떨어지기 마련이다.
익명 대신 명명을 사용하는 것의 단점은 더 많은 코드를 작성해야 한다는 것이다.
반면에 익명 함수는 명명 함수 작성 시 항상 사용해야만 하는 문장을 작성하지 않아도 되기 때문에 코드를 줄이는 데는 뛰어나지만, 함수에 더 이상 이름이 없다는 단점이 있다.
간단하고 자명한 논리는 익명 함수를 써도 일반적으로 괜찮지만, 복잡한 논리는 명명 함수를 쓰는 것이 이점이 더 많다.
함수형 스타일의 프로그래밍을 사용하면 코드의 가독성이 좋아지고 코드를 더 견고하게 만드는 이점이 있다.
함수 스타일을 채택한다고 해서 반드시 인라인 익명 함수를 사용해야 하는 것은 아니며, 명명 함수를 사용하여 함수 스타일의 코드를 쉽게 작성할 수 있다.
일부 개발자들은 너무 많은 논리와 때로는 다른 익명 함수들을 중첩해서 가지고 있는 거대한 익명 함수를 생성한다.
익명 함수가 두세줄 이상으로 늘어나기 시작할 때, 여러 개의 명명 함수로 분리하면 코드의 가독성이 좋아진다.
한꺼번에 너무 많은 일을 하는 큰 함수를 분리하는 것은 코드의 가독성을 (그리고 재사용성 및 모듈화까지도) 개선하기 위한 좋은 방법이다.
함수형 스타일의 코드를 작성할 때 이 점을 잊지 말아야 한다.
익명 함수가 길어지고 복잡해지면, 로직을 더 작은 단위의 명명 함수로 작성해야 한다.

프로그래밍 언어의 새로운 기능을 적절하게 사용하라

프로그래밍 언어 설계자는 새로운 기능을 추가하기 전에 매우 신중하게 생각하기 때문에 새로운 기능으로 인해 코드의 가독성이 높아지거나 코드가 더 견고해지는 경우가 많이 있을 수 있다.
하지만 프로그래밍 언어의 그런 새로운 기능을 사용하고 싶은 마음이 간절히 들 때, 그것이 정말로 그 일에 가장 적합한 도구인지 솔직하게 생각해봐야 한다.
일반적으로 코드 품질을 개선한다면 언어가 제공하는 기능을 사용하는 것이 바람직하다.
그러나 개선 사항이 적거나 다른 개발자가 그 기능에 익숙하지 않다면 차라리 사용하지 않는 것이 좋을 때도 있다.
프로그래밍 언어에서 새로운 기능은 다 이유가 있기에 추가된다.
이런 새로운 기능을 사용하면 큰 이점이 있을 수 있지만 코드 작성에서와 동일하게 해당 기능이 단지 새로워서가 아니라 작업에 적합한 도구이기 때문에 사용한다는 점을 분명히 해야 한다.

 Chapter 6. 예측 가능한 코드를 작성하라

예측 가능한 코드를 작성하는 것은 무언가를 분명하게 하는 것일 때가 많다.
함수가 아무것도 반환하지 않을 때가 있거나 처리해야 할 특별한 시나리오가 있는 경우 이 사실을 다른 개발자에게 확실하게 알려야 한다.
그렇지 않으면 코드의 동작이 그들이 실제 일어날 것이라고 생각하는 것과 일치하지 않을 위험이 있다.

매직값을 반환하지 말아야 한다

매직값은 함수의 정상적인 반환 유형에 들어맞기 때문에 이 값이 갖는 특별한 의미를 인지하지 못하고, 이에 대해 적극적으로 경계하지 않으면 정상적인 반환값으로 오인하기 쉽다.
값이 없을 수 있는 경우 이것이 코드 계약의 명백한 부분에서 확인할 수 있도록 하는 것이 훨씬 좋다.
이를 위한 쉬운 방법 중 하나는 널 안전성을 지원하는 경우에는 널이 가능한 유형을 반환하고, 널 안전성을 지원하지 않는 경우에는 옵셔널 값을 반환하는 것이다.
이를 통해 호출하는 쪽에서 값이 없을 수 있음을 인지하고 적절한 방법으로 이를 처리할 수 있게 해야 한다.
널값이나 비어 있는 옵셔널을 반환하는 것의 단점은 값이 없는 이유를 명시적으로 전달하지 않는다는 점이다.

널 객체 패턴을 적절히 사용하라

값을 얻을 수 없을 때 널값이나 빈 옵셔널을 반환하는 대신 널 객체 패턴을 사용할 수 있다.
널 객체 패턴을 사용하는 이유는 널값을 반환하는 대신 유효한 값이 반환되어 그 이후에 실행되는 로직에서 널값으로 인해 시스템에 피해가 가지 않도록 하기 위함이다.
이것의 가장 간단한 형태는 빈 문자열이나 빈 리스트를 반환하는 것이지만, 더 정교한 형태로는 모든 멤버 함수가 아무것도 하지 않거나 기본값을 반환하는 클래스를 구현하는 것이 있다.
빈 컬렉션을 반환하면 코드가 개선될 수 있다
함수가 리스트, 집합, 배열과 같은 컬렉션을 반환할 때 컬렉션의 값을 얻을 수 없는 경우가 있다.
값이 지정이 안 됐다거나 주어진 상황에서 컬렉션에 값이 없을 수 있다.
이 경우 한 가지 방법은 널값을 반환하는 것이다.
널값이 아닌 널이 가능한 유형을 반환하면 호출하는 쪽에서는 반환된 값이 널인지 여부를 확인한 후 사용해야 한다.
하지만 이것 역시 별 이점 없이 코드만 지저분하게 만드는데 널 값의 경우와 동일하게 차이를 구분할 일이 거의 없기 때문이다.
따라서 이런 상황에서는 널 객체 패턴을 사용해서 코드를 개선할 수 있다.
호출하는 쪽에서는 널값인지 확인할 필요가 없다.
빈 문자열을 반환하는 것도 때로는 문제가 될 수 있다
일부 개발자들은 널 대신 빈 컬렉션을 반환하는 것이 문자열에도 적용되어야 한다고 주장하는데, 이 경우 널 대신 빈 문자열이 반환되어야 한다는 것이다.
이 방식이 적절한지의 여부는 문자열이 어떻게 사용되는지에 달려있다.
어떤 경우에는 문자열이 문자들을 모아 놓은 것에 지나지 않으며, 이 경우 널 대신 빈 문자열을 반환하는 것이 적절할 수 있다.
문자열이 이것을 넘어서는 의미를 지닐 때, 널 대신 빈 문자열을 반환하는 것이 문제가 될 수 있다.
더 복잡한 널 객체는 예측을 벗어날 수 있다
함수를 호출할 때 널 객체 패턴을 사용하는 것은 본질적으로 빈 상자를 파는 것과 같다.
호출하는 쪽에서 빈 상자를 받고 놀라거나 황당해할 가능성이 있다면, 널 객체 패턴을 피하는 것이 가장 좋을 것이다.
널 객체 패턴의 더 복잡한 형태 중 하나는 클래스를 만들고 무해한 (정확히는 무해하다고 가정하는) 값을 클래스 안에 두는 것이다.
널 객체 구현은 예상을 벗어나는 동작을 유발할 수 있다
일부 개발자들은 널 객체 패턴에서 한 단계 더 나아가 널 객체 전용의 인터페이스나 클래스를 정의한다.
인터페이스나 클래스가 단순히 무언가를 반환하는 기능보다는 무언가를 수행하는 기능을 가지고 있을 때 이런 것이 필요한 것처럼 보일 수 있다.
그러나 호출하는 쪽에서 이 내용을 확인하고 싶어할지가 전혀 명확하지 않기 때문에 그다지 큰 개선 사항은 아니다.
호출하는 쪽에서 널 객체 전용 인스턴스인지 확인하도록 요구하는 것은 어색하고 널을 확인하는 것보다 더 번거로울 수도 있다.
널 객체 패턴은 여러 형태로 나타날 수 있다.
이것을 사용하거나 접할 때 그것이 정말 적절한지 아니면 예상을 벗어나는 동작을 할 가능성이 있는지에 대해 의식적으로 생각해봐야 한다.
널 안전성과 옵셔널을 사용하는 것이 인기를 얻음에 따라 ‘값이 없음’을 훨씬 쉽고 안전하게 나타낼 수 있게 되었다.

예상치 못한 부수 효과를 피하라

부수 효과는 어떤 함수의 호출이 함수 외부에 초래한 상태 변화를 의미한다.
함수가 반환하는 값 외에 다른 효과가 있다면 이는 부수 효과가 있는 것이다.
일반적인 부수 효과 유형은 다음과 같다.
사용자에게 출력 표시
파일이나 데이터베이스에 무언가를 저장
다른 시스템을 호출하여 네트워크 트래픽 발생
캐시 업데이트 혹은 무효화
부수 효과는 소프트웨어 작성 시 불가피하고 부수 효과가 없는 소프트웨어는 무의미할 것이다.
어느 시점에서 사용자, 데이터베이스, 다른 시스템 등에 무언가를 출력해야 한다는 것은 적어도 코드의 일부에서는 부수 효과가 있어야 한다는 것을 의미한다.
부수 효과가 예상되고 코드를 호출한 쪽에서 그것을 원한다면 괜찮지만, 부수 효과가 예상되지 않을 경우 놀라움을 유발하고 버그로 이어질 수 있다.
부수 효과를 피하거나 그 사실을 분명하게 하라
정보를 얻는 함수는 일반적으로 부수 효과를 일으키지 않기 때문에 개발자의 자연스러운 정신 모델에서는 그러한 함수들이 부수 효과를 일으키지 않을 것이라고 가정한다.
따라서 어떤 함수가 부수 효과를 일으킨다면, 그 함수를 호출하는 쪽에서 이 사실에 대해 명백하게 알 수 있도록 하는 책임이 함수의 작성자에게 있다.
애초에 부수 효과를 일으키지 않는 것이 예측 가능한 코드를 위해 가장 좋은 방법이지만, 실제로 그렇게 하기 어려운 경우도 있다.
부수 효과를 피할 수 없을 때 적절하게 이름을 짓는 것은 그 사실을 명백하게 나타내는 매우 효과적인 방법이다.

입력 매개변수를 수정하는 것에 주의하라

입력으로 받은 객체에는 그 함수에 필요한 정보가 있지만, 이 함수가 호출된 뒤에도 해당 객체를 다른 용도로 사용할 가능성이 크다.
입력 매개변수를 수정하는 것은 함수가 외부의 무언가에 영향을 미치기 때문에 부수 효과의 또 다른 예다.
함수는 매개변수를 통해 입력을 가져오거나 빌려와서 반환값을 통해 결과를 제공하는 것이 일반적이다.
따라서 대부분의 개발자는 입력 매개변수의 수정이 일어나지 않을 것이라고 예상하고 이 부수 효과가 일어난다면 깜짝 놀랄 것이다.
변경하기 전에 복사하라
입력 매개변수 내의 값을 어쩔 수 없이 변경해야 하는 경우에는 변경 전에 새 자료구조에 복사하는 것이 최상의 방법이다.
값을 복사하면 메모리나 CPU, 혹은 두 가지 모두와 관련해 성능에 영향을 미칠 수 있다.
하지만 입력 매개변수의 변경으로 인해 발생할 수 있는 예기치 못한 동작이나 버그와 비교하면 성능이 크게 문제되지 않는 경우가 많다.
그러나 매우 많은 양의 데이터를 처리하거나, 저가 하드웨어에서 실행될 가능성이 있는 경우 입력 매개변수에 변경이 가해지는 것을 피하기 어려운 경우도 있다.

오해를 일으키는 함수는 작성하지 말라

개발자가 어떤 함수를 호출하는 코드를 접하면 그들은 자신들이 보는 것에 기초하여 함수 내부에서 무슨 일이 일어나고 있는지에 대해 생각한다.
코드 계약의 명백한 부분(예: 이름)은 개발자가 코드를 살펴볼 때 주로 인식하게 되는 항목이다.
중요한 입력이 누락되었을 때 아무것도 하지 않으면 놀랄 수 있다
매개변수가 없더라도 호출할 수 있고 해당 매개변수가 없으면 아무 작업도 수행하지 않는 함수가 있다면, 이 함수가 수행하는 작업에 대해 오해의 소지가 있을 수 있다.
호출하는 쪽에서는 해당 매개변수의 값을 제공하지 않고 함수를 호출하는 것의 심각성을 모를 수 있으며, 코드를 읽는 사람은 함수 호출 시 항상 무언가 작업이 이루어진다고 잘못 생각할 수 있다.
중요한 입력은 필수 항목으로 만들라
중요한 매개변수가 널이 가능한 값을 받아들일 수 있게 하면 호출하는 쪽에서는 호출하기 전에 널값 여부를 확인할 필요가 없다.
이렇게 하면 호출하는 쪽의 코드는 간단해지는 반면 오해를 초래할 수 있고 버그가 발생할 가능성이 크게 증가하는데 일반적으로 바람직한 절충은 아니다.
어떤 매개변수 없이는 함수가 수행하려는 작업을 못 하는 경우 그 매개변수는 해당 함수에 중요하다.
이러한 매개변수에 대해서는 값을 사용할 수 없는 경우 함수를 호출할 수 없도록 널을 허용하지 않는 것이 더 안전할 수 있다.
널 확인 코드를 호출하는 쪽에 추가할 경우 코드 줄의 수가 증가할 수 있지만 (특히 호출이 많은 경우), 코드가 잘못 해석되거나 예상과 다른 동작을 할 가능성은 줄어들 수 있다.
예상을 벗어나는 코드 때문에 발생한 버그를 수정하는데 드는 시간과 노력은 널 여부를 확인하는 문장을 읽는데 드는 시간보다 훨씬 더 크다.
코드를 명확하게 작성하는 것의 이점은 코드를 몇 줄 더 추가하는 비용을 훨씬 더 능가한다.

미래를 대비한 열거형 처리

열거형을 처리해야 하는 경우 나중에 열거형에 더 많은 값이 추가될 수 있다는 점을 기억하는 것이 중요하다.
이것을 무시하고 코드를 작성하면, 자기 자신 혹은 다른 개발자들의 예측을 벗어나는 좋지 않은 결과를 초래할 수 있다.
때때로 개발자들은 열거형 내의 현재 값을 보고 if문을 사용해서 처리할 수 있겠다고 생각한다.
이것은 열거형의 현재 값에 대해서는 문제가 없지만, 나중에 다른 값이 추가될 경우에는 문제가 될 수 있다.
모든 경우를 처리하는 스위치 문을 사용하라
문제점은 열거형의 일부 값을 명시적이 아닌 암시적인 방식으로 처리한다는 점이다.
더 나은 접근법은 모든 열것값을 명시적으로 처리하고, 처리되지 않은 새로운 열것값이 추가되는 경우 코드 컴파일이 실패하거나 테스트가 실패하게 하는 것이다.
이를 위한 일반적인 방법은 모든 경우를 다 처리하는 스위치 문을 사용하는 것이다.
기본 케이스를 주의하라
스위치 문은 일반적으로 처리되지 않은 모든 값에 대해 적용할 수 있는 기본 케이스를 지원한다.
열거형을 처리하는 스위치 문에 기본 케이스를 추가하면 향후 열거형 값이 암시적으로 처리될 수 있으며 잠재적으로 예기치 않은 문제와 버그가 발생할 수 있다.

이 모든 것을 테스트로 해결할 수는 없는가?

예상을 벗어나는 코드를 방지하기 위한 코드 품질 향상 노력에 반대하는 주장을 하는 사람들이 가끔 있다.
테스트가 이러한 모든 문제를 잡아낼 것이기 때문에 이런 노력은 시간 낭비라는 것이다.
코드를 작성하는 그 시점에는 코드를 어떻게 테스트할지 제어할 수 있다.
모든 테스트 케이스를 다 처리하고 테스트 관련 지식이 풍부하며 코드에 대한 모든 올바른 행동과 가정을 확인하는, 거의 완벽한 테스트 코드를 작성할 수 있다.
그러나 예상을 벗어나는 코드를 피하는 것은 여러분이 작성한 코드에 대한 기술적 정확성 때문만은 아니다.
다른 개발자가 여러분의 코드를 사용해 코드 작성을 할 때 역시 그 코드가 올바르게 작동하도록 하기 위한 작업이다.
테스트는 매우 중요하다.
아무리 많은 코드 구조화나 코드 계약에 대한 걱정도 고품질의 철저한 테스트를 대체할 수 없다.
그러나 그 반대 역시 사실이다.
직관적이지 않거나 예상을 벗어나는 코드에 숨어 있는 오류를 테스트만으로는 방지하기 어렵다.

 Chapter 7. 코드를 오용하기 어렵게 만들어라

코드가 오용하기 쉽게 작성된다면, 조만간 오용될 가능성이 있고 소프트웨어가 올바르게 작동하지 않을 것이다.
비합리적이거나 애매한 가정에 기반해서 코드가 작성되거나 다른 개발자가 잘못된 일을 하는 것을 막지 못할 때 코드는 오용되기 쉽다.
코드를 잘못 사용할 수 있는 몇 가지 일반적인 경우는 다음과 같다.
호출하는 쪽에서 잘못된 입력을 제공
다른 코드의 부수 효과(입력 매개변수 수정 등)
정확한 시간이나 순서에 따라 함수를 호출하지 않음
관련 코드에서 가정과 맞지 않게 수정이 이루어짐

불변 객체로 만드는 것을 고려하라

불변성이 바람직한 이유를 이해하기 위해서는 그 반대인 가변 객체가 어떻게 문제를 일으킬 수 있는지 고려해야 한다.
가변 객체는 추론하기 어렵다.
코드를 작성할 때 만약 객체가 불변이라면, 객체를 여기저기에 전달하더라도 어디서도 그 객체가 변경됐거나 무언가 추가되지 않았다는 것을 확신할 수 있다.
가변 객체는 다중 스레드에서 문제가 발생할 수 있다.
객체가 가변적이면 해당 객체를 사용하는 다중 스레드 코드가 특히 취약할 수 있다.
한 스레드가 객체를 읽는 동안 다른 스레드가 그 객체를 수정하는 경우 오류가 발생할 수 있다.
예를 들어 한 스레드가 리스트에서 마지막 요소를 제거하는 동안 다른 스레드가 그 요소를 읽으려는 경우다.
객체를 불변으로 만드는 것이 항상 가능하지도 않고, 또 항상 적절한 것도 아니다.
필연적으로 상태 변화를 추적해야 하는 경우도 있고 이때는 가변적인 자료구조가 필요하다.
하지만 가변적인 객체는 코드의 복잡성을 늘리고 문제를 일으킬 수 있기 때문에, 기본적으로는 불변적인 객체를 만들되 필요한 곳에서만 가변적이 되도록 하는 것이 바람직하다.
객체를 생성할 때만 값을 할당하라
모든 값이 객체의 생성 시에 제공되고 그 이후로는 변경할 수 없도록 함으로써 클래스를 불변적으로 만들 수 있고 오용도 방지할 수 있다.
불변성에 대한 디자인 패턴을 사용하라
클래스에서 세터 함수를 제거하고 멤버 변수를 파이널로 표시하면 클래스가 불변적이 되고 버그를 방지할 수 있다.
일부 값이 반드시 필요하지 않거나 불변적인 클래스의 가변적 버전을 만들어야 하는 경우, 클래스를 보다 다용도로 구현해야 할 필요가 있다.
빌더 패턴
클래스를 구성할 수 있는 일부 값이 선택 사항인 경우 생성자를 통해 해당 값을 모두 설정하는 것은 상당히 까다로울 수 있다.
이 경우에 세터 함수를 추가하여 클래스를 변경할 수 있도록 만드는 것보다는 빌더 패턴을 사용하는 것이 더 낫다.
쓰기 시 복사 패턴
클래스의 인스턴스를 변경해야 하는 경우도 있다.
쓰기 시 복사 패턴의 경우 값을 변경하면 클래스의 새 인스턴스가 생성되며, 이 새로운 인스턴스에는 원하는 변경 사항이 반영되지만 기존 인스턴스는 수정되지 않는다.

객체를 깊은 수준까지 불변적으로 만드는 것을 고려하라

클래스가 실수로 가변적으로 될 수 있는 일반적인 경우는 깊은 가변성 때문이다.
이 문제는 멤버 변수 자체가 가변적인 유형이고 다른 코드가 멤버 변수에 액세스할 수 있는 경우에 발생할 수 있다.
방어적으로 복사하라
클래스의 어떤 객체에 대한 참조를 클래스 외부에서도 가지고 있으면 깊은 가변성과 관련된 문제가 발생할 수 있다.
클래스가 참조하는 객체가 클래스 외부의 코드에서는 참조할 수 없도록 하면 이 문제를 방지할 수 있다.
이것은 클래스가 생성될 때 그리고 게터 함수를 통해 객체가 반환될 때 객체의 복사본을 만들면 가능해진다.
하지만 복사하는데 비용이 많이 들 수 있고 클래스 내부에서 발생하는 변경을 막아주지 못하는 경우가 많다.
불변적 자료구조를 사용하라
불변적인 자료구조를 사용하는 것은 클래스가 깊은 불변성을 갖도록 보장하기 위한 좋은 방법 중 하나다.
방어적으로 복사해야 하는 단점을 피하고 실수로라도 클래스 내의 코드에서 변경되지 않도록 보장한다.

지나치게 일반적인 데이터 유형을 피하라

정수, 문자열 및 리스트 같은 간단한 데이터 유형은 코드의 기본적인 구성 요소 중 하나로 일반적이고, 다재다능하며, 다른 모든 것들을 대표할 수 있다.
정수나 리스트와 같은 유형으로 표현이 가능하다고 해서 그것이 반드시 좋은 방법은 아니다.
설명이 부족하고 허용하는 범위가 넓을수록 코드 오용은 쉬워진다.
일반적이고 바로 가져다 쓸 수 있는 데이터 유형을 사용하는 것이 때로는 빠르고 쉬운 방법처럼 보일 수 있다.
하지만 무언가 구체적인 것을 나타낼 필요가 있을 때, 적은 노력을 추가로 들여서 전용 유형을 정의하는 것이 더 나을 때가 많다.
코드가 훨씬 더 명확해지고 오용하기 어렵기 때문에 중장기적으로 보자면 시간이 절약된다.

시간 처리

시간을 다룰 때 코드를 잘못 사용하고 혼동을 일으킬 여지가 굉장히 많다.
시간을 나타낼 때 일반적으로 정수나 큰 정수를 사용한다.
이것으로 어느 한순간을 의미하는 시각과 시간의 양, 두 가지를 모두 나타낸다.
일치하지 않는 단위
정수 유형은 값이 어떤 단위에 있는지 나타내는데 전혀 도움이 되지 않는다.
함수 이름, 매개변수 이름, 주석문을 사용하여 단위를 나타낼 수 있지만, 여전히 코드를 오용하기가 상대적으로 쉽다.
시간대 처리 오류
순간으로서의 시간은 일반적으로 유닉스 시간 이후 지나간 초(윤초는 무시)로 나타낸다.
이것을 보통 타임스탬프라고 부르는데, 이벤트가 발생했거나 발생할 시간을 정확하게 식별하기 위한 방법이다.
날짜와 순간의 차이는 미묘한 것일 수 있지만, 이 두 가지를 다른 방식으로 다루지 않는다면 문제가 될 수 있다.
사용자가 날짜를 입력하고 이를 로컬 표준시 내의 날짜 및 시간으로 해석하면 다른 표준시 사용자가 정보에 액세스할 때 다른 날짜가 표시될 수 있다.
적절한 자료구조를 사용하라
타사 혹은 제3자 오픈소스 라이브러리는 모두 Instant라는 클래스와 Duration이라는 클래스를 제공한다.
이 중 하나를 사용하면 유형이 나타내는 것이 순간인지 아니면 시간의 양인지 알 수 있다.
또 다른 이점은 단위가 유형 내에 캡슐화되어 있다는 점이다.

데이터에 대해 진실의 원천을 하나만 가져야 한다

코드에서 숫자, 문자열, 바이트 스트림과 같은 종류의 데이터를 처리하는 경우가 많다.
데이터는 종종 두 가지 형태로 제공된다.
기본 데이터 : 코드에 제공해야 할 데이터. 코드에 이 데이터를 알려주지 않고는 코드가 처리할 방법이 없다.
파생 데이터 : 주어진 기본 데이터에 기반해서 코드가 계산할 수 있는 데이터
기본 데이터는 일반적으로 프로그램에서 진실의 원천이 된다.
또 다른 진실의 원천은 유효하지 않은 상태를 초래할 수 있다
기본 데이터와 파생 데이터를 모두 처리하는 코드를 작성할 때, 논리적으로 잘못된 상태가 발생할 수 있고 그러한 코드를 작성하면 코드의 오용이 너무 쉬워진다.
기본 데이터를 유일한 진실의 원천으로 사용하며 정의할 수 있는 데이터 모델과 이러한 데이터 모델이 논리적으로 잘못된 상태를 허용하는지에 대해 시간을 들여서 숙고해볼 만한 가치가 있다.
데이터 계산에 비용이 많이 드는 경우
하나의 값이 아니라 목록을 가지고 있다고 가정해보자.
이때 목록은 기본 데이터이고 목록을 통해 파생된 데이터를 계산하려면 전체 목록을 확인해야 하기 때문에 비용이 많이 드는 일이다.
이와 같이 파생된 값을 계산하는데 많은 비용이 든다면, 그 값을 지연 계산한 후에 결과를 캐싱하는 것이 좋다.

논리에 대한 진실의 원천을 하나만 가져야 한다

진실의 원천은 코드에 제공된 데이터에만 적용되는 것이 아니라 코드에 포함된 논리에도 적용된다.
코드의 한 부분에서 수행되는 일이 다른 부분에서 수행되는 일과 일치해야 하는 경우가 많다.
코드의 두 부분이 서로 일치하지 않으면 소프트웨어가 제대로 작동하지 않을 것이다.
그러므로 논리를 위한 진실의 원천 역시 단 하나만 존재하도록 하는 것이 중요하다.

 Chapter 8. 코드를 모듈화 하라

모듈화의 주된 목적 중 하나는 코드가 향후에 어떻게 변경되거나 재구성될지 정확히 알지 못한 상태에서 변경과 재구성이 용이한 코드를 작성하는 것이다.
이를 달성하기 위한 핵심 목표는 각각의 기능(또는 요구 사항)이 코드베이스의 서로 다른 부분에서 구현되어야 한다는 것이다.
이것을 달성한 후에 요구 사항 중 하나가 변경된다면, 코드베이스에서 그 요구 사항이나 기능과 관련된 부분만 수정하면 된다.

의존성 주입의 사용을 고려하라

일반적으로 클래스는 다른 클래스에 의존한다.
잘 구성된 코드에서는 높은 수준의 문제를 하위 문제로 잘 나눠서 자신의 전용 클래스를 통해 해결되는 경우가 많다.
그러나 하위 문제에 대해 해결책이 항상 하나만 존재하는 것은 아니므로 하위 문제를 재구성할 수 있는 방식으로 코드를 작성하는 것이 유용할 수 있다.
의존성 주입은 이를 달성하는데 도움이 될 수 있다.
의존성 주입 프레임워크
의존성 주입은 클래스를 좀 더 변경할 수 있게 해주는 장점이 있지만, 생성하는 부분의 코드는 더 복잡해진다는 단점도 있다.
이 단점을 해소하기 위해 팩토리 함수를 작성할 수도 있지만 팩토리 함수를 많은 클래스에 대해 만들어야 한다면 그것 또한 힘든 작업이고 반복적으로 작성하는 코드가 많아질 수 있다.
의존성 주입 프레임워크를 사용하면 의존성 주입과 관련된 작업을 수동으로 하지 않아도 되기 때문에 개발 작업이 쉬워진다.

인터페이스에 의존하라

이는 코드를 모듈화하고 적응성을 높이기 위한 보다 일반적인 기술로 이어진다.
어떤 클래스에 의존하고 있는데 그 클래스가 어떤 인터페이스를 구현하고 필요한 기능이 그 인터페이스에 모두 정의되어 있으면, 클래스에 직접 의존하기보다는 인터페이스에 의존하는 것이 일반적으로 더 바람직하다.
구체적인 구현에 의존하면 적응성이 제한된다
구체적인 구현 클래스에 의존하면 인터페이스를 의존할 때보다 적응성이 제한되는 경우가 많다.
인터페이스는 하위 문제를 해결하기 위한 추상화 계층을 제공하는 것으로 생각할 수 있다.
이 인터페이스를 구체적으로 구현하는 클래스는 하위 문제를 해결하는데 있어 추상적이지 않고 좀 더 구현 중심적인 방식으로 해결한다.
더 추상적인 인터페이스에 의존하면 대개의 경우 더 간결한 추상화 계층과 더 나은 모듈화를 달성할 수 있다.

클래스 상속을 주의하라

클래스 상속은 확실히 쓸모가 있고 때로는 적합한 도구이다.
두 가지 사물이 진정한 is-a 관계를 갖는다면 상속이 적절할 수 있다.
상속을 사용할 수 있는 상황에서 많은 경우 구성을 상속 대신 사용할 수 있다.
즉, 클래스를 확장하기보다는 인스턴스를 가지고 있음으로써 한 클래스를 다른 클래스로부터 구성할 수 있다.
이렇게 하면 종종 상속의 함정을 피할 수 있고 모듈화와 내구성이 향상된 코드를 작성할 수 있다.
구성을 사용하라
상속을 사용할 때 서브클래스는 슈퍼클래스의 모든 기능을 상속하고 외부로 제공한다.
이 경우 서브클래스를 사용하는 쪽에서 슈퍼클래스의 기능을 사용할 수도 있는 매우 이상한 공개 API가 만들어진다.
상속 대신 구성을 사용하면 서브클래스가 전달이나 위임을 사용하여 명시적으로 노출하지 않는 한 슈퍼클래스의 기능이 노출되지 않는다.
진정한 is-a 관계여도 문제다
진정한 is-a 관계가 있다 하더라도 상속은 여전히 문제가 될 수 있다는 점을 알아야 한다.
취약한 베이스 클래스 문제
서브클래스가 슈퍼클래스에서 상속되고 해당 슈퍼클래스가 나중에 수정되면 서브클래스가 작동하지 않을 수도 있다.
따라서 코드를 변경할 때 그 변경이 문제없을지 판단하기가 어려운 경우가 있을 수 있다.
다이아몬드 문제
일부 언어는 두 개 이상의 슈퍼클래스를 확장할 수 있는 다중 상속을 지원한다.
여러 슈퍼클래스가 동일한 함수의 각각 다른 버전을 제공하는 경우 문제가 발생할 수 있는데, 어떤 슈퍼클래스로부터 해당 함수를 상속해야 하는지 모호하기 때문이다.
문제가 있는 계층 구조
많은 언어가 다중 상속을 지원하지 않으므로 클래스는 오직 하나의 클래스만 직접 확장할 수 있다.
이를 단일 상속이라고 하며 클래스 계층 구조에 포함할 수 없는 문제가 발생할 수 있다.
믹스인과 트레이트는 다중 상속 및 문제 있는 클래스의 계층 구조로 인해 유발되는 문제점 중 일부를 해결하는데 도움이 된다.
이를 통해 기존 클래스를 상속하지 않고도 해당 클래스의 기능 일부를 사용하고 공유할 수 있다.
하지만 클래스 상속과 비슷하게, 믹스인과 트레이트를 사용한 코드는 여전히 간결한 추상화 계층을 갖지 못하거나 적응성이 낮아질 수 있다.

클래스는 자신의 기능에만 집중해야 한다

모듈화의 핵심 목표 중 하나는 요구 사항이 변경되면 그 변경과 직접 관련된 코드만 수정한다는 것이다.
단일 개념이 단일 클래스 내에 완전히 포함된 경우라면 이 목표는 달성할 수 있다.
어떤 개념과 관련된 요구 사항이 변경되면 그 개념에 해당하는 단 하나의 클래스만 수정하면 된다.
자신의 기능에만 충실한 클래스를 만들라
코드 모듈화를 유지하고 한 가지 사항에 대한 변경 사항이 코드의 한 부분만 영향을 미치도록 하기 위해, 클래스는 가능한 한 자신의 기능에만 충실하도록 해야 한다.
클래스는 서로에 대한 어느 정도의 지식을 필요로 할 때도 있지만, 가능한 한 이것을 최소화하는 것이 좋을 때가 많다.
이를 통해 코드 모듈화를 유지할 수 있으며 적응성과 유지관리성을 크게 개선할 수 있다.

관련 있는 데이터는 함께 캡슐화하라

클래스를 통해 여러 가지를 함께 묶을 수 있다.
너무 많은 것들을 한 클래스에 두지 않도록 주의해야 하지만 한 클래스 안에 함께 두는 것이 합리적일 때는 그렇게 하는 것의 이점을 놓쳐서도 안 된다.
이렇게 하면 코드는 여러 항목의 세부 사항을 다루는 대신, 그 항목들이 묶여 있는 단일한 클래스가 제공하는 상위 수준의 개념을 다룰 수 있다.
이를 통해 코드는 더욱 모듈화하고 변경된 요구 사항을 해당 클래스에서만 처리할 수 있다.

반환 유형에 구현 세부 정보가 유출되지 않도록 주의하라

간결한 추상화 계층을 가지려면 각 계층의 구현 세부 정보가 유출되지 않아야 한다.
구현 세부 정보가 유출되면 코드의 하위 계층에 대한 정보가 노출될 수 있으며, 향후 수정이나 재설정이 매우 어려워질 수 있다.
코드에서 구현 세부 정보를 유출하는 일반적인 형태 중 하나는 해당 세부 정보와 밀접하게 연결된 유형을 반환하는 것이다.
따라서 외부로 노출할 개념을 최소화하는 유형을 새로 정의해 사용하면 좀 더 모듈화된 코드와 간결한 추상화 계층을 얻을 수 있다.

예외 처리 시 구현 세부 사항이 유출되지 않도록 주의하라

구현 세부 정보가 유출될 수 있는 또 다른 일반적인 경우는 예외를 발생할 때다.
클래스는 인터페이스에 의존하므로 이 인터페이스를 구현하는 어떤 클래스로도 설정할 수 있다.
서브클래스는 이러한 구현 클래스 중 하나일 뿐이지 유일한 클래스가 아니고 완전히 다른 유형의 예외를 발생시킬 수도 있다.
이렇게 되면 예외 처리문이 예외를 제대로 처리하지 못하고 프로그램이 멈추거나 코드의 좀 더 높은 층위에서 도움이 별로 되지 않는 오류 메시지를 사용자에게 보여줄 수도 있다.
추상화 계층에 적절한 예외를 만들라
구현 세부 사항의 유출을 방지하기 위해 코드의 각 계층은 주어진 추상화 계층을 반영하는 오류 유형만을 드러내는 것이 이상적이다.
이것은 하위 계층의 오류를 현재 계층에 적합한 오류 유형으로 감싸면 가능하다.
이렇게 하면 호출하는 쪽에 적절한 추상화 계층이 제시되면서 동시에 원래의 오류 정보가 손실되지 않는다는 것을 의미한다.

 Chapter 9. 코드를 재사용하고 일반화할 수 있도록 하라

앞서 상위 수준의 문제를 해결할 때 일련의 하위 문제로 세분화해서 해결하는 방법에 대해 이야기했다.
다른 개발자가 이미 주어진 하위 문제를 해결했다면, 해당 문제에 대한 해결책을 재사용한느 것이 타당하다.
이렇게 하면 시간을 절약할 수 있고, 재사용하는 코드는 이미 테스트되고 실행되었기 때문에 버그 발생 가능성도 줄어든다.
하지만 안타깝게도 하위 문제에 대한 해결책이 이미 존재한다고 해서 항상 재사용할 수 있는 것은 아니다.
다른 개발자가 구현한 해결책이 자신의 사례에 맞지 않는 가정을 하거나, 그 해결책이 자신에게는 필요 없는 다른 기능과 함께 구성된 경우 이러한 문제가 발생할 수 있다.
따라서 이 점을 적극적으로 고려하여 향후에 재사용이 가능하도록 의도적으로 코드를 작성하고 구조화하는 것이 바람직하다.

가정을 주의하라

코드 작성 시 가정을 하면 코드가 더 단순해지거나, 더 효율적으로 되거나, 둘 다일 수도 있다.
그러나 이러한 가정으로 인해 코드가 더 취약해지고 활용도가 낮아져 재사용하기에 안전하지 않을 수 있다.
불필요한 가정을 피하라
어떠한 가정은 비용-이익 상충관계를 고려하면 별로 가치가 없을 수도 있다.
코드를 작성할 때 필요 이상으로 성능비용과 같은 문제에 주의를 기울이는 경우가 있다.
그러나 코드에 가정이 들어가면 취약성의 측면에서도 관련 비용을 수반한다는 것을 기억하는 것이 중요하다.
특정한 가정으로 인해 성능이 눈에 띄게 향상되거나 코드가 크게 단순해진다면, 그 가정은 충분히 가치 있는 것일 수 있다.
그러나 가정으로 인해 얻는 이득이 미미하다면, 오히려 비용이 이점보다 훨씬 클 수 있다.
가정이 필요하면 강제적으로 하라
코드에 가정이 있을 때, 다른 개발자들이 그것을 여전히 모를 수 있다는 사실을 염두에 두어야 한다.
그래서 우리가 상정한 가정으로 인해 다른 개발자들이 무의식중에 곤란을 겪지 않도록 하기 위해 가정을 강제적으로 시행해야 한다.
이를 위해 일반적으로 다음 두 가지 방법을 사용할 수 있다.
1.
가정이 깨지지 않게 만들라
가정이 깨지면 컴파일되지 않는 방식으로 코드를 작성할 수 있다면 가정이 항상 유지될 수 있다.
2.
오류 전달 기술을 사용하라
가정을 깨는 것이 불가능하게 만들 수 없는 경우에는 오류를 감지하고 오류 신호 전달 기술을 사용하여 신속하게 실패하도록 코드를 작성할 수 있다.

전역 상태를 주의하라

전역상태 또는 전역변수는 실행되는 프로그램 내의 모든 컨텍스트 사이에 공유된다.
전역변수는 프로그램 내의 모든 컨텍스트에 영향을 미치기 때문에 전역변수를 사용할 때는 누구도 해당 코드를 다른 목적으로 재사용하지 않을 것이라는 암묵적인 가정을 전제한다.
전역상태는 코드를 매우 취약하게 만들고 재사용하기도 안전하지 않기 때문에 일반적으로 이점보다 비용이 더 크다.
공유 상태에 의존성 주입하라
클래스가 자신의 생성자 안에서 사용하는 클래스의 인스턴스를 생성함으로써 하드 코딩된 의존성을 갖는 대신 그 클래스의 인스턴스를 주입 받는다.
의존성 주입은 전역 상태를 사용하는 것보다 더 통제된 방법으로 서로 다른 클래스 간에 상태를 공유하는 좋은 방법이기도 하다.
이렇게 변경된 클래스에서 주목해서 살펴봐야 할 사항은 다음과 같다.
클래스의 정적 변수는 두 인스턴스를 생성하면 두 인스턴스 모두 서로 다른 항목을 가질 수 있다.
클래스의 정적 함수는 각 인스턴스를 통해서만 액세스할 수 있다.

기본 반환값을 적절하게 사용하라

기본값은 코드(및 소프트웨어)를 훨씬 쉽게 사용할 수 있으므로 활용할 가치가 충분하다.
하지만 코드의 어느 부분에서 사용할지 조심하는 것이 바람직하다.
기본값을 반환하면 그 위의 모든 상위 계층에서 해당 값을 사용할 것이라고 가정하기 때문에 코드 재사용과 적응성을 제한할 수 있다.
낮은 층위의 코드에서 기본값을 반환하는 것은 특히 문제가 될 수 있다.
단순히 널을 반환하고 더 높은 층위에서 기본값을 구현하는 것이 나을 수 있는데, 상위 수준에서 상정한 가정은 유효할 가능성이 크다.

함수의 매개변수를 주목하라

함수가 데이터 객체나 클래스 내에 포함된 모든 정보가 있어야 하는 경우에는 해당 함수가 객체나 클래스의 인스턴스를 매개변수로 받는 것이 타당하다.
이렇게 하면 함수 매개변수의 수가 줄어들고 캡슐화된 데이터의 자세한 세부 사항을 처리해야 하는 코드가 필요 없다.
그러나 함수가 한두 가지 정보만 필요로 할 때는 객체나 클래스의 인스턴스를 매개변수로 사용하는 것은 코드의 재사용성을 해칠 수 있다.
함수는 필요한 것만 매개변수로 받도록 하라
일반적으로 함수가 필요한 것만 받도록 하면 코드는 재사용성이 향상되고 이해하기도 쉬워진다.
하지만 이와 관련해서 어떻게 할지 자신이 판단하는 것이 좋다.
10가지 항목을 캡슐화하는 클래스가 있고 그중 8개를 필요로 하는 함수가 있다면, 캡슐화 객체 전체를 함수에 전달하는 것이 합리적이다.
캡슐화된 객체 대신 8개의 값을 전달하는 것은 모듈성을 해칠 수 있다.
많은 것들이 그렇듯 모든 상황에 적용되는 한 가지 정답은 없으며, 취하고 있는 방법의 장단점과 초래할 결과를 알고 있는 것이 좋다.

제네릭 사용을 고려하라

클래스는 종종 다른 유형 혹은 클래스의 인스턴스나 참조를 갖는다.
이것의 대표적인 예는 리스트 클래스다.
어떤 경우에는 문자열 리스트가 필요하지만 다른 경우에는 정수 리스트가 필요할 수 있다.
문자열과 정수를 저장하기 위해 완전히 다른 별개의 리스트 클래스가 필요하다면 상당히 번거로울 것이다.
다행히도 많은 언어가 제네릭 혹은 템플릿을 지원한다.
다른 클래스를 참조하는 코드를 작성하지만 그 클래스가 어떤 클래스인지 신경 쓰지 않는다면 제네릭의 사용을 고려해야 한다.
제네릭을 사용하면 아주 적은 양의 추가 작업이 있긴 하지만 코드의 일반화가 크게 향상된다.

 Chapter 10. 단위 테스트의 원칙

단위 테스트는 상대적으로 격리된 방식으로 코드의 구별되는 단위를 테스트하는 것에 관한 것이다.
코드의 단위라는 것이 정확히 의미하는 바는 다양할 수 있지만, 특정 클래스, 함수, 코드 파일을 의미할 때가 많다.
상대적으로 격리된 방식이라고 할 때 이것이 의미하는 바도 역시 다양하고 해석의 여지가 있다.
대부분의 코드는 격리되어 있지 않고, 다른 수많은 코드에 의존한다.
어떤 개발자들은 단위 테스트에서 테스트 대상이 되는 코드가 의존하는 코드를 차단하려고 하는 반면, 다른 개발자들은 의존하는 코드를 포함해서 테스트하는 것을 선호한다.

단위 테스트 기초

테스트와 관련하여 기억해야 할 몇 가지 중요한 개념과 용어는 다음과 같다.
테스트 중인 코드
‘실제 코드’라고도 하고 테스트의 대상이 되는 코드를 의미한다.
테스트 코드
단위 테스트를 구성하는 코드를 가리킨다.
테스트 코드는 일반적으로 ‘실제 코드’와는 별도의 파일에 있지만, 실제 코드의 파일과 테스트 코드의 파일 사이에 일대일 매핑이 있는 경우가 많기 때문에 GuestList.lang이라는 실제 코드가 있다면 단위 테스트 코드는 GuestListTest.lang이라는 파일에 있다.
때로는 실제 코드와 테스트 코드가 같은 디렉터리에 서로 나란히 있기도 하고, 때로는 테스트 코드가 코드베이스의 완전히 다른 부분에 있을 때도 있다.
테스트 케이스
테스트 코드의 각 파일에는 일반적으로 여러 테스트 케이스가 있고, 각 테스트 케이스는 특정 동작이나 시나리오를 테스트한다.
실제로 테스트 케이스는 일반적으로 함수이고 가장 단순한 테스트 케이스가 아니라면 보통 다음과 같이 세 개의 섹션으로 나뉘어져 있다.
준비(given)
테스트할 특정 동작을 호출하려면 먼저 몇 가지 설정을 수행해야 하는 경우가 많다.
예를 들어 일부 테스트 값을 정의하거나, 의존성을 설정하거나, 테스트 대상이 되는 클래스의 인스턴스를 올바르게 설정하고 생성하는 것 등이 포함된다.
이것은 종종 테스트 케이스의 시작 부분에서 별개의 코드 블록으로 배치된다.
실행(when)
테스트 중인 동작을 실제로 호출하는 코드를 나타낸다.
이것은 일반적으로 테스트 대상이 되는 코드에 존재하는 함수를 호출하는 것을 수반한다.
단언(then)
테스트 중인 동작이 실행되고 나면 실제로 올바른 일이 발생했는지 확인해야 한다.
여기에는 일반적으로 반환값이 예상한 값과 같거나 일부 결과 상태가 예상과 같은지 확인하는 작업이 포함된다.
테스트 러너
이름에서 알 수 있듯이 테스트 러너는 실제로 테스트를 실행하는 도구다.
테스트 코드 파일이 주어지면 각 테스트 케이스를 실행하고 통과 혹은 실패한 케이스에 대한 자세한 결과를 출력한다.

좋은 단위 테스트는 어떻게 작성할 수 있는가?

훼손의 정확한 감지
테스트 중인 코드가 어떤 식으로든 훼손되면 컴파일되지 않거나 테스트가 실패해야 한다.
이것은 매우 중요한 두 가지 역할을 수행한다.
코드에 대한 초기 신뢰를 준다.
새로운 코드나 코드 변경 사항과 함께 철저한 테스트 코드를 작성하면 코드가 코드베이스로 병합되기 전에 이러한 실수를 발견하고 수정할 수 있다.
미래의 훼손을 막아준다.
코드 변경(또는 다른 이벤트)으로 인해 잘 돌아가던 기능이 작동하지 않는 것을 회귀라고 한다.
이러한 회귀를 탐지할 목적으로 테스트를 실행하는 것을 회귀 테스트라고 한다.
정확성의 또 다른 측면을 고려하는 것도 중요하다.
논리적 오류를 경험한 적이 있는 사람이라면 누구나 알겠지만 ‘코드가 훼손되면 반드시 실패한다’는 것이 반드시 ‘코드가 훼손될 때만 테스트가 실패한다’는 것을 의미하는 것은 아니다.
코드에서 어떤 부분이 훼손될 때 그리고 오직 훼손된 경우에만 테스트가 실패하도록 하는 것은 매우 중요하다.
세부 구현 사항에 독립적
일반적으로 개발자가 코드베이스에 가할 수 있는 변경은 두 가지 종류가 있다.
기능적 변화
코드가 외부로 보이는 동작을 수정한다.
리팩터링
큰 함수를 작은 함수로 분할하거나 재사용하기 쉽도록 일부 유틸리티 코드를 다른 파일로 옮기는 등의 코드의 구조적 변화를 의미한다.
리팩터링이 올바르게 수행되더라도 이론적으로 코드의 외부에서 보이는 동작(또는 기능적 속성)이 변경되면 안 된다.
코드베이스를 변경할 때 일반적으로 기능만 변경하거나 리팩터링만 해야지 두 가지 작업을 동시에 수행하는 것은 좋지 않다.
기능적 변화와 리팩터링을 동시에 하면 기능적 변화로 예상되는 동작의 변화와 리팩터링의 실수로 발생하는 동작의 변화를 구분하기 어려울 수 있다.
테스트가 구현 세부 정보에 의존하지 않으면 코드 리팩터링에 실수가 있었는지 확인해주는 테스트 결과를 신뢰할 수 있다.
잘 설명되는 실패
코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.
테스트 실패가 잘 설명되도록 하는 좋은 방법 중 하나는 하나의 테스트 케이스는 한 가지 사항만 검사하고 각 테스트 케이스에 대해 서술적인 이름을 사용하는 것이다.
이렇게 하면 한 번에 모든 것을 테스트하려고 하는 하나의 큰 테스트 케이스보다 각각의 특정 동작을 확인하기 위한 작은 테스트 케이스가 많이 만들어진다.
테스트가 실패할 때 실패한 케이스의 이름을 확인하면 어떤 동작이 작동하지 않는지 정확하게 알 수 있다.
이해할 수 있는 테스트 코드
한 코드에 대해 세 가지 동작이 테스트된다고 가정해보자.
개발자가 이러한 동작 중 하나만 의도적으로 변경할 경우 해당 동작에 대한 테스트 케이스만 변경하고 다른 두 가지 동작에 대한 테스트 케이스는 변경하지 않고 그대로 두는 것이 이상적이다.
개발자가 자신이 변경한 사항이 원하는 동작에만 영향을 미친다는 확신을 가지려면 테스트의 어느 부분에 영향을 미치고 있는지, 테스트 코드에 대한 수정이 필요한지 여부를 알 수 있어야 한다.
이를 위해서는 서로 다른 테스트 케이스가 무엇을 테스트하는지 그리고 어떻게 테스트하는지 이해하고 있어야 한다.
쉽고 빠르게 실행
대부분의 단위 테스트는 꽤 자주 실행된다.
단위 테스트의 중요한 기능 중 하나는 잘못된 코드가 코드베이스에 병합되는 것을 방지하는 것이다.
따라서 많은 코드베이스에서 관련 테스트를 통과해야만 병합이 가능한 병합 전 검사를 수행한다.
테스트를 빠르고 쉽게 유지해야 하는 또 다른 이유는 개발자가 실제로 테스트를 할 수 있는 기회를 극대화하기 위함이다.

퍼블릭 API에 집중하되 중요한 동작은 무시하지 말라

퍼블릭 API에 초점을 맞추면 세부 사항이 아닌 코드 사용자가 궁극적으로 신경 쓸 동작에 집중할 수 밖에 없게 되는데, 세부 사항은 목적을 이루기 위한 수단일 뿐이다.
이렇게 하면 실제로 중요한 사항만 테스트하는 데 도움이 되며, 테스트 과정에서 구현 세부 사항에 상관없이 테스트를 수행할 수 있다.
중요한 동작이 퍼블릭 API 외부에 있을 수 있다
가능하면 퍼블릭 API를 사용하여 코드의 동작을 테스트해야 한다.
이는 순전히 퍼블릭 함수의 매개변수, 반환값, 오류 전달을 통해 발생하는 동작만 테스트해야 한다는 의미다.
그러나 코드의 퍼블릭 API를 어떻게 정의하느냐에 따라 퍼블릭 API만으로는 모든 동작을 테스트할 수 없는 경우가 있다.
다양한 의존성을 설정하거나 특정 부수 효과가 발생했는지 여부를 확인하는 것이 이에 해당한다.
서버와 상호작용하는 코드
코드를 테스트하기 위해 서버로부터 필요한 값을 받을 수 있도록 서버를 설정하거나 시뮬레이션해야 할 수 있다.
또한 서버를 얼마나 자주 호출하는지, 요청이 유효한 형식인지 등과 같이 서버에 어떤 부수 효과가 있는지 확인해야 할 수도 있다.
데이터베이스에 값을 저장하거나 읽는 코드
모든 동작을 수행하기 위해 데이터베이스에 저장된 여러 다른 값으로 코드를 테스트해야 할 수도 있다.
또한 코드가 부수 효과로 데이터베이스에 어떤 값을 저장하는지 확인해야 할 수도 있다.
‘퍼블릭 API만을 이용해 테스트하라’와 ‘실행 세부 사항을 테스트하지 말라’는 둘 다 훌륭한 조언이지만, 테스트를 어떻게 할지 안내하는 원칙일 뿐 ‘퍼블릭 API’와 ‘구현 세부 사항’의 정의는 주관적이고 상황에 따라 달라질 수 있다는 점을 알아야 한다.
궁극적으로 중요한 것은 테스트를 구현 세부 사항에 최대한 독립적으로 수행하도록 주의를 기울여야 하므로 다른 대안이 없는 경우에만 퍼블릭 API를 벗어나 테스트해야 한다.

테스트 더블

코드는 다른 것들에 의존하는 경향이 있고, 코드의 모든 동작을 완벽하게 테스트하기 위해 종종 입력을 설정하고 부수 효과를 검증해야 한다.
하지만 테스트에서 의존성을 실제로 사용하는 것이 항상 가능하거나 바람직한 것만은 아니다.
의존성을 실제로 사용하는 것에 대한 대안으로 테스트 더블이 있다.
테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.
테스트 더블을 사용하는 이유
테스트 단순화
일부 의존성은 테스트에 사용하기 까다롭고 힘들다.
의존성은 많은 설정이 필요하거나 하위 의존성을 설정해야 할 수도 있다.
이러면 테스트는 복잡하고 구현 세부 사항과 밀접하게 결합될 수 있다.
의존성을 실제로 사용하는 대신 테스트 더블을 사용하면 작업이 단순해진다.
테스트로부터 외부 세계 보호
일부 의존성은 실제로 부수 효과를 발생한다.
코드의 종속성 중 하나가 실제 서버에 요청을 전송하거나 실제 데이터베이스에 값을 쓰게 되면, 사용자나 비즈니스에 중요한 프로세스에 나쁜 결과를 초래할 수 있다.
이러한 상황에서 테스트 더블을 사용하면 외부 세계에 있는 시스템을 테스트의 동작으로부터 보호할 수 있다.
외부로부터 테스트 보호
외부 세계는 비결정적일 수 있다.
다른 시스템이 데이터베이스에 쓴 값을 의존성 코드가 읽는다면 이 값은 시간이 지남에 따라 변경될 수 있다.
이 경우 테스트 결과를 신뢰하기 어려울 수도 있다.
반면에 테스트 더블은 항상 동일하게 결정적 방식으로 작동하도록 설정할 수 있다.
목(mock)
목은 클래스나 인터페이스를 시뮬레이션하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다.
함수가 호출될 때 인수에 제공되는 값을 기록한다.
테스트 대상 코드가 의존성을 통해 제공되는 함수를 호출하는지 검증하기 위해 목을 사용할 수 있다.
따라서 목은 테스트 대상 코드에서 부수 효과를 일으키는 의존성을 시뮬레이션하는 데 가장 유용하다.
스텁(stub)
스텁은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션한다.
이를 통해 테스트 대상 코드는 특정 멤버 함수를 호출하고 특정 값을 반환하도록 의존성을 시뮬레이션할 수 있다.
그러므로 스텁은 테스트 대상 코드가 의존하는 코드로부터 어떤 값을 받아야 하는 경우 그 의존성을 시뮬레이션하는 데 유용하다.
목과 스텁은 문제가 될 수 있다
목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 테스트는 실제적이지 않다.
클래스나 함수에 대해 목 객체를 만들거나 스텁할 때 테스트 코드를 작성하는 개발자는 목이나 스텁이 어떻게 동작할지 결정해야 한다.
클래스나 함수가 실제와 다르게 동작하도록 하는 것은 아주 위험하다.
이렇게 하면 테스트는 통과하고 모든 것이 잘 작동한다고 착각하지만 코드가 실제로 실행되면 부정확하게 동작하거나 버그가 발생할 수 있다.
구현 세부 사항과 테스트가 밀접하게 결합하여 리팩터링이 어려워질 수 있다.
리팩터링을 통해 구현 세부 사항만 변경했을 뿐 동작은 변경하지 않았다.
그러나 현재 대부분의 테스트는 목을 사용하고 있고, 리팩터링된 함수들이 호출되지 않기 때문에 테스트는 실패한다.
즉, 테스트는 구현 세부 사항에 구애받지 않아야 한다.
리팩터링을 수행한 개발자는 테스트 통과를 위해 많은 테스트 케이스를 수정해야 하므로 리팩터링이 의도치 않게 동작을 변경하지 않았다는 확신을 하기 어렵다.
페이크(fake)
페이크는 클래스(또는 인터페이스)의 대체 구현체로 테스트에서 안전하게 사용할 수 있다.
페이크는 실제 의존성의 공개 API를 정확하게 시뮬레이션하지만 구현은 일반적으로 단순한데, 외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다.
페이크의 요점은 코드 계약이 실제 의존성과 동일하기 때문에 실제 클래스(또는 인터페이스)가 특정 입력을 받아들이지 않는다면 페이크도 마찬가지라는 것이다.
따라서 실제 의존성에 대한 코드를 유지보수하는 팀이 일반적으로 페이크 코드도 유지보수해야 하는데, 실제 의존성에 대한 코드 계약이 변경되면 페이크의 코드 계약도 동일하게 변경되어야 하기 때문이다.

테스트 철학으로부터 신중하게 선택하라

테스트 주도 개발
TDD는 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하는 것을 지지한다.
실제 코드는 테스트만 통과하도록 최소한으로 작성하고 이후에 구조를 개선하고 중복을 없애기 위해 리팩터링을 한다.
방금 언급한 바와 같이 TDD 지지자들은 일반적으로 테스트 케이스를 격리하고 한 테스트 케이스는 하나의 동작만 테스트하도록 집중하며 구현 세부 사항을 테스트하지 않는 등의 여러 다른 모범 사례를 지지한다.
행동 주도 개발
BDD는 사람마다 조금씩 다른 의미를 가질 수 있지만 이 철학의 핵심은 사용자, 고객, 비즈니스의 관점에서 소프트웨어가 보여야 할 행동(또는 기능)을 식별하는 데 집중하는 것이다.
이런 원하는 동작은 소프트웨어가 개발될 수 있는 형식으로 포착되고 기록된다.
테스트는 소프트웨어 자체의 속성보다는 이러한 원하는 동작을 반영해야 한다.
이런 행동이 정확히 어떻게 포착되고 기록되는지, 어떤 이해관계자가 그 과정에 관여하는지, 그리고 얼마나 공식화되는지는 조직마다 다를 수 있다.
수용 테스트주도 개발
ATDD도 사람마다 약간 다른 의미를 가질 수 있으며, BDD와 비슷한데 정의에 따라 BDD와 겹치는 정도가 다르다.
ATDD는 고객의 관점에서 소프트웨어가 보여줘야 하는 동작(또는 기능)을 식별하고 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락 테스트를 만드는 것을 수반한다.
TDD와 마찬가지로 실제 코드를 구현하기 전에 이러한 테스트를 생성해야 한다.
이론적으로 합격 테스트가 모두 통과하면 소프트웨어는 완전한 것이며 고객이 수락할 준비가 된 것이다.
테스트 철학 및 방법론은 개발자들이 효과적이라고 생각하는 작업 방식을 문서화한다.
하지만 결국에는 궁극적으로 달성하고자 하는 목표가 그 목표에 도달하기 위해 선택한 작업 방식보다 더 중요하다.
중요한 것은 우리가 좋은 품질의 테스트 코드를 철저하게 작성하고 고품질의 소프트웨어를 생산하는 것이다.

 Chapter 11. 단위 테스트의 실제

기능뿐만 아니라 동작을 시험하라

테스트 대상 코드가 수행하는 작업이 여러 가지가 있으며 이 각각의 작업에 대해 테스트 케이스를 따로 작성해야 한다.
각 함수를 테스트하는 데만 집중할 때의 문제점은 한 함수가 종종 여러 개의 동작을 수행할 수 있고 한 동작이 여러 함수에 걸쳐 있을 수 있다는 점이다.
함수별로 테스트 케이스를 하나만 작성하면 중요한 동작을 놓칠 수 있다.
단순히 눈에 보이는 대로 함수 이름을 테스트 목록에 넣기보다는 함수가 수행하는 모든 동작으로 목록을 채우는 것이 좋다.
모든 동작이 테스트되었는지 거듭 확인하라
코드가 제대로 테스트되는지 여부를 측정하기 위한 한 가지 좋은 방법은 수정된 코드에 버그나 오류가 있음에도 여전히 테스트를 통과할 수 있는지에 대해 생각해보는 것이다.
요점은 테스트 대상 코드의 각 줄은 그것이 존재하는 이유가 있어야 한다는 것이다.
코드가 나타내는 중요한 동작이 있는 경우 그 동작을 테스트하는 테스트 케이스가 있어야 하므로, 기능을 변경한 경우 적어도 하나의 테스트 케이스가 실패해야 한다.
그렇지 않으면 모든 행동이 테스트되는 것이 아니다.
오류 시나리오를 잊지 말라
간과하기 쉬운 또 다른 중요한 동작은 오류 시나리오가 발생할 때 코드가 어떻게 동작하는가다.
오류가 자주 발생할 것으로 예상하지 않기 때문에 이러한 경우는 다소 경계 조건처럼 보일 수 있다.
그럼에도 불구하고 코드가 서로 다른 오류 시나리오를 처리하고 알리는 방법은 코드를 작성하거나 호출하는 쪽 모두가 관심을 갖는 중요한 동작이다.

테스트만을 위해 퍼블릭으로 만들지 말라

프라이빗 함수는 구현 세부 사항이며 클래스 외부의 코드가 인지하거나 직접 사용하는 것이 아니다.
때로는 이러한 프라이빗 함수 중 일부를 테스트 코드에서도 접근할 수 있도록 만들어 직접 테스트하고자 할 수 있다.
그러나 이는 좋은 생각이 아닐 때가 많다.
구현 세부 사항과 밀접하게 연관된 테스트가 될 수 있고 궁극적으로 우리가 신경 써야 하는 코드의 동작을 테스트하지 않을 수 있기 때문이다.
코드를 더 작은 단위로 분할하라
퍼블릭 API로 모든 것을 완벽하게 테스트하는 것이 어려워 보이는 진짜 이유는 클래스가 많은 다양한 개념을 포함하고 있기 때문이다.
여기서 해결책은 코드를 더 작은 계층으로 나누는 것이다.

한 번에 하나의 동작만 테스트하라

각각의 동작을 테스트하려면 약간 다른 시나리오를 설정해야 하므로, 각각의 시나리오는 그에 해당하는 별도의 테스트 케이스로 테스트하는 것이 가장 자연스럽다.
그러나 떄로는 하나의 시나리오로 여러 동작을 테스트하도록 만드는 방법이 있다.
하지만 가능하다고 해서 다 좋은 것은 아니다.
각 동작은 자체 테스트 케이스에서 테스트하라
모든 것을 한꺼번에 테스트하는 테스트 케이스는 정확히 무엇이 변경됐는지 알려주는 대신, 무언가 변경됐다는 것만 알려준다.
따라서 코드를 의도적으로 변경할 때 그 변경으로 인해 어떤 동작이 영향을 받았고 어떤 동작이 영향을 받지 않았는지 정확히 알기 어렵다.
훨씬 더 나은 접근법은 잘 명명된 테스트 케이스를 사용하여 각 동작을 개별적으로 테스트하는 것이다.
각 동작을 하나의 테스트 케이스로 테스트하면 장점이 있지만, 코드 중복이 많아지는 단점도 있다.
매개변수를 사용한 테스트
매개변수를 사용한 테스트는 많은 코드를 반복하지 않고도 모든 동작을 테스트할 수 있는 좋은 도구다.
테스트 케이스에서 매개변수를 사용하기 위해 설정하는 구문과 방법은 테스트 프레임워크마다 다를 수 있다.

공유 설정을 적절하게 사용하라

테스트 케이스는 의존성을 설정하거나 테스트 데이터 저장소에 값을 채우거나 다른 종류의 상태를 초기화하는 등 어느 정도의 설정이 필요할 때가 있다.
일반적으로 다음과 같이 두 가지 시점에서 공유 설정 코드를 실행하도록 설정할 수 있다.
BeforeAll
테스트 케이스가 실행되기 전에 단 한 번 실행된다.
BeforeEach
각 테스트 케이스가 실행되기 전에 매번 실행된다.
공유 설정을 실행하는 방법뿐만 아니라 해체 코드를 실행하는 방법도 제공한다.
AfterAll
모든 테스트 케이스가 실행된 후 한 번 실행된다.
AfterEach
각 테스트 케이스가 실행된 후 매번 실행된다.
이와 같은 설정 코드 블록을 사용하면 설정을 서로 다른 테스트 케이스 간에 공유할 수 있다.
상태 공유
설정된 모든 상태가 모든 테스트 케이스 간에 공유된다.
이러한 유형의 설정은 설정을 실행하는데 시간이 오래 걸리거나, 비용이 많이 드는 경우에 유용할 수 있다.
그러나 설정된 상태가 가변적인 경우에는 한 테스트 케이스의 실행 결과가 다른 테스트 케이스에 악영향을 미칠 수 있는 위험이 있다.
설정 공유
테스트 케이스는 이 코드에 의한 모든 설정을 공유한다.
설정 코드가 특정 값을 포함하거나 특정 방식으로 의존성을 설정하는 경우 각 테스트 케이스는 그 특정 값 또는 그 방식으로 구성된 의존성을 가지고 실행된다.
설정은 각 테스트 케이스 전에 실행되므로 테스트 케이스 간에 공유되는 상태는 없다.
서로 다른 테스트 케이스 간에 가변적인 상태를 공유하면 문제가 발생하기 매우 쉽다.
가능하다면 상태를 공유하지 않는 것이 최선이다.
하지만 상태 공유가 꼭 필요하다면 한 테스트 케이스에 의해 변경된 상태가 다른 테스트 케이스에 영향을 미치지 않도록 조심해야 한다.
상태를 공유하지 않거나 초기화하라
가변적인 상태를 공유하는 데서 오는 문제점을 해결하기 위한 가장 분명한 방법은 애초에 공유하지 않는 것이다.
공유하는 것을 피할 수 없다면 각 테스트 케이스 간에 반드시 상태가 초기화되도록 많은 주의를 기울여야 한다.
이를 통해 한 테스트 케이스가 다른 테스트 케이스에 악영향을 미치지 않도록 해야 한다.
중요한 설정은 테스트 케이스 내에서 정의하라
모든 테스트 케이스에 대해 반복해서 설정을 하는 것이 어려워 보일 수 있지만 테스트 케이스가 특정 값이나 설정 상태에 의존한다면 그렇게 하는 것이 더 안전한 경우가 많다.
보통 헬퍼 함수를 사용해 이 작업을 좀 더 쉽게 할 수 있기 때문에 코드를 반복하지 않아도 된다.
설정 공유가 적절한 경우
테스트 설정을 절대 공유해서는 안 된다는 의미는 아니다.
필요하면서도 테스트 케이스의 결과에 직접적인 영향을 미치지는 않는 설정이 있을 수 있다.
이같은 경우에는 설정 공유를 통해 불필요한 코드 반복을 피할 수 있고 테스트는 좀 더 뚜렷한 목적을 갖고 이해하기 쉬워진다.

적절한 어서션 확인자를 사용하라

리스트에 특정 원소가 포함되어 있는지 확인하는 테스트를 작성한다고 생각해보자.
isEqualTo() 어서션을 사용하면 리스트의 원소에 특정한 순서가 없을 때 반환되는 순서가 변경되면 테스트가 실패한다.
반환된 리스트를 비교하는 대신 contains() 와 함께 isTrue() 어서션을 사용하면 반환되는 순서가 변경되더라도 테스트는 실패하지 않는다.
그러나 또 다른 문제가 있다.
테스트가 실패하는 경우 왜 실패하는지 이유를 제대로 설명해주지 않는다.
각각의 어서션 확인자는 자신들의 목적에 따라 각자 다른 실패 메시지를 생성한다.
따라서 가장 적절한 어서션 확인자를 선택하는 것이 중요하다.

테스트 용이성을 위해 의존성 주입을 사용하라

테스트 용이성은 모듈화와 밀접한 관련이 있다.
서로 다른 코드가 느슨하게 결합하고 재설정이 가능하면, 테스트는 훨씬 더 쉬워지는 경향을 띤다.
의존성 주입은 코드를 좀 더 모듈화하기 위한 효과적인 기술이며, 따라서 코드의 테스트 용이성을 높이기 위한 효과적인 기술이기도 하다.

테스트에 대한 몇 가지 결론

소프트웨어를 테스트하기 위해 개발자들이 사용할 수 있는 다양한 기술이 많다.
소프트웨어를 높은 기준으로 작성하고 유지하려면 이러한 기술을 혼용해서 사용해야 하는 경우가 많다.
단위 테스트가 가장 흔한 유형이지만, 단위 테스트만으로는 테스트의 모든 요구 사항을 충족할 수 없기 때문에 다양한 테스트 유형과 수준에 대해 알아보고 새로운 툴과 기술에 대한 최신 정보를 유지하는 것이 좋다.