Skip to content

Latest commit

 

History

History
632 lines (516 loc) · 34.4 KB

Good Code, Bad Code.md

File metadata and controls

632 lines (516 loc) · 34.4 KB

이 책을 읽는 이유: 동료들과 더 좋은 코드 리뷰를 위해

CH01. 코드 품질

  • 고품질의 코드는
    • 신뢰할 수 있고
    • 유지보수가 쉬우며
    • 요구 사항을 충족하는
    • 소프트웨어가 될 가능성을 최대한으로 높여줌
  • 코드는 작동해야 한다
    • 당연한 코드의 첫 번째 목표
    • 원하는 목적대로 동작해야 함
  • 코드는 작동이 멈추면 안 된다
    • 코드가 의존하는 다른 코드가 변경되더라도 작동하도록 해야 함
  • 코드는 변경된 요구 사항에 적응할 수 있어야 한다
    • 변화하는 비지니스 환경과 사용자 선호에 따라 가정이 더이상 유효하지 않고 새로운 기능이 추가될 수 있음
    • 이에 쉽게 적응할 수 있어야 함
    • 하지만 너무 먼 미래를 고려하면 속도가 늦어지니 적당한 트레이드오프를 찾아야 함
  • 코드는 이미 존재하는 기능을 중복 구현해서는 안 된다
    • 기존 코드를 재활용하면
      • 시간과 노력을 절약하고
      • 버그 가능성을 줄여주고
      • 기존 전문지식을 활용하며
      • 코드가 이해하기 쉬운
      • 효과를 얻을 수 있음
  • 코드 품질의 핵심 요소 (6가지임)
    • 코드는 읽기 쉬워야 한다
    • 코드는 예측 가능해야 한다
    • 코드를 오용하기 어렵게 만들어라
    • 코드를 모듈화해라
    • 코드를 재사용 가능하고 일반화할 수 있게 작성하라
    • 테스트가 용이한 코드를 작성하고, 제대로 테스트해라

CH02. 추상화 계층

  • 문제를 해결하는 코드를 잘 구성하는 것은

    → 간결한 추상화 계층을 만드는 것으로 귀결될 때가 많음

  • 왜 추상화 계층을 만들어야할까?

    • 이미 해결된 하위문제에 대해서 생각하지 않아도 되게 함
  • 이를 위해서는 클래스도 잘 나눠야하고 함수도 잘 나눠야 함

    • 한 클래스가 300줄을 넘어간다는 것은 제대로 추상화되지 않았을 가능성이 높음
    • 클래스는 응집력이 높게 설계해야 함
      • 응집력은 순서에 의한 순차적 응집력과 기능에 의한 기능적 응집력이 있음
    • 예를 들어, 함수명이 너무 길어진다는 것은
      • 해당 함수가 두 가지 이상의 일을 하고 있거나
      • 해당 함수가 제대로 추상화되지 않았을
      • 가능성이 높음
  • 하나의 하위 문제에 대해 (1) 둘 이상의 서로 다른 구체적인 구현이 가능하고 (2) 구현 클래스 사이에 전환이 필요하다면

    → 인터페이스를 사용하는 것이 좋음

    • 하지만 한 가지 구현만 존재하고 앞으로 다른 구현을 추가할 계획이 없다면 팀과 결정해야함
    • 이런 경우에는 인터페이스를 사용하는 것엔 장단점이 있으니 충분히 고민해보고 결정하자

