Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ch4, Ch5, Ch8, Ch9, Ch10, Ch11 정리 #14

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Chapter10/mangcho.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# 좋은 코드, 나쁜 코드 (10) 단위 테스트 원칙

date: March 10, 2024
preview: 좋은 코드, 나쁜 코드 10장을 읽고 정리한 글입니다.

---

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

좋은 단위 테스트가 가져야 할 기능은 다음과 같다.

- 훼손의 정확한 감지
- 세부 구현 사항에 독립적
- 잘 설명되는 실패
- 이해할 수 있는 테스트 코드
- 쉽고 빠르게 실행

**훼손의 정확한 감지**

초기에 작성된 코드가 코드베이스에 병합되기 전에 테스트를 통해 실수를 발견할 수 있다. 또한, 코드 변경으로 인해 잘 돌아가던 기능이 동작하지 못하는 경우를 회귀라고 하는데 회귀를 탐지할 목적으로 테스트를 실행할 수 있다.

**세부 구현 사항에 독립적**

코드베이스가 생기는 변화는 기능적 변화와 리팩터링이다. 기능적 변화가 생기면 테스트도 충분히 수정할 가능성이 높다. 리팩터링은 코드의 구조만 변경되는 것이기에 테스트가 유지되어야 한다. 하지만 테스트가 깨지는 경우는 실수로 코드의 동작을 변경했다는 의미다.

**잘 설명되는 실패**

코드의 변경으로 테스트가 실패한다면, 문제 상황을 쉽게 파악할 수 있어야 한다. 제대로 알려주지 않는다면 그것을 알아내기 위해 많은 시간을 낭비해야 한다.

**이해할 수 있는 테스트 코드**

너무 많은 것을 테스트하거나 너무 많은 공유 테스트 설정을 하는 경우에 이해하기 어려울 수 있다. 테스트 코드를 이해하기 쉽게 만들어야 하는 이유는 일부 개발자들은 사용설명서로 사용하기 때문이다.

**쉽고 빠르게 실행**

단위 테스트는 매우 자주 실행된다. 테스트에 오랜 시간이 걸리면 테스트 자체가 힘든 작업이 돼버린다.

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

개발자 사이에서 종종 언급되는 말이 있다. 다음은 훌륭한 조언이지만 절대적이지 않다.

- Public API만을 이용해 테스트하라
- 실행 세부 사항을 테스트하지 말라

하지만 추상화 계층 관점에서는 한 코드가 다른 코드에 대해 알아야 할 모든 것이 퍼블릭 API로 제공되기 때문에 그 외의 모든 것은 구현 세부사항이라고 볼 수 있다. 그러나 테스트에 관해서는 퍼블릭 API로 제공되지 않는 것 중에서도 테스트 코드가 알아야 할 다른 사항들이 있을 수 있다. 예를 들어, 다음과 같은 상황이 존재한다.

- 서버와 상호작용하는 코드
- 데이터베이스에 값을 저장하거나 읽는 코드

### 💭 테스트 더블

다른 코드에 의존성이 있는 코드를 테스트하는 방법 중 하나는 테스트 더블이다. 테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있게 만든다. 목, 스텁, 페이크 등이 있는데 페이크를 사용하는 것이 낫다.

테스트 더블을 사용하는 이유는 다음과 같다.

- 테스트 단순화

실제 의존성을 설정하거나 하위 종속성에서 무언가를 검증할 필요가 없다. 더 빠르게 테스트를 실행할 수 있다.

- 테스트로부터 외부 세계 보호

테스트가 실제 데이터베이스에 의존하는 경우에 데이터가 더러워지는 경우가 예시다.

- 외부로부터 테스트 보호

반대로 실제 데이터베이스에 의존하는 경우에 데이터의 변경으로 테스트가 깨지는 경우가 예시다.


**목**

클래스나 인터페이스를 시뮬레이션하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 하지 않는다.


**스텁**

함수가 호출되면 미리 정해 놓은 값을 반환한다. 테스트 대상 코드가 의존하는 다른 코드로부터 값을 받아와야 하는 경우에 그 의존성을 시뮬레이션하는 데 유용하다.



하지만 목과 스텁은 문제가 될 수 있다.

- 목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 테스트는 실제적이지 않다.
- 구현 세부 사항과 테스트가 밀접하게 결합하여 리팩터링이 어려워질 수 있다.

**페이크**

페이크는 실제 의존성의 공개 API를 정확하게 시뮬레이션하지만 구현은 단순하고 외부와 통신하는 대신에 페이크 내의 멤버 변수에 상태를 저장한다. 요점은 코드 계약이 실제 의존성과 동일하기 때문에 실제 클래스(또는 인터페이스)가 특정 입력을 받아들이지 않는다면 페이크도 마찬가지라는 점이다.