CH03. 다른 개발자와 코드 계약

  • 다른 개발자들이 활발하게 코드를 변경하더라도 코드의 품질이 유지되려면 코드가 튼튼하고 사용하기 쉬워야 함

  • 이를 위해 명심할 것

    • 자신에게 분명하다고 해서 다른 사람에게도 분명한 것은 아님

      → 이를 위해 주석을 많이 사용하는 것이 아닌, 코드 그 자체로 이해하기 쉽고 설명되게 해야함

    • 다른 개발자는 무의식중에 나의 코드를 망가뜨릴 수 있음

      → 내가 작성한 코드에 대해 테스트코드를 작성하고 이를 병합 전 체크하게 함으로써 망가지는 것을 방지해야함

    • 시간이 지나면 자신의 코드를 기억하지 못함

      → 위의 두 항목은 언제든지 나에게도 나의 코드에 대해 찾아올 수 있음

  • 어떻게 내가 작성한 코드의 사용법을 다른 사람들이 알게할까?

    • 그들이 알아야할 내용

      • 여러 가지 상황에서 어떤 함수를 호출해야 하는지
      • 클래스가 무엇을 나타내고 언제 사용되어야 하는지
      • 어떤 값을 인수로 사용해야 하는지
      • 코드가 수행하는 동작이 무엇인지
      • 어떤 값을 반환하는지
    • 그리고 보통 이를 위해

      • 함수/클래스/열거형 등의 이름을 살펴보거나
      • 함수와 생성자의 매개변수 유형 or 반환값의 유형과 같은 데이터 유형을 살펴봄
    • 따라서 일단 뭐든지간에 “이름을 잘 지어야” 직관적으로 명확하게 전달할 수 있음

    • 또한, 코드가 오작동하지 않도록 데이터 유형을 잘 정해둬야함

    • 작성한 코드를 사용할 때 구체적인 제약 사항이 있다면 주석을 사용하는 것은 바람직하지 않을 수 있음

      → 코드 자체에 지켜진 순서를 따르지 않으면 적절한 에러를 발생시키거나 null을 반환하게 함으로써 해결해야함

      (예를 들어, 생성자를 호출하면 안되는 경우 아예 private으로 만들어버린다던지)

    • 이러한 해결 방법은 ‘상태’와 ‘가변성’을 최소화 or 외부로의 노출을 없앰

CH04. 오류

  • 복구 가능성

    • 복구 가능한 오류

      • 치명적이지 않은 경우, 오류가 발생하더라도 사용자가 알아채지 못하도록 적절히 처리하면 작동을 계속할 수 있음

      • 그 예로, 잘못된 사용자 입력 or 네트워크 오류 or 중요하지 않은 작업 오류 등이 있음

      • 시스템 외부의 무언가에 의해 야기되는 오류는 그에 대한 처리를 해주기 위해 노력해야 함

        → 예상할 수 있는 경우이기 때문

    • 복구할 수 없는 오류

      • 프로그래밍 오류일 떄가 많음 → 코드를 잘못 사용
      • 피해를 최소화하고 개발자가 문제를 발견하고 해결할 가능성을 최대화 하는 것이 방법
    • 그렇다면 어떻게 복구할 수 있을까?

      • 하드 코딩이 되어있다면 → 당연히 복구 불가능

      • 일반적으로 함수가 어떻게 사용되고 어디서 호출되는지 알 수 없음

        → 즉, 해당 함수를 호출하는 쪽에서 복구를 진행해야함

      • 그렇기에 아래의 경우에는 호출 함수의 오류는 호출하는 쪽에서 복구하고자 하는 것으로 간주해야 함

        • 함수가 어디서 호출될지 그리고 호출 시 제공되는 값이 어디서 올지 정확한 지식이 없을 때
        • 코드가 미래에 재사용될 가능성이 아주 희박할 때
  • 어떻게 호출하는 쪽에서 오류를 알 수 있을까?

    = 어떻게 오류를 전달할 수 있을까?

    • 명시적 방법: 오류가 발생할 수 있음을 인지할 수 밖에 없도록 함
      • 검사 예외 (Checked Exception)
        • 예외 처리를 위한 코드를 작성하거나 함수 시그니처에 해당 예외 발생을 선언하도록 강제함
      • 널값이 가능한 반환 유형
        • 에러 발생 시 널이 반환될 수 있다는 것을 호출하는 쪽에서 강제적으로 인지하게 함 (시그니처 or 객체 타입으로 확인)
        • 널 안정성을 지원하지 않는다면 옵셔널 반환 유형을 사용해야 함
      • 리절트 반환 유형
        • 널값 or 옵셔널 타입의 경우, 오류 정보를 전달할 수 없다는 단점이 있음
        • value와 error를 모두 포함하도록 리절트 객체 사용
        • 하지만 언어 자체에서 지원하지 않는다면 섬세하게 구현되어야함
          • 예를 들어, getValue() 호출 전에 무조건 hasError() 가 실행되어야함
      • 아웃컴 반환 유형
        • 반환값을 통해 오류 발생 여부를 알림 → 값을 강제적으로 확인해야 하므로 명시적임 (결과를 나타내는 값임)
        • 하지만, 반환값을 무시하거나 값을 반환한다는 사실을 인식 못할 수 있음 → 한계가 있음
        • 자바의 경우 @CheckReturnValue 어노테이션을 통해 반환값을 무시하면 컴파일러가 경고하도록 할 수 있음
    • 암시적 방법: 오류가 발생할 수 있음을 알리지만, 신경쓰지 않아도 됨
      • 비검사 예외 (Unchecked Exception)
        • 이 경우에는 예외가 발생할 수 있다는 사실을 모를 수 있음
      • 프로미스 or 퓨처
        • 호출하는 쪽에서는 잠재적인 오류의 시나리오를 완벽하게 알지 못함 → then() 함수만 사용할 수 있음
      • 매직값 반환
        • 개발자가 정한 특정 값에 의미를 부여하여 알림
  • 견고성 vs 실패

    • 오류 발생 시 선택할 수 있는 방법

      • 실패 (더 높은 계층이 오류를 처리하거나 전체 프로그램의 작동을 멈춤)
      • 견고성 (오류를 처리하고 계속 진행)
    • 대부분의 경우 실패가 최선임

    • “신속하게 실패하라”

      = 가능한 한 문제의 실제 발생 지점으로부터 가까운 곳에서 오류를 나타내야 함

      → 복구할 수 있는 경우, 호출하는 쪽에서 오류로부터 안전하게 복구할 수 있는 기회를 최대한 제공함

      → 복구할 수 없는 경우, 개발자가 문제를 신속하게 파악하고 해결할 수 있는 기회를 최대한 제공함

    • “요란하게 실패하라”

      = 오류가 발생하는데도 불구하고 아무도 모르는 상황을 막고자 함

      • 예외를 발생시켜 프로그램이 중단되게 해야 함
    • 여기서 모순점이 존재함

      • 가장 요란하게 실패하는 것은 프로그램을 멈추는 것인데, 이것은 프로그램을 견고하지 못하게 만듦

      try-catch 등을 활용하여 오류가 발생해도 프로그램을 진행시킬 수 있게 하고 오류를 기록 & 모니터링해야함

    • 오류를 숨기지 말아야 함

      • 때로는 오류를 숨기고 아무 일도 없었던 것처럼 동작하도록 코드를 작성하고 싶을 수 있음
      • 하지만 이것은 소프트웨어가 의도한대로 작동하지 않게 만듦
      • 오류를 숨기는 안티 패턴
        • 기본값 반환 → 기본값이 의미가 있을 수 있기에 이렇게 하지 말고 오류를 던져야함
        • 널 객체 패턴 → 유용할 수 있지만 종종 버그로 이어지기 쉬움
        • 아무것도 하지 않음 (e.g. return;) → 호출하는 쪽에서는 정상 처리가 된것으로 생각하므로 바람직하지 않음

    ⇒ 복구 가능성이 없는 오류가 발생하면 “신속”하고 “요란”하게 실패하는 것이 최상의 방법

  • 비검사 예외를 사용해야한다 vs 명시적 기법을 사용해야한다

    • 오류 핸들링에는 아직 정도가 없기에 두 의견이 분분함
    • “비검사 예외를 사용해야한다”의 장점
      • 코드 구조 개선
        • 대부분의 오류 처리가 코드의 상위 계층에서 이뤄질 수 있기 때문에 코드의 구조를 개선할 수 있음
      • 개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함
        • 검사 예외를 사용하게 되면, 시그니처에 표시해야함
        • 하지만 이 경우, 해당 함수를 호출하는 모든 코드를 수정해야함
        • 이게 귀찮다고 여겨 오류를 숨기고 try-catch만으로 처리시킬 수 있음
        • 이러한 사고와 과정이 실용적이지 못하다는 주장
    • “명시적 기법을 사용해야 한다”의 장점
      • 매끄러운 오류 처리
        • 비검사 예외를 사용하면 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기 어려움
        • 호출하는 쪽에 잠재적 오류를 강제적으로 인식하도록 하면 이러한 오류를 좀 더 매끄럽게 처리할 가능성이 커짐
      • 실수로 오류를 무시할 수 없음
      • 개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함
        • 비검사 예외를 사용하면 제대로 문서화된다는 보장이 없음

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

  • 서술형 명칭 사용

    • 서술적이지 않은 이름은 코드를 읽기 어렵게 만듦
      • 변수명/함수명/클래스명 을 해당 로직 (or 도메인)에 맞게 작성해야 코드만 봐도 이해할 수 있음
    • 주석문으로 서술적인 이름을 대체할 수 없음
      • 주석으로 로직 or 변수 or 함수를 설명하는 건 제발 자제하자..
  • 주석문의 적절한 사용

    • 주석문 (문서화)를 사용한다면 일반적으로 두가지 목적이 있음
      • 코드가 무엇을 하는지 설명
      • 코드가 왜 그 일을 하는지 설명
    • 주석을 남용하면 안되는 이유는? → 주석문도 유지보수해야함
    • 중복된 주석문은 유해할 수 있음
      • 코드 그 자체로 설명이 되고 이해가 쉬운 경우, 주석문은 중복임
    • 주석문으로 가독성 높은 코드를 대체할 수 없음
    • 주석문은 코드의 이유를 설명하는 데에는 유용함
      • 제품 or 비지니스 의사 결정
      • 이상하고 명확하지 않은 버그에 대한 해결책
      • 의존하는 코드의 예상을 벗어나는 동작에 대처
    • 주석문은 유용한 상위 수준의 요약 정보를 제공할 수 있다
      • 클래스와 같은 상위 수준에 대해서 요약 정보를 제공하는 것은 유용할 수 있음
  • 코드 줄 수를 고정하지 마라

    • 짧은 코드일수록 일반적으로는 좋음

    • 하지만 너무 강박에 빠지면 안됨

    • 짧게 하기 위해 이런 코드를 작성하는 것은 지양하자

      Boolean isIdValid(UInt16 id) {
      	return countSetBits(id & 0x7FFF) % 2 == ((id & 0x8000) >> 15);
      }
      • 3항연산자도 마찬가지
  • 일관된 코딩 스타일을 고수하라

    • 파스칼 케이스를 사용할지 카멜 케이스를 사용할지 정하고 일괄 적용하자
  • 함수 내부를 깊게 중첩시키지 말자

  • 호출 함수의 가독성을 챙기자

    • 매개변수를 알기 쉽게 하자 (호출 시점에)

      → 타입 힌트 사용

      → Enum 활용

  • 하드코드된 상수는 상수 변수로 만들어서 관리하자

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

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

    • 매직값은 버그를 유발할 수 있음

      • 레거시 코드에서 찾아볼 수 있음 (e.g. indexOf())
      • 과거에는 더 명시적인 전달 기법이 가능하지 않거나 실용적이지 않아서 합리적인 방법이었음
      • 매직값의 존재는 보통 주석을 통해 파악할 수 있는데, 이를 인지하지 못하면 버그를 유발함

      Null or Optional or Exception 을 반환해야 함

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

    • Collection 객체가 그냥 Null보다 나을 수 있음
    • 하지만 빈 문자열의 경우에는 오해의 소지가 있으니 그냥 Null 반환이 나음
  • 예상치 못한 부수 효과를 피하라

    • 한 함수 내에 함수의 역할과 다른 기능이 존재한다면 해당 함수를 호출하는 쪽에서는 부수 기능에 의한 side effect를 예상할 수 없음
    • 그리고 이러한 코드는 멀티 스레드 환경에서는 더 큰 버그를 유발할 수 있음

    ⇒ 문제를 해결하려면, 함수를 분리하던지 아니면 함수명을 다른 기능에 대해 알 수 있게끔 작성

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

    • 입력 매개변수를 수정하는 것은 함수가 외부의 무언가에 영향을 미친다는 것 → 잠재적 버그 유발

    ⇒ 변경하고 싶다면 복사해서 변경해야 함

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

    • 매개변수에 대해 명확히 명시해주는 것이 좋음

    Enum을 사용하면 깔끔하게 처리해줄 수 있음

    (e.g. Null 이 들어오지 못하게 하거나, 특정 타입만 들어오게 하거나)

  • 하지만 Enum 사용에 주의할 점이 있음

    • if 문으로 처리하면, 앞으로 추가될 Enum 값에 대해 확장적이지 못함

    switch 문을 사용하고 기존에 정의된 값이 아니라면 Exceptionthrow하자

    (그리고 모든 값에 대해 Exception이 발생하지 않는 테스트 코드를 추가하자)

    (그리고 default + throw exception은 지양하자)

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

  • 코드가 오용될 수 있는 몇 가지 일반적인 경우

    • 호출하는 쪽에서 잘못된 입력을 제공
    • 다른 코드의 부수 효과
    • 정확한 순서에 따라 함수를 호출하지 않음
    • 관련 코드에서 가정과 맞지 않게 수정이 이루어짐

    → 를 어떻게 방지할까?

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

    • 가변 객체의 뭐가 문제길래?

      • 가변 객체는 추론하기 어려움
      • 가변 객체는 멀티스레드 환경에서 문제가 발생하기 쉬움
    • 아래의 코드를 보자

      class TextOptions {
          private Font font;
          private Double fontSize;
      
          TextOptions(Font font, Double fontSize) {
              this.font = font;
              this.fontSize = fontSize;
          }
      
          void setFont(Font font) {
              this.font = font;
          }
      
          void setFontSize(Double fontSize) {
              this.fontSize = fontSize;
          }
          ...
      }
      • 이 코드의 문제는 set 함수들을 호출할 수 있는 클라이언트에 제한이 없다는 것임
      • 클래스 내부에 가변 객체가 존재하고 이 가변 객체들을 마음껏 변경할 수 있다는 것이 문제
    • 해결책 1: 객체를 생성할 때만 값을 할당하라 (그 이후엔 불변)

    • 해결책 2: 불변성에 대한 디자인 패턴을 사용하라

      • 빌더 패턴 or 쓰기 시 복사 패턴
  • 객체를 깊은 수준까지 불변적으로 마드는 것을 고려하라

    • 불변 필드로 선언했지만, 해당 필드가 객체를 참조하고 있다면 다른 경로를 통해 수정될 수 있음
    • 해결책1: 방어적으로 복사하라
      • 참조해야하는 객체유형 (e.g. Reference Type)를 생성자로 받게될 경우, 복사해서 받으면 됨
      • 그리고 getter에서도 복사해서 return해주면 됨
      • 단점이 있다면 복사하는 오버헤드가 클 수 있다는점
    • 해결책2: 불변적 자료구조를 사용하라
      • 자바의 경우에는 외부 라이브러리를 사용해야함..
  • 지나치게 일반적인 데이터 유형을 피하라

    • 지나치게 일반적인 유형은 오용될 수 있음

      • 왜냐? 일반적인 유형 (e.g. List) 이기 때문에 무엇을 의미하는지 직관적으로 알 수 없음

      → 적절한 자료구조를 만들어 쓰자

  • 시간 처리

    • 정수로 시간을 나타내는 것은 문제가 될 수 있음

      • 왜냐?
      • 정수로 표현된 시간은 많은 것을 의미할 수 있음 (e.g. 절대적인 시간, 기간 등)
      • 또 단위도 정말 다양함
      • 또 시간대도 다양함

      → 이것 또한 적절한 자료구조를 사용해서 해결해야 함.. (e.g. java.time)

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