페이크를 사용할 때의 장점은 다음과 같다.

- 페이크로 인해 보다 실질적인 테스트가 이루어질 수 있다.
- 페이크를 사용하면 구현 세부 정보로부터 테스트를 분리할 수 있다.

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

테스트를 둘러싼 여러 철학과 방법론이 존재한다.

- **TDD (테스트 주도 개발)**

실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성한다. 실제 코드는 테스트만 통과하도록 최소한만 작성하고 이후에 구조를 개선하고 중복을 없얘기 위한 리팩터링을 한다.

- **BDD (행동 주도 개발)**

이 철학의 핵심은 사용자, 고객, 비즈니스 관점에서 소프트웨어가 보여야 할 행동을 식별하는 데에 집중하는 것이다.

- **ATDD (수용 테스트 주도 개발)**

고객의 관점에서 소프트웨어가 보여줘야 하는 동작을 식별하고 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락 테스트를 만드는 것이다. TDD와 마찬가지로 실제 코드를 구현하기 전에 이러한 테스트를 생성해야 한다. 합격 테스트를 모두 통과하면 소프트웨어는 완전한 것이며 고객이 수락할 준비가 된 것이다.


### 💭 요약

- 코드 베이스에 제출된 거의 모든 '실제 코드'는 그에 해당하는 단위 테스트가 동반되어야 한다.
- '실제 코드'가 보여주는 모든 동작에 대해 이를 실행해보고 결과를 확인하는 테스트 케이스가 작성되어야 한다. 아주 간단한 테스트 케이스가 아니라면 각 테스트 케이스 코드는 준비, 실행 및 단언의 세 가지 부분으로 나누는 것이 일반적이다.
- 바람직한 단위 테스트의 주요 특징은 다음과 같다.
- 문제가 생긴 코드의 정확한 탐지
- 구현 세부 정보에 구애받지 않음
- 실패가 잘 설명됨
- 이해하기 쉬운 테스트 코드
- 쉽고 빠르게 실행
- 테스트 더블은 의존성을 실제로 사용하는 것이 불가능하거나 현실적으로 어려울 때 단위테스트에 사용할 수 있다. 테스트 더블의 몇 가지 예는 다음과 같다.
- 목
- 스텝
- 페이크
- 목 및 스텁을 사용한 테스트 코드는 비현실적이고 구현 세부 정보에 밀접하게 연결될 수 있다.
- 목과 스텁의 사용에 대한 여러 의견이 있다. 필자의 의견은 가능한 한 실제 의존성이 테스트에 사용되어야 한다는 것이다. 이렇게 할 수 없다면, 페이크가 차선책이고, 목과 스텁은 최후의 수단으로만 사용되어야 한다.
87 changes: 87 additions & 0 deletions Chapter11/mangcho.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 좋은 코드, 나쁜 코드 (11) 단위 테스트의 실제

date: March 10, 2024
preview: 좋은 코드, 나쁜 코드 11장을 읽고 정리한 글입니다.

---

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

단순히 눈에 보이는 대로 함수 이름을 테스트 목록에 넣기보다는 함수가 수행하는 모든 동작으로 목록을 채우는 것이 좋다.

한 메서드는 다음과 같은 동작을 한다고 생각하자.

- 다음 중 해당하는 사항이 있으면 대출에 기각된다.
- 신용 등급이 좋지 않다.
- 이미 대출이 있다.
- 대출이 금지된 고객이다.
- 주택담보대출 신청이 받아들여진다면 최대 대출 금액은 고객의 수입에서 지출을 뺀 금액에 10을 곱한 금액이다.

함수 이름을 목록에 넣는다면 1개의 테스트가 만들어진다. 하지만 동작을 테스트로 만든다면 4개 이상의 테스트가 만들어진다. 모든 동작이 테스트되었는지 확인하기 위해 코드를 검토할 때, 사용할 수 있는 체크리스트가 있다.

- 삭제해도 여전히 컴파일되거나 테스트가 통과되는 라인이 있는가
- if문의 참 거짓 논리를 반대로 해도 테스트가 통과하는가
- 논리 연산자나 산술 연산자를 다른 것으로 대체해도 테스트가 통과하는가
- 상숫값이나 하드 코딩된 값을 변경해도 테스트가 통과하는가

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

프라이빗 함수는 구현 세부 사항이다. 테스트를 위해 퍼블릭 함수로 만드는 것의 문제점은 다음과 같다.