CH08. 코드를 모듈화하라

  • 모듈화의 주된 목적은?
    • 코드는 어떻게된 변경되거나 재구성될 가능성이 있음

      → 변경과 재구성이 용이한 코드가 필요하고 모듈화를 통해 이게 쉬워질 수 있음

⇒ 하위 문제에 대한 해결책의 자세한 세부 사항들이 독립적이고 밀접하게 연관되어있지 않게 함

  • 의존성 주입의 사용을 고려하라
    • 하드코딩된 의존성은 문제가 될 수 있음

      class RoutePlanner {
      	private final RoadMap roadMap;
      
      	RoutePlanner() {
      		this.roadMap = new NorthAmericaRoadMap();
      	}
      
      	...
      }
      
      interface RoadMap {
      	List<Road> getRoads();
      	List<Junction> getJunctions();
      }
      • 위처럼 생성자에서 의존성을 선언하여 갖게되는 경우, RoadMap의 다른 구현을 사용하고자 할때 이 코드를 무조건 수정해야 함
      • 또는 RoadMap의 생성자가 변경되었을 떄에도 영향을 받게 됨
      • RoutePlanner의 생성이 쉽다는 장점이 있기는 함
    • 의존성을 주입받도록 고쳐보자

      class RoutePlanner {
      	private final RoadMap roadMap;
      
      	RoutePlanner(RoadMap roadMap) {
      		this.roadMap = roadMap;
      	}
      
      	...
      }
      • 이렇게 되면 RoutePlannerRoadMap의 구현체가 어떻게 바뀌던 수정될 필요가 없어졌음
  • 인터페이스에 의존하라
    • 구현체가 변경될 가능성이 있다면 → 인터페이스 타입으로 주입받게 하는 것이 편함
  • 클래스 상속을 주의하라
    • 상속보다는 Composite Pattern을 활용하여 필드로 갖고있게 하는 것이 좋을 때가 많음
    • 상속은 추상화에 방해가 될 수 있음
    • 상위 클래스의 수정이 이를 상속 받는 모든 하위클래스들에게 영향을 미침
    • 진정한 is-a 관계여도 신중히 고려해보자
  • 클래스는 자신의 기능에만 집중해야 한다
    • 다른 클래스와 지나치게 연관되게 하면 안됨

      class Book {
      	private final List<Chapter> chapters;
      
      	int wordCount() {
      		return chapters.map(getChapterWordCount).sum();
      	}
      
      	private static int getChapterWordCount(Chapter chapter) {
      		return chapter.getPrelude().wordCount() + chapter.getSections().map(section -> section.wordCOunt)).sum();
      	}
      
      	...
      }
      
      class Chapter {
      	TextBlock getPrelude() { ... }
      	
      	List<TextBlock> getSections() { ... }
      
      	...
      }
      • 여기서 잘못된 점은?
      • Book 클래스가 Chapter의 단어 수를 세는 로직을 갖고 있음
      • Book 클래스는 Chapter에 대한 세부 로직을 갖고 있을 이유가 전혀 없음
      • Book 클래스에 관한 함수들은 Book 클래스의 메소드로 옮겨주자
    • 디미터의 법칙

      • 한 객체가 다른 객체의 내용이나 구조에 대해 가능한 한 최대로 가정하지 않아야 함
      • 예를 들어 잘못된 코드를 보면 Book 클래스는 Chapter 클래스와 상호관계를 하지만 내부에서 TextBlock 클래스와도 상호작용을 하고 있음
    • 관련 있는 데이터는 함께 캡슐화하라

      • 외부에서 매개변수로 받을 필요가 없는 데이터의 경우, 내부 필드로 존재하는 것이 옳은 방향임
    • 반환 유형에 구현 세부 정보가 유출되지 않도록 주의하라

      • 전용 DTO를 만들어 사용하자
      • 또는 적절한 기본 자료구조를 사용하자
    • 예외 처리 시 구현 세부 사항이 유출되지 않도록 주의하라

      • 그에 맞는 Exception 클래스를 정의하여 사용

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

풀고자 하는 하위 문제에 대해 다른 개발자가 미리 해결한 코드가 있을 수 있음

→ 하지만 항상 재사용하기 좋은 것은 아님, 그럼 어떻게 재사용 가능한 코드를 만들 수 있을까?

  • 가정을 주의하라

    • 가정을 하면 개발은 훨씬 쉬워짐, 하지만 활용도가 낮아지고 재사용하기 어려워짐

    • 이러한 가정을 잘 모르고 재사용하게 되면 당연히 버그가 발생하게 됨

      class Articcle {
      	private List<Section> sections;
      	...
      	List<Image> getAllImages() {
      		for (Section section in sections) {
      			if (section.containsImages()) {
      				// 기사 내에 이미지를 포함하는 섹션은 최대 하나만 있다. [가정]
      				return section.getImages();
      			}
      		}
      	}
      }
      • Method명만 보면 모든 이미지를 가져온다고 생각할 수 있으나, 사실 가정에 의해 한 섹션의 이미지만 가져옴
    • 해결책1: 가정을 제거하자

      class Articcle {
      	private List<Section> sections;
      	...
      	List<Image> getAllImages() {
      		List<Image> images = new ArrayList<>();
      		for (Section section in sections) {
      			images.addAll(section.getImages());
      		}
      		return images;
      	}
      }
      • 모든 Section의 이미지를 찾게 로직을 수정한다고 해도 성능의 큰 차이가 나지는 않음
    • 해결책2: 가정이 필요하면 강제적으로하자

      class Articcle {
      	private List<Section> sections;
      	...
      	Section? getImageSection() {
      		// 기사 내에 이미지를 포함하는 섹션은 최대 하나만 있다. [가정]
      		return sections.filter(section -> section.containsImages()).first();
      	}
      }
      • 이러면 Method명에서도 일단 알 수 있고
      • Return type이 nullable 한 것에서도 확인할 수 있음
      • 하지만 이미지를 두 개 이상 갖고 있는 Section을 처리할 때 사용할 수 있는 오해는 아직 남아있음
      class Articcle {
      	private List<Section> sections;
      	...
      	Section? getOnlyImageSection() {
      		List<Section> imageSections = sections.filter(section -> section.containsImages());
      		assert(imageSections.size() <= 1, "기사가 여러 개의 이미지 섹션을 갖는다.");
      		return imageSections.first();
      	}
      }
      • 이렇게 아예 검증을 추가하고 오류를 발생시키게 해야함
  • 전역 상태를 주의하라

    • 전역변수 (자바에서는 static)는 프로그램 내의 모든 컨텍스트에 영향을 미치므로 해당 코드를 다른 목적으로 재사용(수정)하지 않을 것이라는 암묵적인 가정을 전제함
    • 전역 상태를 갖는 코드는 재사용하기에 안전하지 않을 수 있음
    class ShoppingBasket {
    	private static List<Item> items = new ArrayList<>();
    		
    	static void addItem(Item item) {
    		items.add(item);
    	}
    
    	static List<Item> getItems() {
    		return List.copyOf(items);
    	}
    }
    • 이 코드의 문제는 뭘까?

    • 장바구니를 클라이언트가 아닌 서버가 관리한다면, 사용자당 인스턴스를 하나씩 만들어야 저 코드의 가정이 이루어짐

    • 장바구니 저장 기능이 추가된다면? → 재사용 불가

    • 다른 물건에 대한 장바구니가 필요하다면? → 재사용 불가

    • 어떻게 해결하지?

      → 정적 클래스를 인스턴스화하고 사용 시에 의존성 주입을 받아 사용하도록 하자

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

    • 낮은 층위의 코드의 기본 반환값은 재사용성을 해칠 수 있음
      • 결국 기본 반환값은 하드코딩되는 값이므로 높은 수준의 코드에서 처리해줄수록 재사용성이 증가함
      • 예를 들어, 기본 반환값으로 null을 반환하면 이를 사용하는 상위 코드에서 알맞게 처리해주면 됨
  • 함수의 매개변수를 주목하라

    • 필요 이상으로 매개변수를 받는 함수는 재사용하기 어려울 수 있음
  • 제네릭의 사용을 고려하라

    • 다른 클래스를 참조하지만, 여러 클래스가 가능하다면 제네릭을 쓰자