- 이 테스트는 실제로 우리가 신경 쓰는 행동을 테스트하는 것이 아니다. 우리가 신경써야 할 것은 구현 세부 사항이 아니라 퍼블릭 API이다.
- 테스트가 구현 세부 사항에 독립적이지 못하게 된다.
- 다른 개발자가 퍼블릭으로 바꾼 함수에 의존할 수 있다.

퍼블릭 API만을 사용해서 테스트하는 것이 좋다. 하지만 클래스가 더 복잡하거나 많은 논리를 포함하면 퍼블릭 API를 통해 모든 동작을 테스트하는 것이 까다로울 수 있다. 이 경우는 코드의 추상화 계층이 너무 크다는 것을 의미하기 때문에 코드를 더 작은 단위로 분할하는 것이 유익하다.

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

여러 동작을 한꺼번에 테스트하면 테스트가 제대로 안 될 수 있다. 테스트 코드를 이해하기 어려운 단점도 존재한다. 이를 해결하는 방법은 각 동작을 자체 테스트 케이스에서 테스트하면 된다. 코드 중복이 많아진다는 단점도 존재하지만 이것은 매개변수화된 테스트로 방지할 수 있다.

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

테스트 케이스에 의존성을 설정하거나 테스트 데이터를 만드는 것은 비용이 많이 들 수 있다. 이를 해결하는 방법은 다음 2가지가 있다.

- **상태 공유**

모든 테스트 케이스 전에 한 번 실행된다. 설정된 모든 상태는 모든 테스트 케이스 간에 공유된다. Ex) BeforeAll

- **설정 공유**

테스트 케이스는 이 코드에 의해 모든 설정을 공유한다. 각 테스트 케이스 전에 실행되므로 테스트 케이스 간에 공유되는 상태는 없다. Ex) BeforeEach



비용을 줄이기 위해 상태와 설정을 공유하더라도 오히려 비용이 증가하는 경우가 발생한다. 적절하게 사용하는 것이 중요하다. 또한 상태와 설정을 공유하게 되면 초기화 등의 문제가 발생할 수 있으므로 조심해야 한다.

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

적절한 어서션은 테스트가 실패했을 경우에 이유를 잘 설명한다.


**적절한 어서션 확인자**



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

데이터베이스와 같이 외부 의존성을 갖는 코드는 테스트하기 어렵다. 이는 의존성 주입을 통해 해결할 수 있다.


### 💭 테스트에 대한 몇 가지 결론

- 통합 테스트: 여러 구성 요소와 하위 시스템을 연결하는 프로세스를 통합이라고 한다. 이런 통합이 잘 이루어져 작동하는지 확인하기 위한 테스트
- 종단 간 테스트: 소프트웨어의 처음부터 끝까지 통과하는 모든 여정을 테스트

- 회귀 테스트: 소프트웨어의 동작이나 기능이 바람직하지 않은 방식으로 변경되지 않았는지 확인하기 위해 정기적으로 수행하는 테스트
- 골든 테스트: 특성화 테스트라고도 하며, 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샷으로 저장한 것을 기반으로 한다. 테스트 수행 후 코드가 생성한 출력이 다르면 테스트는 실패한다.
- 퍼즈 테스트: 많은 무작위 값이나 '흥미로운' 값으로 코드를 호출하고 그들 중 어느 것도 코드의 동작을 멈추지 않는지 점검

### 💭 요약

- 각 함수를 테스트하는 것에 집중하다 보면 테스트가 충분히 되지 못하기 쉽다. 보통은 모든 중요한 행동을 파악하고 각각의 테스트 케이스를 작성하는 것이 더 효과적이다.
- 결과적으로 중요한 동작을 테스트해야 한다. 프라이빗 함수를 테스트하는 것은 거의 대부분 결과적으로 중요한 사항을 테스트하는 것이 아니다.
- 한 번에 한 가지씩만 테스트하면 테스트 실패의 이유를 더 잘 알 수 있고 테스트 코드를 이해하기가 더 쉽다.
- 테스트 설정 공유는 양날의 검이 될 수 있다. 코드반복이나 비용이 큰 설정을 피할 수 있지만 부적절하게 사용할 경우 효과적이지 못하거나 신뢰할 수 없는 결과를 초래할 수 있다.
- 의존성 주입을 사용하면 코드의 테스트 용이성이 상당히 향상될 수 있다.
- 단위테스트는 개발자들이 가장 자주 다루는 테스트 수준이지만 이것만이 유일한 테스트는 아니다. 높은 품질의 소프트웨어를 작성하고 유지하려면 여러가지 테스트 기술을 함께 사용해야 할 때가 많다.
78 changes: 78 additions & 0 deletions Chapter4/mangcho.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 좋은 코드, 나쁜 코드 (4) 오류