CH10. 단위 테스트의 원칙

  • 단위 테스트 기초
    • 테스트 코드는 일반적으로 대상 코드와 1:1 맵핑됨
    • 테스트 케이스는 arrange (given) → act (when) → assert (then) 의 과정을 거침
    • 테스트 대상 로직이 작동하는 모든 테스트 케이스에 대한 코드가 존재해야 함
  • 좋은 단위 테스트는 어떻게 작성할 수 있는가?
    • 좋은 단위 테스트가 가져야할 5가지 주요 기능
      • 훼손의 정확한 감지
        • 코드에 대한 초기 신뢰를 줌
        • 미래의 훼손을 막아줌
      • 세부 구현 사항에 독립적
        • 동작에 대한 테스트 코드를 작성한 경우, 리팩토링한 코드의 동작을 코드 수정 없이 검증할 수 있음
        • 그리고 기능 변경과 리팩토링은 같이 하면 안됨
      • 잘 설명되는 실패
        • 개발자의 코드 수정으로 기존 테스트 코드가 실패하면 → 왜 실패인지 알려줘야함
        • 이를 위해 테스트 케이스의 이름을 상세히 작성하고
        • 실패 메시지를 구체적으로 적어줘야 함
      • 이해할 수 있는 테스트 코드
      • 쉽고 빠르게 실행
  • 퍼블릭 API에 집중하되 중요한 동작은 무시하지 말라
    • 구현사항인 private 메소드를 제외한 퍼블릭 API에 대한 테스트는 동작에 대한 테스트가 맞음
    • 구현 세부사항은 언제든 변경될 수 있고, 퍼블릭 API의 관심사가 아니므로 테스트 코드 작성을 해야할 필요는 없음
    • 하지만 여러 의존성에 대한 것은 어떻게 해야할까?
    • 의존성이 테스트 대상 코드의 동작에 큰 영향을 준다면 함께 테스트되어야하지만
    • 그게 아니라면 테스트 대상이 아님
  • 테스트 더블
    • 위에서 언급한 의존성을 실제로 사용하는 것은 힘든 경우가 많기 때문에 목 / 스텁 / 페이크 방법이 제안됨
    • 테스트 더블을 사용하는 이유
      • 테스트 단순화 → 의존성을 제거하였기 때문
        • 의존성을 다 추가한다면 엄청 번거로워짐
        • 그리고 의존성을 제거하면 테스트 코드의 속도 역시 빨라짐
      • 테스트로부터 외부 세계 보호 → 실제 의존성에 작업이 전달되지 않기 때문
      • 외부로부터 테스트 보호 → 테스트를 독립된 데이터를 사용하여 수행할 수 있음
    • Mock (목)
      • 부수 효과를 일으키는 의존성을 시뮬레이션하는 데 가장 유용함
      • 목을 통해 어떠한 함수가 올바른 인수로 호출되는지 확인함 (verify)
    • Stub (스텁)
      • 이게 주로 말하는 Mocking인듯
      • 함수가 호출될 때 미리 정해 놓은 값을 반환하게 함
    • 하지만 Mock과 Stub에 반대하는 측도 있음
      • 실제 의존성의 작동이 아님
        • 관련 비지니스 로직을 제대로 알 지 못하면 말도 안되는 테스트케이스 일 수 있음
      • 구현 세부 사항과 밀접하게 결합하여 리팩토링이 어려워질 수 있음
    • Fake (페이크)
      • 의존성을 구현하는 페이크 클래스를 만들어 사용
      • 근데 너무 귀찮아 보임
    • 런던파 vs 고전파
      • 런던파: 테스트 대상 코드의 상호작용을 테스트 (”어떻게” 작동하는가)
        • 단위 테스트가 더욱 격리됨 (독립적임)
        • 테스트 코드 작성이 더욱 쉬워짐
      • 고전파: 코드의 결과 상태와 의존성을 테스트 (최종 결과가 “무엇”인가)
        • 목은 실제가 아니므로 정확한 검증이 아님
    • 결국 그냥 작은 로직들에 대해서는 Mocking해서 단위테스트 하고 큰 로직은 통합테스트하자