date: February 28, 2024

---

### 💭 복구 가능성

오류는 다음 두 가지로 분류된다.

- 복구 가능한 오류
- 네트워크 오류가 발생하면 연결을 재시도할 수 있다.
- 중요하지 않은 작업에서 오류가 발생한다면, 시스템을 계속해도 괜찮다.
- 복구할 수 없는 오류
- 잘못된 입력 인수로 호출
- 일부 필요한 상태를 사전에 초기화하지 않음

오류를 복구할 방법이 있다면, 개발자는 문제를 빨리 발견하고 해결하는 것이 좋다. 여기에서 **신속한 실패**와 요란한 **실패의 개념**이 나온다.

### 💭 견고성 vs 실패

오류를 대처하는 방법은 다음 중 하나이다.

- 더 높은 코드 계층이 처리하거나 전체 프로그램이 작동을 멈춘다.
- 오류를 처리하고 계속 진행한다.

**신속하게 실패하라**

가능한 한 문제의 발생 지점으로부터 가까운 곳에서 오류가 나타나게 하는 것이다. 신속한 실패의 장점은 다음과 같다.

- 발생하자마자 바로 실패나 오류를 보여주지 않으면 문제가 발생할 때 디버그하기 어렵다.
- 코드가 제대로 작동하지 않거나 잠재적으로 문제를 일으킬 수 있다.

**요란하게 실패하라**

오류가 발생하는데도 아무도 모른다면 고칠 방법이 없다. 요란한 실패는 이를 대처하기 위한 수단이다.

다음은 오류를 처리하는 좋지 않은 방법이다.

- 기본값 반환 Ex) -1
- 아무것도 하지 않음

### 💭 **오류 전달 방법**

오류가 발생하면 일반적으로 더 높은 계층으로 오류를 알려야 한다. 전달하는 방법에는 크게 2가지가 있다.

- 명시적 방법

코드를 직접 호출한 쪽에서 오류가 발생할 수 있음을 인지할 수 밖에 없도록 한다.

- 암시적 방법

코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류를 신경쓰지 않아도 된다.



자바는 검사 예외(checked exception)와 비검사 예외(unchecked exception)의 개념을 모두 가지고 있다. 예외를 지원하는 대부분의 주요 언어는 비검사 예외만 가지고 있으므로 자바 이외의 거의 모든 언어에서 예외라는 용어는 일반적으로 비검사 예외를 의미한다.

**명시적 방법: 검사 예외 → checked exception**

컴파일러는 검사 예외에 대해 호출하는 쪽에서 예외를 인지하도록 강제적으로 조치하는데, 호출하는 쪽에서는 예외 처리를 위한 코드를 작성하거나 자신의 함수 시그니처에 해당 예외 발생을 선언해야 한다. 따라서 검사 예외를 사용하는 것은 오류를 전달하기 위한 명시적인 방법이다.

**암시적 방법: 비검사 예외 → unchecked exception**

비검사 예외를 사용하면 다른 개발자들은 코드가 이 예외를 발생시킬 수 있다는 사실을 전혀 모를 수 있다. 따라서 비검사 예외는 오류가 발생할 수 있다는 것을 호출하는 쪽에서 인지하리라는 보장이 없기 때문에 오류를 암시적으로 알리는 방법이다.

### 💭 요약

- 오류에는 크게 두 가지 종류가 있다.
- 시스템이 복구할 수 있는 오류
- 시스템이 복구할 수 없는 오류
- 해당 코드에 의해 생성된 오류로부터 복구할 수 있는지 여부를 해당 코드를 호출하는 쪽에서만 알 수 있는 경우가 많다.
- 에러가 발생하면 신속하게 실패하는 것이 좋고, 에러를 복구할 수 없는 경우에는 요란하게 실패하는 것이 바람직하다.
- 오류를 숨기는 것은 바람직하지 않을 때가 많으며, 오류가 발생했다는 신호를 보내는 것이 바람직하다.
- 오류 전달 기법은 두 가지 범주로 나눌 수 있다.
- 명시적 방법: 코드 계약의 명확한 부분. 호출하는 쪽에서는 오류가 발생할 수 있음을 인지한다.
- 암시적 방법: 코드 계약의 세부 조항을 통해 오류에 대한 설명이 제공되거나 전혀 설명이 없을 수도 있다. 오류가 발생할 수 있다는 것을 호출하는 쪽에서 반드시 인지하는 것은 아니다.
- 복구할 수 없는 오류에 대해서는 암시적 오류 전달 기법을 사용해야 한다.
Loading