CH11. 단위 테스트의 실제

단위 테스트에 적용할 수 있는 여러가지 실제적인 기술을 설명함

  • 기능뿐만 아니라 동작을 실험하라
    • 테스트 대상 코드가 수행하는 작업은 여러가지 → 각각의 작업에 대해 테스트 케이스를 따로 작성해야 함
    • 기능에만 집중하면 해피케이스 하나에 대해서만 테스트 케이스가 존재하게 됨
    • 테스트 코드가 실제 코드보다 양이 많은 경우가 있는데, 이것이 정상임
    • 오류 발생 상황 역시 테스트되어야 함
  • 테스트만을 위해 퍼블릭으로 만들지 말라
    • 프라이빗 함수는 구현의 세부사항임
    • 그리고 테스트 코드는 구현의 세부사항을 위해 존재하는 것이 아님
    • 테스트코드는 퍼블릭 API에 대해서만 존재해야한다!!!!!!
    • 퍼블릭 API가 너무 크고 복잡할 때, 다른 프라이빗 함수를 퍼블릭으로 만들고 테스트하고싶어짐
    • 하지만 이것은 추상화 계층이 크다는 것을 의미하고 더 작은 단위로 분할해야 한다!!! 명심하자
  • 한 번에 하나의 동작만 테스트하라
    • 여러 동작을 한꺼번에 테스트하면 테스트가 제대로 안 될 수 있음
    • 그리고 실패 이유에 대해 명확하지 않음
  • 공유 설정을 적절하게 사용하라
    • beforeEach, beforeAll, afterEach, afterAll 을 잘 활용하자
    • 하지만 테스트 케이스 간에 영향을 줄 수 있는 상태에 대한 공유는 피해야 함 (독립적이어야하기 때문)
    • 중요한 설정은 테스트 케이스 내부에서 정의해야 함
  • 적절한 어서션 확인자를 사용하라
    • 즉, 테스트 결과에 대해 검증을 어서션을 사용해서 하라는 뜻
    • 적절한 어셔션을 사용해야 테스트 케이스 실패 시, 실패한 이유를 명확하게 알 수 있음
  • 테스트 용이성을 위해 의존성 주입을 사용하라
    • 의존성이 하드 코딩되어있다면, 모킹이 불가해짐