티스토리 뷰

나의 공부방

[개발서적] 좋은 코드, 나쁜 코드 핵심 내용 정리 및 요약

망나니개발자 2024. 5. 28. 10:00
반응형

 

 

1. 코드 품질


[ 1.2 코드 품질의 목표 ]

필자는 코드를 작성할 때 다음과 같은 네 가지 상위 수준의 목표를 달성하려고 한다. 필자가 생각하기로 이러한 것을 달성하는 데 도움이 되면 그 코드는 높은 품질의 코드이고, 방해된다면 낮은 품질의 코드다.

  1. 작동해야 한다.
  2. 작동이 멈춰서는 안 된다.
  3. 변화하는 요구 사항에 적응해야 한다.
  4. 이미 존재하는 기능을 또다시 구현해서는 안 된다.

 

 

1.2.1 코드는 작동해야 한다

코드의 첫 번째 목표는 그것이 애초 작성된 목적대로 동작해야 한다는 것이다. 코드는 우리가 해결하려고 하는 문제를 실제로 해결해야 한다. 이것은 또한 버그가 없다는 것을 의미하는데, 버그가 존재하면 코드가 제대로 작동하지 않고 문제를 완전히 해결하지 못할 가능성이 있기 때문이다.

 

 

1.2.2 코드는 작동이 멈추면 안 된다

코드는 고립된 환경에서 홀로 실행되는 것이 아니다. 주의하지 않으면 주변 상황이 바뀌면서 동작이 멈출 수 있다.

  • 코드는 다른 코드에 의존할 수 있는데, 그 코드가 수정되고 변경될 수 있다.
  • 새로운 기능이 필요할 때 코드를 수정해야 할 수도 있다.
  • 우리가 해결하려고 하는 문제는 시간이 지남에 따라 변경된다. 소비자 선호, 비즈니스 요구, 고려해야 할 기술 등이 바뀔 수 있다.

 

 

1.2.3 코드는 변경된 요구 사항에 적응할 수 있어야 한다

소프트웨어의 요구 사항이 시간이 지남에 따라 변할 것이라는 사실을 알지만, 또 다른 한편으로 요구 사항이 어떻게 변할 것인지는 정확히 알 수 없다. 코드나 소프트웨어가 시간이 지남에 따라 어떻게 변할지 완벽하고 정확하게 예측하는 것은 불가능하다.

 

 

1.2.4 코드는 이미 존재하는 기능을 중복 구현해서는 안 된다

기존 해결책을 무시하고 자기가 다시 작성하는 대신 이미 구현된 코드를 재사용하면 좋은 몇 가지 이유가 있다.

  • 시간과 노력을 절약한다
  • 버그 가능성을 줄여준다
  • 기존 전문지식을 활용한다
  • 코드가 이해하기 쉽다

 

어떤 하위 문제를 해결하기 위해 자신이 이미 코드를 작성했다면, 다른 개발자들이 동일한 문제를 해결하기 위해 자신만의 코드를 다시 작성하지 않도록 쉽게 재사용할 수 있는 방식으로 코드를 구성해야 한다. 해당 개념은 양방향으로 적용된다.

 

 

 

[ 1.3 코드 품질의 핵심 요소 ]

코드 품질의 여섯 가지 핵심 요소는 다음과 같다.

  1. 코드는 읽기 쉬워야 한다
  2. 코드는 예측 가능해야 한다
  3. 코드는 오용하기 어렵게 만들라
  4. 코드는 모듈화하라
  5. 코드는 재사용 가능하고 일반화할 수 있게 작성하라
  6. 테스트가 용이한 코드를 작성하고, 제대로 테스트하라

 

 

1.3.1 코드는 읽기 쉬워야 한다

코드를 작성하고 난 후 어느 시점이 되면 다른 개발자가 그 코드를 읽고 이해해야 하는 상황이 반드시 온다. 코드가 병합되기 전에 코드 검토를 받아야 한다면, 그 일은 코드 작성 후 바로 일어날 것이다.

코드의 가독성이 떨어진다면, 다른 개발자가 그 코드를 이해하는 데 많은 시간을 들여야 한다. 또한 코드의 기능에 대해 잘못 이해하거나 몇 가지 중요한 세부 사항을 놓칠 가능성 역시 크다. 이런 일이 일어난다면 코드 검토 중에 버그를 발견할 가능성이 작고, 새로운 기능을 추가하기 위해 다른 사람이 코드를 수정할 때 새로운 버그가 도입될 가능성이 크다.

 

 

1.3.2 코드는 예측 가능해야 한다

우리의 코드를 사용하는 다른 개발자는 이름, 데이터 유형, 일반적인 관행과 같은 단서를 사용해 코드가 입력값으로 무엇을 예상하는지, 코드가 무슨 일을 하는지, 그리고 무엇을 반환하는지에 대한 정신 모델을 구축한다. 이 정신 모델과 어긋나는 어떤 일이 코드에서 일어나면, 이로 인해 버그가 아무도 모르게 코드 내로 유입되는 일이 너무도 많이 일어난다.

 

 

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

자신이 작성한 코드에 잘못된 것들이 꽂히면, 모든 것이 폭발할 수 있다. 시스템은 작동을 멈추고, 데이터베이스가 영구적으로 손상되거나, 중요한 데이터가 손실될 수 있다. 큰 문제가 일어나지는 않더라도 작동하지 않을 가능성이 크다. 자신이 작성한 코드가 호출된 데는 이유가 있는데, 그 코드가 잘못 사용된다면 중요한 일이 수행되지 않거나, 이상하게 동작하지만 눈에 띄지 않는다는 것을 의미할 수도 있다.

 

 

1.3.4 코드를 모듈화하라

코드를 외부에 의존하지 않고 실행할 수 있는 모듈로 나누는 것이 이로울 때가 많다. 이렇게 하면 변화하는 요구 사항에 더 쉽게 적응할 수 있는 코드를 작성하는 데 도움이 된다. 왜냐하면 한 가지 기능을 변경한다고 해서 다른 부분까지 변경할 필요가 없기 때문이다. 모듈화된 시스템은 일반적으로 이해하기 쉽고 추론하기 쉬운데, 기능이 관리 가능한 단위로 이루어지고 기능 단위 간 상호작용이 잘 정의되고 문서화되기 때문이다. 코드가 모듈화되어 작성되면 처음에 작동이 시작되고 그 후에도 계속해서 잘 작동할 가능성이 커진다. 왜냐하면 코드가 하는 일을 개발자들이 오해할 소지가 적기 때문이다.

 

 

1.3.5 코드를 재사용 가능하고 일반화할 수 있게 작성하라

  • 재사용성
    • 어떤 문제를 해결하기 위한 무언가가 여러 가지 다른 상황에서도 사용할 수 있음
    • 문제는 동일한데, 상황은 다름
  • 일반화성
    • 개념적으로는 유사하지만 서로 다른 미묘한 문제들을 해결할 수 있음
    • 문제가 다르지만 해결책은 같음

 

 

 

1.3.6 테스트가 용이한 코드를 작성하고 제대로 테스트하라

테스트는 두 가지 핵심 사항에 대한 주된 방어 수단이 될 때가 많다

  • 버그나 제대로 동작하지 않는 기능을 갖는 코드가 코드베이스에 병합되지 않도록 방지
  • 버그나 제대로 동작하지 않는 기능을 갖는 코드가 배포되지 않도록 막고 서비스 환경에서 실행되지 않도록 보장

 

따라서 테스트는 코드가 동작하고, 멈추지 않고 계속 잘 실행하도록 보장하기 위해 필수적인 부분이다. 테스트는 다음과 같은 이유에서 정말 중요하다.

  • 소프트웨어 시스템과 코드베이스는 너무 크고 복잡해 한 사람이 모든 세부 사항을 알 수 없고,
  • (아무리 똑똑한 개발자라 해도) 사람은 실수를 하는 존재다.

 

 

[ 1.4 고품질 코드 작성은 일정을 지연시키는가? ]

단기적으로는 고품질 코드를 작성하는 데 시간이 걸릴 수 있다는 것이다. 높은 품질의 코드를 작성하는 것은 보통 우리 머릿속에 떠오르는 것을 바로 코딩하는 것보다 조금 더 많은 생각과 노력이 필요하다. 하지만 일반적으로 고품질 코드를 작성하는 것이 개발 시간을 단축해준다.

코드 품질을 고려하지 않고 먼저 떠오르는 대로 코딩하면 처음에는 시간을 절약할 수 있다. 그러나 이런 코드는 머지않아 취약하고 복잡한 코드베이스로 귀결될 것이며, 점점 이해하기 어렵고 추론할 수 없는 코드가 된다. 새로운 기능을 추가하거나 버그를 수정하는 것이 점점 더 어려워지고 시간도 더 많이 걸리는데, 작동하지 않는 코드를 처리하고 재설계해야 하기 때문이다.

 

 

 

2. 추상화 계층


코드를 구성하는 방법은 코드 품질의 기본적인 측면 중 하나이며, 코드를 잘 구성한다는 것은 간결한 추상화 계층을 만드는 것으로 귀결될 때가 많다.

 

 

[ 2.1 널값 및 의사코드 규약]

  • 값이 제공되지 않거나 함수가 원하는 값을 반환할 수 없는 경우가 빈번하므로 “값이 없다”의 개념은 유용함
  • 값이 널일 수 있거나 혹은 널이면 안되는 경우가 명백한 것은 아니라서 문제가 발행함. 널 확인하는 것을 자주 잃어버리기 때문임

 

 

[ 2.2 왜 추상화 계층을 만드는가? ]

코드 작성은 복잡한 문제를 계속해서 더 작은 하위 문제로 세분화하는 작업이다.

어떤 문제를 하위 문제로 계속해서 나누어 내려가면서 추상화 계층을 만든다면, 같은 층위 내에서는 쉽게 이해할 수 있는 몇 개의 개념만을 다루기 때문에 개별 코드는 특별히 복잡해 보이지 않을 것이다. 소프트웨어 엔지니어로서 문제를 해결할 때 이것이 목표가 되어야 한다. 비록 문제가 엄청나게 복잡할지라도 하위 문제들을 식별하고 올바른 추상화 계층을 만듦으로써 그 복잡한 문제를 쉽게 다룰 수 있다.

 

 

 

[ 2.3 코드의 계층 ]

2.3.1 API 및 구현 세부 사항

  • 코드를 호출할 때 볼 수 있는 내용
    • 퍼블릭 클래스, 인터페이스 및 함수(메서드)
    • 이름, 입력 매개변수 및 반환 유형이 표현하고자 하는 개념
    • 코드 호출 시 코드를 올바르게 사용하기 위해 알아야 하는 추가 정보(ex 호출 순서)
  • 코드를 호출할 때 볼 수 없는 내용
    • 구현 세부 사항

 

2.3.2 함수

각 함수에 포함된 코드가 하나의 잘 써진 짧은 문장처럼 읽히면 이상적이다. 여러 가지 다른 개념을 한 번에 말하고 있다면 좋은 문장이라고 할 수 없다. 함수가 하는 일을 다음 중 하나로 제한하면 이해하기 쉽고 단순한 문장으로 표현되는 함수를 작성하기 위한 좋은 전략이 될 수 있다.

  • 단일 업무 수행
  • 잘 명명된 다른 함수를 호출해서 더 복잡한 동작 구성

 

함수를 작게 만들고 수행하는 작업을 명확하게 하면 코드의 가독성과 재사용성이 높아진다. 코드를 마구 작성하다 보면 너무 길어서 읽을 수 없는 함수가 되기 쉽다. 따라서 코드 작성을 일단 마치고 코드 검토를 요청하기 전에 자신이 작성한 코드를 비판적으로 다시 한번 살펴보는 것이 좋다. 함수를 한 문장으로 표현하기 어렵게 구현했다면 로직의 일부를 잘 명명된 헬퍼 함수로 분리하는 것을 고려해봐야 한다.

 

 

2.3.3 클래스

단일 클래스의 이상적인 크기에 대해 논의하고 다음과 같은 많은 이론과 경헙 법칙을 제시한다.

  • 줄수: “한 클래스는 코드 300줄을 넘지 않아야 한다”와 같은 가이드라인이 존재함
    • 300줄 이하의 클래스는 무조건 적절한 크기임을 뜻하는 것은 아님
    • 이것은 어떤 것이 잘못되었을지도 모른다는 경고의 역할만 할 뿐, 어떤 것이 옳다는 보장은 아님
  • 응집력: “한 클래스 내의 모든 요소들이 얼마나 잘 속해 있는지를 보여주는 척도”로, 좋은 클래스는 매우 응집력이 강함
    • 순차적 응집력: 한 요소의 출력이 다른 요소에 대한 입력으로 필요할 때 발생함, 원두를 갈기 전에는 커피를 추출할 수 없는 것과 같음
    • 기능적 응집력: 여러 요소들이 모여서 하나의 일을 성취하는 데 기여할 때 발생함, 케이크를 만들기 위해 반죽을 섞을 그릇, 나무 숟가락, 케이크 통이 필요한 것과 같음
  • 관심사의 분리: “시스템이 각각 별개의 문제(또는 관심사)를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙”
    • 게임 콘솔이 TV와 동일한 제품으로 함께 묶이지 않고, TV와 분리되는 방식과 같음

 

응집력과 관심사의 분리에 대해 생각할 때는 서로 관련된 여러 가지 사항을 하나의 사항으로 간주하는 것을 어느 수준에서 해야 유용할지 결정해야 한다. 이것은 매우 주관적일 수 있기 때문에 종종 보기보다 까다로울 수 있다.

너무 많은 일을 하는 거대한 클래스를 코드베이스에서 흔히 볼 수 있는데, 이렇게 하면 앞에서 설명했듯이 코드 품질의 저하로 이어질 때가 많다. 클래스 구조를 설계할 때 코드 품질의 네 가지 핵심 요소를 충족하는지 신중하게 생각하면 좋다. 시간이 지남에 따라 클래스가 조금씩 늘어나다가 너무 커질 수 있으므로 기존 클래스를 수정할 대나 새로운 클래스를 작성할 때 이러한 요소를 고려하는 것이 도움이 된다. 코드를 적절한 크기의 클래스로 세분화하는 것은 추상화 계층을 잘 만들기 위한 가장 효과적인 도구이기 때문에 이를 위한 시간과 노력을 들일 만한 가치가 충분히 있다.

 

2.3.4 인터페이스

구현 세부 사항이 계층 사이에 유출되지 않도록 하기 위해 사용할 수 있는 한 가지 접근법은 어떤 함수를 외부로 노출한 것인지를 인터페이스를 통해 결정하는 것이다. 그 다음 이 인터페이스에 정의된 대로 클래스가 해당 계층에 대한 코드를 구현한다. 이보다 위에 있는 계층은 인터페이스에 의존할 뿐 로직을 구현하는 구체적인 클래스에 의존하지 않는다.

하나의 추상화 계층에 대해 두 가지 이상의 다른 방식으로 구현을 하거나 다른 방식으로 구현을 하거나 향후 다르게 구현할 것으로 예상되는 경우 인터페이스를 정의하는 것이 좋다.

주어진 추상화 계층에 대해 한 가지 구현만 있고 향후에 다른 구현을 추가할 계획이 없더라도 여전히 인터페이스를 통해 추상화 계층을 표현해야 하는가는 여러분과 여러분의 팀이 결정할 사안이다. 몇몇 소프트웨어 공학 철학은 이 상황에서도 여전히 인터페이스를 사용할 것을 권고한다.

  • 장점
    • 퍼블릭 API를 매우 명확하게 보여준다
    • 한 가지 구현만 필요하다고 잘못 추측한 것일 수 있다
    • 테스트를 쉽게 할 수 있다
    • 같은 클래스로 두 가지 하위 문제를 해결할 수 있다
  • 단점
    • 더 많은 작업이 필요하다
    • 코드가 복잡해질 수 있다

 

필자의 개인적인 경험으로 볼 때 모든 클래스에 인터페이스를 붙이는 극단적인 입장의 코드는 종종 통제가 불가능하고, 불필요하게 복잡해지며, 이해와 수정이 어렵다. 인터페이스를 사용할 경우 그 장점이 확실한 상황에서는 인터페이스를 사용하되, 인터페이스만을 위한 인터페이스를 작성해서는 안된다. 그럼에도 깨끗하고 뚜렷한 추상화 계층을 만드는 데 집중하는 것은 여전히 중요하다. 인터페이스를 정의하지 않더라도 클래스에서 어떤 함수를 퍼블릭으로 노출할지 매우 신중하게 생각해야 하며 구현 세부 사항이 유출되지 않도록 해야 한다. 일반적으로 클래스를 작성하거나 수정할 때마다 나중에 필요한 경우 인터페이스를 붙이는 것이 어려워지지 않도록 코드를 작성해야 한다.

 

 

2.3.5 층이 너무 얇아질 때

코드를 별개의 계층으로 세분화하면 장점이 많지만 다음과 같은 추가 비용이 발생한다.

  • 클래스를 정의하거나 의존성을 새 파일로 임포트하려고 반복적으로 사용하는 코드로 인해 코드의 양이 늘어난다.
  • 로직의 이해를 위해 파일이나 클래스를 따라갈 때 더 많은 노력이 필요하다.
  • 인터페이스 뒤에 계층을 숨기게 되면 어떤 상황에서 어떤 구현이 사용되는지 파악하는 데 더 많은 노력이 필요하다. 이로 인해 로직을 이해하거나 디버깅하는 것이 더 어려워질 수 있다.

 

 

 

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


[ 3.1 자신의 코드와 다른 개발자의 코드 ]

다른 개발자들이 활발하게 코드를 변경하더라도 코드의 품질이 유지되려면 코드가 튼튼하고 사용하기 쉬워야 한다. 고품질 코드를 작성할 때 가장 중요한 고려 사항 중 하나는 다른 개발자가 변경하거나 코드와 상호작용할 때 발생할 수 있는 문제가 없는지, 또 발생한다면 그 문제를 어떻게 완화할 수 있을지를 이해하고 선제적으로 조치하는 것이다.

코드를 작성할 때 다음 세 가지를 고려하는 것이 유용하다.

  • 자신에게 명백하다고 해서 다른 사람에게도 명백한 것은 아니다.
    • 코드를 작성하기 시작하면 해결하려는 문제에 대해 생각하면서 몇 시간 혹은 며칠을 보내면서 자신의 로직에 너무 익숙해짐
    • 하지만 다른 개발자들은 그 문제를 이해하고 어떻게 해결할지에 대해 생각할 수 있는 시간을 아직 충분히 갖지 못한 상태임
    • 이것을 항상 고려하고 코드가 어떻게 사용되어야 하는지, 무엇을 하는지, 그리고 왜 그 일을 하고 있는지 설명하는 것이 유용함
    • 주석문을 많이 작상하라는 것이 아니고, 코드를 이해하기 쉽고 코드 자체로 설명이 되게 하는 것이 좋음
  • 다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.
    • 다른 개발자의 코드 변경으로 인해 자신의 코드베이스가 깨지는 문제를 해결하기 위해 컴파일이 중지되거나 테스트가 실패하도록 만들 수 있음
    • 코드에 문제가 생겼을 때 이 두 가지 중 하나가 일어나도록 하는 것이 고품질 코드 작성과 관련된 많은 고려 사항들이 궁극적으로 이루고자 하는 것임
  • 시간이 지남에 따라 자신의 코드를 기억하지 못한다.
    • 자신에게는 분명한데 다른 사람에게는 분명하지 않을 수 있다는 것, 혹은 다른 사람들이 무의식중에 자신의 코드를 작동하지 않게 만드는 것과 관련해 살펴본 모든 내용이 어느 순간 자신에게 적용됨
    • 배경지식이 거의 없거나 전혀 없는 사람에게도 자신의 코드가 이해하기 쉬어야 하고, 잘 작동하던 코드에 버그가 발생하는 것이 어려워야 함
    • 이렇게 하는 것은 다른 사람에게 호의를 베푸는 것이기도 하지만 미래의 자신에게도 유익한 일임

 

 

[ 3.2 여러분이 작성한 코드의 사용법을 다른 사람들은 어떻게 아는가? ]

여러분이 작성한 코드를 어떻게 사용해야 하는지 알아내기 위해 다른 개발자가 할 수 있는 일은 다음과 같다. 이 중 처음 세 가지만이 실제로 사용할 만하고, 그 중에서도 이름과 데이터 유형을 확인하는 것이 문서를 읽는 것보다 더 신뢰할 만하다.

  • 함수, 클래스, 열거형 등의 이름을 살펴본다.
  • 함수와 생성자의 매개변수 유형 또는 반환값의 유형과 같은 데이터 유형을 살펴본다.
  • 함수/클래스 수준의 문서나 주석문을 읽어본다.
  • 직접 와서 묻거나 채팅/이메일을 통해 문의한다.
  • 여러분이 작성한 함수나 클래스의 자세한 구현 코드를 읽는다.

 

추상화 계층을 만드는 데 있어 요점은 개발자가 한 번에 몇 가지 개념만 처리해야 하고, 그 문제가 어떻게 해결되었는지 정확히 알지 못하더라도 하위 문제에 대한 해결책을 사양할 수 있어야 한다는 것이다. 코드를 사용하는 방법을 알기 위해 개발자가 구현 세부 사항을 읽어야 한다면 이는 분명히 추상화 계층의 많은 이점을 부정하는 것이 된다.

 

 

[ 3.3 코드 계약 ]

계약에 의한 프로그래밍(programming by contract) 또는 계약에 의한 디자인(design by contract)이라는 용어를 접해 본 적이 있을 것이다. 이 원칙은 이전 절에서 논의한 개념들 중 일부를 공식화하는 원칙으로 다른 사람이 어떻게 코드를 사용할지, 그리고 코드가 무엇을 할 것으로 기대할 수 있는지에 대한 것이다. 이 철학에서는 서로 다른 코드 간의 상호작용을 마치 계약처럼 생각한다. 코드의 계약에 대한 용어를 다음과 같은 세 가지 범주로 나누면 유용하다.

  • 선결 조건(precondition): 코드를 호출하기 전에 사실이어야 하는 것, 예를 들어 시스템이 어떤 상태에 있어야 하는지, 코드에 어떤 입력을 공급해야 하는지와 같은 상황
  • 사후 조건(postcondition): 코드가 호출된 후에 사실어야 하는 것, 예를 들어 시스템이 새로운 상태에 놓인다든지 반환되는 값과 같은 상황
  • 불변 사항(invariant): 코드가 호출되기 전과 후에 시스템 상태를 비교해서 변경되지 않아야 하는 사항

 

여러분이 작성하는 코드는 어떤 종류의 계약을 맺는 것이라고 봐도 무방하다. 그 이유는 코드를 호출하는 사람에게 무언가를 설정하거나 입력(선결 조건)을 제공해야 할 요건을 부여하고, 호출 결과 일어날 일 혹은 반환될 값(사후 조건)에 대한 기대를 갖게 하기 때문이다.

 

 

3.3.1 계약의 세부 조항

  • 계약의 명확한 부분
    • 함수와 클래스 이름: 호출하는 쪽에서 이것을 모르면 코드를 사용할 수 없다.
    • 인자 유형: 호출하는 쪽에서 인자의 유형을 잘못 사용하면 코드는 컴파일조차 되지 않는다.
    • 반환 유형: 호출하는 쪽에서 함수의 반환 유형을 알아야 한다. 이 유형과 일치하지 않는 유형은 컴파일되지 않는다.
    • 체크 예외: 호출하는 코드가 이것을 처리하지 않으면 코드는 컴파일되지 않는다.
  • 세부 사항
    • 주석문과 문서: 실제 계약 세부조항처럼 꼼꼼하게 읽어봐야 하지만 실제로는 잘 읽지 않는다. 개발자는 이 사실을 실용적인 관점에서 봐야 한다.
    • 언체크 예외: 주석문에 예외가 나열되어 있다면 이것은 세부 조항이다. 어떤 때는 심지어 세부 조항에도 없을 수 있다.

 

코드 계약에서 조건을 명백하게 하는 것이 세부 조항을 사용하는 것보다 훨씬 낫다. 사람들은 세부 조항을 읽지 않는 경우가 매우 많으며, 심지어 읽더라도 그것을 대충 훑어보기 때문에 잘못 이해할 수 있다. 그리고 앞 부분에서 논의한 바와 같이 문서화는 업데이트가 제때 되지 않기 때문에 세부 조항이 항상 정확한 것도 아니다.

 

 

3.3.2 세부 조항에 너무 의존하지 말라

주석문과 문서의 형태로 된 세부 조항은 간과하고 넘어갈 때가 많기 때문에 다른 개발자들이 해당 코드를 사용할 때 모든 세부 조항을 다 알지 못할 가능성이 크다. 따라서 코드 계약을 전달할 때 세부 조항을 사용하는 것은 신뢰할 만한 방법이 아니다. 또한 시간이 흐르면서 업데이트가 안 될 가능성이 크기 때문에 문서화는 아주 이상적인 방법은 아니다.

다른 개발자가 코드를 올바르게 사용하기 위해 세부 조항에 의존하기보다 잘못된 일을 하는 것을 처음부터 불가능하게 만드는 것이 좋다. 코드 계약의 세부 항목에 있는 어떤 항목에 대해 발생 자체가 불가능하도록 명백한 항목으로 바꾸는 것이 가능한 경우가 있다. 코드가 오용되거나 잘못 설정되면 컴파일조차 되지 않도록 하는 것이 목표다.

 

 

[ 3.4 체크 및 어서션 ]

런타임 검사는 컴파일 타임 확인만큼 강력하지 않은데, 왜냐하면 코드 계약 위반의 발견이 코드를 실행하는 동안 발생하는 문제에 대한 테스트(또는 사용자)에 의존하기 때문이다. 이는 애당초 해당 계약 위반을 논리적으로 불가능하게 만드는 컴파일 타임 확인과는 대조적이다.

그럼에도 불구하고 컴파일러를 사용하여 계약을 강제할 수 있는 실질적인 방법이 없는 상황이 더러 있다. 이러한 경우 런타임 것마를 통해 계약을 확인하는 것이 아예 계약을 확인하지 않는 것보다 낫다.

 

 

3.4.1 체크

체크는 코드 계약이 준수되었는지 확인하기 위한추가적인 로직이며, 준수되지 않을 경우 체크는 실패를 유발하는 오류를 생성한다. 이 실패는 명백해서 놓치고 넘어가는 것이 불가능하다. 체크는 시행 중인 계약 조건에 따라 다음과 같은 범주로 구분된다.

  • 전제 조건 검사: 입력 인수가 올바르거나, 초기화가 수행되었거나, 일부 코드를 실행하기 전에 시스템이 유효한 상태인지 확인하는 경우
  • 사후 상태 검사: 반환값이 올바르거나 일부 코드를 실행한 후 시스템이 유효한 상태인지 확인하는 경우

 

경우에 따라 코드 계약에서 세부 조항을 피할 수 없으며, 이때는 계약이 준수되는지 확인하기 위해 체크를 추가하는 것이 좋다. 하지만 가능하다면 처음부터 세부 조항은 피하는 것이 바람직하다. 코드에 체크가 많이 있으면 세부 조항을 없애는 것에 대해 고려해봐야 한다는 신호일지도 모른다.

 

 

3.4.2 어서션

어서션은 코드 계약을 준수하도록 강제하기 위한 방법이라는 점에서 체크와 매우 유사하다. 조건이 위반되면 오류가 명백하게 보이거나 예외가 발생한다. 어서션과 체크 사이의 주요 차이점은 배포를 위해 빌드할 때 어서션은 보통 컴파일에서 제외된다는 점이며, 이는 코드가 코드가 실제 서비스 환경에서 사용될 때 실패를 명백하게 보여주지 않는다는 것을 의미힌다. 코드를 배포할 때 컴파일하지 않는 이유는 다음과 같다.

  • 성능 향상을 위해: 조건이 위반되는지 확인하려면 CPU 사이클이 필요함, 어서션이 많으면 성능이 저하될 수 있음
  • 코드 오류 발생률을 낮추기 위해: 이로 인해 버그가 눈에 띄지 않을 가능성은 증가하지만 버그 발생 가능성 방지보다 고가용성이 더 중요한 시스템이라면 배포 시에 제외하는 것이 절충이 될 수 있음

 

 

4. 오류


[ 4.1 복구 가능성 ]

4.1.1 복구 가능한 오류

일반적으로 시스템 외부의 무언가에 의해 야기되는 오류에 대해서는 대부분 시스템 전체가 표나지 않고 적절하게 처리하기 위해 노력해야 한다. 왜냐하면 이런 오류는 일어날 것이라고 적극적으로 예상해야 하는 오류이기 때문이다. 여기서 시스템 전체를 지칭한다는 점에 유의하기 바란다.

 

 

4.1.2 복구할 수 없는 오류

오류가 발생하고 시스템이 오류를 복구할 수 있는 합리적인 방법이 없을 때가 있다. 이러한 현상은 프로그래밍 오류 때문에 발생할 때가 많다.

오류를 복구할 수 있는 방법이 없다면, 유일하게 코드가 할 수 있는 합리적인 방법은 피해를 최소화하고 개발자가 문제를 발견하고 해결할 가능성을 최대화하는 것이다. 뒤에서 살펴볼 신속한 실패(failing fast) 와 요란한 실패(failing loudly) 라는 개념은 바로 이에 대한 것이다.

 

 

4.1.3 호출하는 쪽에서만 오류 복구 가능 여부를 알 때가 많다

오류 상황을 처리할 때는 다음과 같은 상황을 신중하게 고려해야 한다.

  • 오류로부터 복구하기를 호출하는 쪽에서 원하는가?
  • 만약 그렇다면 오류를 처리할 필요가 있다는 것을 호출하는 쪽에서는 어떻게 알 수 있을까?

 

보다 일반적으로 다음 중 하나라도 해당되는 경우, 함수에 제공된 값으로 인해 발생하는 오류는 호출하는 쪽에서 복구하고자 하는 것으로 간주해야 한다.

  • 함수가 어디서 호출될지 그리고 호출 시 제공되는 값이 어디서 올지 정확한 지식이 없다.
  • 코드가 미래에 재사용될 가능성이 아주 희박하다. 재사용이 된다면 코드가 어디에서 호출되고 값이 어디서 오는지에 대한 가정이 의미가 없어질 수 있음을 뜻한다.

 

 

4.1.4 호출하는 쪽에서 복구하고자 하는 오류에 대해 인지하도록 하라

함수의 작성자는 함수에서 오류가 발생할 수 있다는 가능성을 호출하는 쪽에서 확실하게 인지하도록 해야 한다. 그렇지 않으면 이 함수를 호출하는 개발자가 오류를 처리하는 코드를 작성하지 않은 상태에서 오류가 발생하는 경우 개발자의 예상과는 다른 결과를 초래할 수 있다. 이로 인해 사용자 버그를 마주하거나 중요한 비즈니스 로직에서 오류가 발생할 수 있다.

 

 

[ 4.2 견고성 vs 실패 ]

오류가 있더라도 처리하고 계속 진행하면 더 견고한 코드라고 볼 수 있지만, 오류가 감지되지 않고 이상한 일이 발생하기 시작한다는 의미도 될 수 있다. 이 절에서는 견고성보다는 실패가 많은 경우에 있어 최선인 이유와 적절한 수준의 로직에서 견고성도 가질 수 있는 방법에 대해 설명한다.

 

 

4.2.1 신속하게 실패하라

신속하게 실패하기(failing fast)는 가능한 한 문제의 실제 발생 지점으로부터 가까운 곳에서 오류를 나타내는 것이다. 복구할 수 있는 오류의 경우 호출하는 쪽에서 오류로부터 훌륭하고 안전하게 복구할 수 있는 기회를 최대한으로 제공하고, 복구할 수 없는 오류의 경우 개발자가 문제를 신속하게 파악하고 해결할 수 있는 기회를 최대한 제공한다.

이렇게 발생하자마자 바로 실패나 오류를 보여주지 않으면 문제가 발생할 때 디버그하기 어려울 뿐만 아니라, 코드가 제대로 작동하지 않거나 잠재적으로 문제를 일으킬 수 있다.

 

 

4.2.2 요란하게 실패하라

요란한 실패는 간단히 말하자면 오류가 발생하는데도 불구하고 아무도 모르는 상황을 막고자 하는 것이다. 이를 위한 가장 명백한(그리고 강압적인) 방법은 예외(또는 이와 유사한 것)를 발생해 프로그램이 중단되게 하는 것이다. 다른 방법은 오류 메시지를 기록하는 것인데 개발자가 얼마나 부지런하게 로그를 확인하는지, 혹은 로그에 방해되는 다른 메시지가 얼마나 있는지에 따라 오류 메시지가 무시될 수도 있다.

 

 

 

4.2.4 오류를 숨기지 않음

어떤 때는 실수를 숨기고 아무 일도 없었던 것처럼 동작하도록 코드를 작성하고 싶은 마음이 생길 수 있다. 이렇게 하면 코드가 훨씬 더 단순해지고 번거로운 오류 처리를 피할 수 있지만, 좋은 생각은 아니다. 오류를 숨기는 것은 복구할 수 있는 오류와 복구할 수 없는 오류 모두에 문제를 일으킨다.

  • 호출하는 쪽에서 복구하고자 할 수도 있는 오류를 숨기면, 호출하는 쪽에서 오류로부터 복구할 수 있는 기회를 없애는 것이다.
  • 복구할 수 없는 오류를 숨키면 프로그래밍 오류가 감춰진다.
  • 이 두 경우 모두 에러가 발생하면 일반적으로 호출하는 쪽에서 예측한 대로 코드가 실행되지 않음을 의미한다.

 

몇몇 하위 절에서는 오류가 발생했다는 사실을 숨길 수 있는 몇 가지 방법을 다룬다. 이러한 기술 중 일부는 다른 상황에서 유용하지만 오류 처리에 있어서는 일반적으로 모두 바람직하지 않다.

  • 기본값 반환
    • 오류가 발생했다는 사실을 숨기고, 호출하는 쪽에서 모든 것이 정상인 것처럼 계속 진행됨
    • 기본값으로 인해 오류가 나중에 이상한 방식으로 나타날 수 있으므로 신속한 실패와 요란한 실패의 원리를 위반함
  • 널 객체 패턴
    • 널 객체 패턴의 예는 빈 리스트부터 클래스까지 다양할 수 있음
    • 널 객체 패턴이 유용한 경우가 있지만, 오류 처리에 사용하는 것은 바람직하지 않음
  • 아무것도 하지 않음
    • 오류가 발생했다는 신호를 보내지 않는 것은, 호출부에서 의도대로 완료되었다고 가정하기 때문에 바람직하지 않음
    • 코드가 하는 일에 대해 개발자가 갖는 정신 모델과 실제 수행 사이의 불일치 가능성이 매우 높음

 

 

[ 4.3 오류 전달 방법 ]

이 작업을 수행하는 방법은 여러 가지가 있으며, 사용하는 언어가 지원하는 오류 처리 기능에 따라 가능한 방법이 달라진다. 오류를 알리는 방법은 크게 두 가지 종류로 나뉜다.

  • 명시적 방법
  • 암시적 방법

 

이 범주는 코드를 사용하는 개발자의 관점에서 오류 발생 가능성에 대해 말하는 것이다. 이것은 오류가 결국 요란하게 실패할지 아니면 조용히 실패할지에 대한 것이 아니다.

 

 

4.3.4 명시적 방법: 널값이 가능한 반환 유형

함수에서 널 값을 반환하는 것은 특정값을 계산하거나 얻는 것이 불가능함을 나타내기 위한 효과적이고 간단한 방법이다. 사용중인 언어가 널 안정성을 지원하는 경우 널 값이 반환될 수 있다는 것을 호출하는 쪽에서 강제적으로 인지하고, 그에 따라 처리할 수 밖에 없다. 따라서 널 안정성을 지원할 때, 널 값이 가능한 반환 유형을 사용하는 것은 오류를 전달하기 위한 명시적인 방법이다.

다만 오류가 발생한 이유에 대한 정보를 제공하지는 않으므로 널값이 의미하는 바를 설명하기 위해 주석문이나 문서를 추가해야 한다.

 

 

4.3.5 명시적 방법: 리절트 반환 유형

널값이나 옵셔널 타입을 반환할 때의 문제 중 하나는 오류 정보를 전달할 수 없다는 것이다. 호출자에게서 값을 얻을 수 없음을 알릴 뿐만 아니라 값을 얻을 수 없는 이유까지 알려주면 유용하다. 이러한 경우에는 리절트 유형을 사용하는 것이 적절할 수 있다.

자신만의 리절트 유형을 정의한다면, 이것이 제대로 사용될지 여부는 다른 개발자가 얼마나 익숙해지는가에 달려 있다. 만약 getValue()를 호출하기 전에 hasError()를 통해 오류를 확인하지 않는다면 무용지물이 된다.

언어가 리절트 유형을 지원하거나 혹은 다른 개발자들이 그 유형에 익숙하다고 가정하면, 리절트 유형을 반환 유형으로 사용하는 것은 오류가 발생할 수 있다는 점을 분명히 하는 것이 된다. 따라서 반환 유형을 사용하는 것은 오류를 알리는 명시적인 방법이다.

 

 

 

 

 

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

비검사 예외와 명시적 오류 전달 기법 중 어느 것을 사용해야 하는지에 대한 논쟁이 있다. 이 두 가지 측면 모두 타당한 주장과 반론이 있으며, 이 절에서 이에 관해 요약해보겠다.

그 전에 기억해야 할 점은 여러분과 여러분의 팀이 동의한 철학이 다른 어떤 주장보다도 중요하다는 점이다. 최악의 상황은 혼재되어 사용되는 상황이다.

 

 

4.5.1 비검사 예외를 사용해야 한다는 주장

  • 코드 구조 개선
    • 비검사 예외를 사용하면 오류가 높은 계층까지 거슬러 올라가고, 그 사이는 오류 처리를 할 필요가 없음
    • 오류 처리 로직이 코드 전체에 퍼지지 않고 별도로 몇 개의 계층에만 있음
  • 개발자들이 무엇을 할 것인지에 대해 실용적이어야 함
    • 개발자들이 너무 많은 명시적 오류 전달을 접하면 결국 잘못된 일을 하게 됨
    • 예외 처리 관련하여 작업의 양이 많아지고, 중간에 오류를 숨겨 버릴 수도 있음

 

 

4.5.2 명시적 기법을 사용해야 한다는 주장

  • 매끄러운 오류 처리
    • 비검사 예외를 사용하면 모든 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기가 어려움
    • 호출하는 쪽에 잠재적 오류를 강제적으로 인식하도록 하면 이러한 오류를 좀 더 매끄럽게 처리 할 가능성이 커짐
  • 실수로 오류를 무시할 수 있음
    • 비검사 예외가 사용되면 적극적인 의사 결정이 들어갈 여지는 줄어들고, 기본적으로 잘못된 일(처리하지 않는) 일어나기 쉬움, 이는 특정 오류가 발생할 수 있다는 사실을 완전히 알지 못하기 때문임
    • 검사 예외는 처리를 위해 적극적인 노력이 필요하고 눈에 띄는 위반 사항이므로, 코드 검수시 명확하게 드러나고 이런 잘못된 코드를 차단할 가능성이 커짐
  • 개발자들이 무엇을 할 것인지에 대해 실용적이어야 함
    • 어떤 코드가 어떤 예외를 발생시킬 것인지 확실하게 알지 못하고, 이로 인해 예외 처리가 어려울 수 있음

 

 

 

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


[ 5.1 서술형 명칭 사용 ]

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

 

 

5.1.2 주석문으로 서술적인 이름을 대체할 수 없다

  • 코드가 훨씬 더 복잡해 보이며, 작성자와 다른 개발자는 코드뿐만 아니라 주석문과 문서도 유지보수 해야 함
  • 코드를 이해하기 위해 계속해서 위/아래로 스크롤해서 찾아가야 함
  • 클래스를 사용할 때, 클래스 내부를 보지 않고 무엇을 하는지와 무엇이 반환되는지 알기 어려움

 

 

[ 5.2 주석문의 적절한 사용 ]

코드 내에 주석문이나 문서화는 다음과 같은 다양한 목적을 수행할 수 있다.

  • 코드가 무엇을 하는지 설명
  • 코드가 왜 그 일을 하는지 설명
  • 사용 지침 등 기타 정보 제공

 

서술적인 이름으로 잘 작성된 코드는 그 자체로 줄 단위에서 무엇을 하는지 설명한다. 코드의 기능을 설명하기 위해 낮은 츠위의 주석문을 많이 추가해야 한다면, 이것은 코드 자체의 가독성이 떨어진다는 신호다. 반면에 코드가 왜 그 일을 하는지에 대한 이유나 배경을 설명하는 주석문은 유용할 때가 많은데, 코드만으로는 이를 명확히 할 수 없기 때문이다.

  • 중복된 주석문은 유해할 수 있다
    • 개발자는 주석 까지 유지보수해야 함
    • 코드를 지저분하게 만듬, 100줄의 코드를 읽으려면 100줄의 주석까지 읽어야 할 수 있음
  • 주석문으로 가독성 높은 코드를 대체할 수 없다
    • 코드 가독성이 높지 않아서 주석문이 필요하지만, 더 나은 접근법은 가독성 높은 코드를 작성하는 것
    • 유지 보수 비용을 줄이고, 주석문의 내용이 업데이트되지 않거나 잘못된 가능성을 없애줌
  • 주석문은 코드의 이유를 설명하는 데 유용하다
    • 제품 또는 비즈니스 의사 결정
    • 이상하고 명확하지 않은 버그에 대한 해결책
    • 의존하는 코드의 예상을 벗어나는 동작에 대처
  • 주석문은 유용한 상위 수준의 요약 정보를 제공할 수 있다
    • 주석과 문서화는 코드만으로는 전달할 수 없는 세부 사항을 설명하거나 코드가 큰 단위에서 하는 일을 요약하는 데 유용힘
    • 물론 이런 주석문과 문서도 유지 보수가 필요하고, 내용이 업데이트 되지 않으면 코드와 맞지 않게 될 수 있음

 

 

[ 5.3 코드 줄 수를 고정하지 말라 ]

일반적으로 코드베이스의 코드 줄 수는 적을수록 좋다. 코드는 일반적으로 어느 정도의 지속적인 유지보수를 필요로 하며 코드의 줄이 많다는 것은 코드가 지나치게 복잡하거나, 기존 코드를 재사용하지 않고 있다는 신호일 수 있다. 또한, 코드 줄이 많으면 읽어야 할 코드의 양이 늘어나기 때문에 개발자의 인지 부하가 증가할 수 있다.

그러나 코드 줄 수는 우리가 실제로 신경 쓰는 것들을 간접적으로 측정해줄 뿐이다. 우리가 정말로 신경 쓰는 것은 코드에 대해 다음과 같은 사항들을 확실하게 하는 것이다.

  • 이해하기 쉽다
  • 오해하기 어렵다
  • 실수로 작동이 안 되게 만들기가 어렵다

 

이를 위해 다음과 같은 내용들을 고려할 수 있다.

  • 간결하지만 이해하기 어려운 코드는 피하라(비트 연산 등)
  • 해결책: 더 많은 줄이 필요하더라도 가독성 높은 코드를 작성하라

 

 

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

  • 일관적이지 않은 코딩 스타일은 혼동을 일으킬 수 있다
  • 해결책: 스타일 가이드를 채택하고 따르라
    • 언어의 특정 기능 사용
    • 코드 들여쓰기
    • 패키지 및 디렉터리 구조화
    • 코드 문서화 방법

 

 

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

  • 새 기능은 코드를 개선할 수 있다
  • 불분명한 기능은 혼동을 일으킬 수 있다
    • 개선 사항이 적거나 다른 개발자가 그 기능에 익숙하지 않다면 차라리 사용하지 않는 것이 좋을 수 있음
  • 작업에 가장 적합한 도구를 사용하라
  • 불분명한 기능은 혼동을 일으킬 수 있다

 

 

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


궁극적으로 개발자는 코드를 사용하는 방법에 대한 정신 모델을 구축한다. 이 정신 모델은 코드 계약에서 발견한 것, 사전 지식, 적용할 수 있다고 생각하는 공통 패러다임에 근거해서 만들어진다. 코드가 실제로 하는 일이 이 정신 모델과 일치하지 않는다면, 예측과 벗어나는 기분 나쁜 일이 일어날 가능성이 크다.

 

 

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

  • 빈 컬렉션을 반환하면 코드가 개선될 수 있다
    • 빈 값이나 컬렉션을 반환하면 호출하는 쪽에서 널값인지 확인할 필요가 없음
    • 이로 인해 호출하는 쪽의 코드는 간단해지고 코드가 예측을 벗어나는 작동을 할 가능성이 매우 낮음
  • 빈 문자열을 반환하는 것도 때로는 문제가 될 수 있다
    • 문자열이 단지 문자를 모아 놓은 것에 불과하고, 그 외의 별 다른 의미가 없다면 일반적으로 빈 문자열 반환은 문제가 없음
    • 문자열이 ID로 사용되는 것처럼 특정한 의미를 갖는 경우 실행할 논리에 영향을 미칠 수 있음
    • 그러므로 문자열이 없을 수 있음을 호출하는 쪽에서 명시적으로 인식하도록 하는 것이 중요함
  • 더 복잡한 널 객체는 예측을 벗어날 수 있다
  • 널 객체 구현은 예상을 벗어나는 동작을 유발할 수 있다

 

 

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

부수 효과가 예상되고 코드를 호출한 쪽에서 그것을 원한다면 괜찮지만, 부수 효과가 예상되지 않을 경우 놀라움을 유발하고 버그로 이어질 수 있다.

  • 분명하고 의도적인 부수 효과는 괜찮다
  • 예기치 않은 부수 효과는 문제가 될 수 있다
  • 해결책: 부수 효과를 피하거나 그 사실을 분명하게 해라

 

정보를 얻는 함수는 일반적으로 부수 효과를 일으키지 않기 때문에 개발자의 자연스러운 정신 모델에서는 그러한 함수들이 부수 효과를 일으키지 않을 것이라고 가정한다. 따라서 어떤 함수가 부수 효과를 일으킨다면, 그 함수를 호출하는 쪽에서 이 사실에 대해 명백하게 알 수 있도록 하는 책임이 함수의 작성자에게 있다.

 

 

 

[ 6.6 미래를 대비한 열거형 처리 ]

열거형에 대해서 개발자들 사이에 논쟁이 있다. 일부에서는 타입 안정성을 제공하고 함수나 시스템에 유효하지 않은 입력을 방지할 수 있는 훌륭하고 간단한 방법이라고 주장한다. 다른 사람들은 열거형의 특정 값을 처리하기 위한 논리가 코드 전반에 퍼져 있게 되기 때문에 간결한 추상화 계층을 막는다고 주장한다. 후자에 속한 개발자들은 종종 다형성이 더 나은 방식이라고 주장한다. 어떤 값이 특정 클래스에서만 사용된다면 그 클래스 내에 해당 값에 대한 정보와 동작을 캡슐화 한 다음, 이 클래스들이 공통 인터페이스를 구현하도록 하자는 것이 이 주장의 요지다.

열거형에 대한 개인적인 의견과가 상관없이, 코드에서 열거형을 접하게 될 가능성이 있고 어느 시점에서는 열거형을 다뤄야 할 가능성도 있다. 그 이유는 다음과 같다.

  • 다른 사람의 코드의 결과를 사용해야 하며 어떤 이유로든 그들이 열거형을 즐겨 사용할 수 있다.
  • 다른 시스템에서 제공하는 결과를 사용하고 있을 때 열거형은 종종 데이터 형식에서 유일하게 실용적인 옵션일 수 있다.

 

열거형을 처리해야 하는 경우 나중에 열거형에 더 많은 값이 추가될 수 있다는 점을 기억하는 것이 중요하다. 이것을 무시하고 코드를 작성하면, 자기 자신 혹은 다른 개발자들의 예측을 벗어나는 좋지 않은 결과를 초래할 수 있다.

  • 미래에 추가될 수 있는 열것값을 암묵적으로 처리하는 것은 문제가 될 수 있다.
  • 해결책: 모든 경우를 처리하는 스위치 문을 사용하라
  • 디폴트 케이스를 주의하라
  • 주의 사항: 다른 프로젝트의 열거형에 의존하는 경우

 

 

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

  • 어떤 개발자들은 테스트에 대해 그다지 부지런하지 않을 수도 있다.
    • 자신의 가정이 틀렸다는 것을 드러내기 위한 충분한 시나리오나 코너 케이스를 테스트하지 않음
    • 특정 시나리오 혹은 매우 큰 입력에서만 문제가 드러나는 경우 이런 상황이 가능함
  • 테스트가 항상 실제 상황을 정확하게 시뮬레이션하는 것은 아니다.
    • 목 객체를 통해 테스트하는 경우 목 객체가 어떻게 행동할 것인지 자신이 생각하는 바대로 프로그래밍함
    • 실제 코드가 개발자의 가정과 예측을 벗어나는 방식으로 동작하지만 개발자가 이를 깨닫지 못한다면, 목 객체 자체를 올바르게 프로그래밍하지 못함
    • 어떤 것들은 테스트하기 매우 어려움. 멀티스레딩과 관련된 버그는 어느 정도 큰 규모에서 실행될 때만 나타날 수 있어서 테스트하기 어렵기로 악명 높음

 

 

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


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

가변 객체의 문제점들은 다음과 같다.

  • 설정 함수를 갖는 가변 클래스에서 잘못된 설정이 쉽게 이루어지고 이로 인해 잘못된 상태가 됨
  • 입력 매개변수를 변경하는 함수가 예상을 벗어나는 동작을 초래하게 됨
  • 가변 객체는 추론하기 어려움
  • 가변 객체는 다중 스레드에서 문제가 발생할 수 있음

 

가변 클래스의 사용은 주의해야 한다.

  • 가변 클래스는 오용하기 쉽다
  • 해결책: 객체를 생성할 때만 값을 할당하라
  • 해결책: 불변성에 대한 디자인 패턴을 사용하라(빌더 패턴, 쓰기 시 복사 패턴)

 

 

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

  • 깊은 가변성은 오용을 초래할 수 있다(동일한 참조를 가짐)
  • 해결책: 방어적으로 복사하라
  • 해결책: 불변적 자료구조를 사용하라

 

 

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

정수나 리스트와 같은 유형으로 표현이 “가능”하다고 해서 그것이 반드시 “좋은” 방법은 아니다. 설명이 부족하고 허용하는 범위가 넓을수록 코드 오용은 쉬워진다.

  • 지나치게 일반적인 데이터 유형은 오용될 수 있다
  • 페어 유형은 오용하기 쉽다
  • 해결책: 전용 타입 사용

 

 

[ 7.4 시간 처리 ]

시간은 단순한 것처럼 보일지 모르지만, 실제로 시간을 나타내는 것은 다음과 같은 점에서 상당히 까다롭다.

  • 어떤 때는 절대적인 시간을 지칭하지만 또 다른 때는 상대적인 시간으로 표현함
  • 시간의 양을 언급하는 경우도 있음, Ex) 30분 대기
  • 표준 시간대, 일광 절약 시간, 윤년, 심지어 윤초와 같은 개념도 있어서 복잡함

 

이번 절에서는 시간에 기초한 개념을 다룰 때 적절한 데이터 유형과 언어 구성 요소를 사용하여 혼동과 오남용을 방지할 수 있는 방법을 논의한다.

  • 정수로 시간을 나타내는 것은 문제가 될 수 있다
    • 한순간의 시간인가, 아니면 시간의 양인가?
    • 일치하지 않는 단위
    • 시간대 처리 오류
  • 해결책: 적절한 자료구조를 사용하라
    • 양으로서의 시간과 순간으로서의 시간의 구분
    • 더 이상 단위에 대한 혼동이 없다
    • 시간대 처리 개선

 

 

 

8. 코드를 모듈화하라


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

  • 하드 코드화된 의존성은 문제가 될 수 있다
  • 해결책: 의존성 주입을 사용하라
  • 의존성 주입을 염두에 두고 코드를 설계하라

 

 

[ 8.3 클래스 상속을 주의하라 ]

  • 클래스 상속은 문제가 될 수 있다
    • 상속은 추상화 계층에 방해가 될 수 있다
    • 상속은 적응성 높은 코드의 작성을 어렵게 만들 수 있다
  • 해결책: 구성을 사용하라
    • 더 간결한 추상화 계층
    • 적응성이 높은 코드
  • 진정한 is-a 관계여도 상속은 여전히 문제가 될 수 있음
    • 취약한 베이스 클래스 문제
    • 다이아몬드 문제
    • 문제가 있는 계층 구조(다중 상속)

 

 

 

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


[ 9.1 가정을 주의하라 ]

===== 섣부른 최적화 =====

코드 최적화는 일반적으로 비용이 든다. 즉, 최적화된 해결책을 구현하는 데 더 많은 시간과 노력이 필요하며 그 결과 코드는 종종 가독성이 떨어지고, 유지 관리가 더 어려워지며, 가정을 하게 되면 견고함이 떨어질 가능성이 있다. 게다가 최적화는 보통 프로그램 내에서 수천 번 혹은 수백만 번 실행되는 코드 부분에 대해 이루어질 때 상당한 이점이 있다.

따라서 대부분의 경우에는 큰 효과 없는 코드 최적화를 하느라고 애쓰기보다는 코드를 읽을 수 있고, 유지보수 가능하며, 견고하게 만드는 데 집중하는 것이 좋다. 코드의 어떤 부분이 여러 번 실행되고, 그 부분을 최적화하는 것이 성능 향상에 큰 효과를 볼 수 있다는 점이 명백해질 때 최적화 작업을 해도 무방하다.

 

 

 

10. 단위 테스트의 원칙


단위 테스트(unit test)에 대해 정확한 정의를 내리는 것이 유용할테지만, 안타깝게도 정확하게 내려진 정의가 없다. 단위 테스트는 상대적으로 격리된 방식으로 코드의 구별되는 단위를 테스트하는 것에 관한 것이다.

단위 테스트를 구성하고 있는 것이 정확히 무엇인지, 그리고 정확한 정의가 없음에도 일부러 정의를 고안해 내고 자신이 작성하는 테스트가 그 정의에 정확히 부합하는지에 대해 너무 집착하지 않는 것이 좋다. 궁극적으로 중요한 것은 코드를 잘 테스트하고 이 작업을 유지보수할 수 있는 방법으로 수행하는 점이다.

 

 

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

10.2.1 훼손의 정확한 감지

코드가 훼손되면 테스트가 실패한다. 그리고 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.

  • 코드에 대한 초기 신뢰를 준다.
    • 새로운 코드나 변경 사항과 함께 철저한 테스트 코드를 작성하면 코드가 병합되기 전 실수를 발견하고 수정할 수 있다.
  • 미래의 훼손을 막아준다.
    • 어느 시점에 다른 개발자가 코드를 변경하는 과정에서 실수로 코드를 훼손할 가능성이 크다.
    • 코드 변경으로 인해 잘 돌아가던 기능이 작동하지 않는 회귀를 탐지할 목적으로 회귀 테스트를 실행할 수 있다.

 

정확성의 또 다른 측면을 고려하는 것도 중요하다. 테스트 대상 코드가 실제로 훼손된 경우에만 테스트가 실패해야 한다.

테스트 대상 코드가 정상임에도 불구하고 때로는 통과하고 때로는 실패하는 테스트를 플래키(flakey)라고 한다. 이것은 보통 무작위성, 타이밍 기반 경쟁 상태(race condition), 외부 시스템에 의존하는 등의 비결정적(indeterministic) 동작에 기인하다.플래키 테스트의 가장 분명한 단점은 개발자들이 결국에는 아무것도 아닌 것으로 판명 날 실패의 원인을 찾느라 시간을 낭비한다는 점이다. 하지만 플래키 테스트는 언뜻 보기보다 훨씬 더 위험하다. 만약 테스트가 계속해서 실패하면서 코드가 훼손됐다고 잘못된 경고를 보인다면, 이내 그 경고를 무시하게 될 것이다. 정말 짜증나면 아예 테스트를 비활성화할 수도 있다. 더 이상 아무도 테스트 실패에 주의를 기울이지 않는다면 테스트가 없는 상황과 다를 바 없다.

 

 

10.2.2 세부 구현 사항에 독립적

세부 구현 사항을 변경하더라도 테스트 코드는 변경하지 않는 것이 이상적이다.

  • 기능적 변화
    • 코드가 외부로 보이는 동작을 수정함
    • 코드를 사용하는 모든 사람에게 영향을 미치므로 코드를 호출하는 쪽을 신중히 고려해야 함
    • 기능적인 변경은 코드의 동작을 수정하기 때문에 테스트도 수정해야 할 것으로 기대하고 예상함
  • 리팩터링
    • 코드의 구조적 변화를 의미하며, 리팩터링이 올바르게 수행되더라도 코드의 외부에서 보이는 동작이 변경되면 안됨
    • 코드를 사용하는 사람에게 영향을 미치지 않아야 함

 

코드는 자주 리팩터링된다. 테스트가 구현 세부 정보에 의존하지 않으면 코드 리팩터링에 실수가 있었는지 확인해주는 테스트 결과를 신뢰할 수 있다.

 

 

10.2.3 잘 설명되는 실패

코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.

테스트 실패가 잘 설명되도록 하는 좋은 방법 중 하나는 하나의 테스트 케이스는 한 가지 사항만 검사하고 각 테스트 케이스에 대해 서술적인 이름을 사용하는 것이다. 이렇게 하면 한 번에 모든 것을 테스트하려고 하는 하나의 큰 테스트 케이스보다 각각의 특정 동작을 확인하기 위한 작은 테스트 케이스가 많이 만들어진다. 테스트가 실패할 때 실패한 케이스의 이름을 확인하면 어떤 동작이 작동하지 않는지 정확하게 알 수 있다.

 

 

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

다른 개발자들이 테스트 코드가 정확히 무엇을 테스트하기 위한 것이고 테스트가 어떻게 수행되는지 이해할 수 있어야 한다.

개발자가 자신이 변경한 사항이 원하는 동작에만 영향을 미친다는 확신을 가지려면 테스트의 어느 부분에 영향을 미치고 있는지, 테스트 코드에 대한 수정이 필요한지 여부를 알 수 있어야 한다. 이를 위해서는 서로 다른 테스트 케이스가 무엇을 테스트하는지 그리고 어떻게 테스트하는지 이해하고 있어야 한다.

테스트 코드를 이해하기 쉽게 만들기 위해 노력해야 하는 또 다른 이유는 일부 개발자들이 테스트를 코드에 대한 일종의 사용 설명서로 사용하기 때문이다. 특정 코드를 어떻게 사용하는지, 혹은 어떤 기능을 제공하는지 궁금하다면 단위 테스트를 통해 알아보는 것도 좋은 방법이다.

 

 

10.2.5 쉽고 빠르게 실행

개발자는 일상 작업 중에 단위 테스트를 자주 실행한다. 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.

단위 테스트를 실행하는 데 한 시간이 걸린다면 코드 변경 병합 요청이 작거나 사소한 것과 상관없이 모든 개발자의 속도가 느려진다. 개발자는 개발하는 동안 단위 테스트를 수없이 많이 실행해야 하기 때문에 느린 단위 테스트는 개발자의 작업 속도를 느리게 만든다.

테스트를 빠르고 쉽게 유지해야 하는 도 다른 이유는 개발자가 실제로 테스트를 할 수 있는 기회를 극대화하기 위함이다. 테스트가 느리면 테스트가 힘든 작업이 되고, 테스트가 힘들면 하고 싶지 않은 마음이 든다. 테스트를 가능한 쉽고 빠르게 실행할 수 있으면 개발자는 더 효율적으로 작업할 수 있고, 테스트 역시 더 광범위하고 철저해진다.

 

 

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

“퍼블릭 API만을 이용한 테스트” 원칙을 따르면 테스트와 구현 세부 정보를 결합하지 않고, 호출하는 쪽에서 실제로 신경 쓰는 사항만 테스트하는 데 집중할 수 있다.

가능하면 퍼블릭 API를 사용하여 코드의 동작을 테스트해야 한다. 이는 순전히 퍼블릭 함수의 매개 변수, 반환 값, 오류 전달을 통해 발생하는 동작만 테스트해야 한다는 의미다. 그러나 코드의 퍼블릭 API를 어떻게 정의하느냐에 따라 퍼블릭 API만으로는 모든 동작을 테스트할 수 없는 경우가 있다. 다양한 의존성을 설정하거나 특정 부수 효과가 발생했는지 여부를 확인하는 것이 이에 해당한다.

궁극적으로 중요한 것은 코드의 모든 중요한 동작을 제대로 테스트하는 것이고, 퍼블릭 API라고 생각하는 것만으로는 이것을 할 수 없는 경우가 있다. 그러나 테스트를 구현 세부 사항에 최대한 독립적으로 수행하도록 주의를 기울여야 하므로 다른 대안이 없는 경우에만 퍼블릭 API를 벗어나 테스트해야 한다.

 

 

[ 10.4 테스트 더블 ]

의존성을 실제로 사용하는 것에 대한 대안으로 테스트 더블(test double)이 있다. 테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.

 

 

[ 10.4.1 테스트 더블을 사용해야 하는 이유 ]

  • 테스트 단순화
    • 일부 의존성은 설정하기 까다롭고 힘듬(많은 매개변수 or 하위 의존성)
    • 또한 많은 설정이 들어가는 테스트는 복잡하고 구현 세부 사항과 밀접하게 결합될 수 있음
    • 의존성을 실제로 사용하는 대신 테스트 더블을 사용하면 작업이 단순해짐
    • 또한 테스트를 더 빠르게 실행할 수도 있음
  • 테스트로부터 외부 세계 보호
    • 일부 의존성은 실제로 부수 효과를 발생시킴
    • 실제 서버에 요청을 전송하거나 실제 디비에 값을 저장하면 중요한 프로세스에 나쁜 결과를 초래할 수 있음
    • 테스트 더블을 사용하면 외부 세계에 있는 시스템을 테스트의 동작으로부터 보호할 수 있음
  • 외부로부터 테스트 보호
    • 외부 세계는 비결정적일 수 있음, 다른 시스템이 DB에 쓴 값은 시간이 지남에 따라 변경될 수 있음
    • 이로 인해 테스트를 신뢰하기 어려울 수 있음
    • 테스트 더블은 항상 동일하게 결정적 방식으로 작동하도록 설정할 수 있음

 

 

10.4.2 목

목은 클래스나 인터페이스를 시뮬레이션 하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다. 테스트가 의존성을 통해 제공되는 함수를 호출하는지 검증하기 위해 목을 사용할 수 있다. 따라서 목은 테스트 대상 코드에서 부수 효과를 일으키는 의존성을 시뮬레이션하는 데 가장 유용하다.

 

 

10.4.3 스텁

스텁은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션한다. 이를 통해 테스트 대상 코드는 특정 멤버 함수를 호출하고 특정 값을 반환하도록 의존성을 시뮬레이션할 수 있다. 그러므로 스텁은 테스트 대상 코드가 의존하는 코드로부터 어떤 값을 받아야 하는 경우 그 의존성을 시뮬레이션하는데 유용하다.

 

 

10.4.4 목과 스텁은 문제가 될 수 있다

  • 목과 스텁은 실제적이지 않은 테스트를 만들 수 있다
    • 목 객체를 만들거나 스텁할 때 테스트 코드를 작성하는 개발자는 목이나 스텁이 어떻게 동작할지 결정해야 함
    • 클래스나 함수가 실제와 다르게 동작하도록 하는 것은 아주 위험함
    • 그러면 테스트는 통과하고 모든 것이 잘 작동한다고 착각하지만 코드가 실제로 실행되면 부정확하게 동작하거나 버그가 발생할 수 있음
  • 목과 스텁을 사용하면 테스트가 구현 세부 정보에 유착될 수 있다
    • 실제 호출이 일어났는지 확인하는 목을 사용함
    • 따라서 목과 스텁은 최소한으로 사용하는 것이 최선임

 

 

10.4.5 페이크

페이크는 클래스 또는 인터페이스의 대체 구현체로 테스트에서 안전하게 사용할 수 있다.

페이크의 요점은 코드 계약이 실제 의존성과 동일하기 때문에 실제 클래스가 특정 입력을 받아들이지 않는다면 페이크도 마찬가지라는 것이다. 따라서 실제 의존성에 대한 코드를 유지보수 하는 팀이 일반적으로 페이크 코드도 유지보수해야 하는데, 실제 의존성에 대한 코드 계약이 변경되면 페이크의 고크 계약도 동일하게 변경되어야 하기 때문이다.

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

 

 

10.4.6 목에 대한 의견

  • mockist: 실제 의존성 사용을 피하고 목을 사용해야 함
    • 단위 테스트가 더욱 격리됨
    • 테스트 코드 작성이 더욱 쉬워짐
  • classicist: 목이나 스텁은 최소한으로 사용되고, 실제 의존성을 사용하는 것을 최우선으로 해야 함. 실제 의존성 사용이 불가능할 때 페이크를 선호하고, 불가능하면 취후의 수단으로 사용해야 함
    • 목은 코드가 특정 코드를 호출 하는지만 확인할 뿐 실제 호출이 유효한지는 검증하지 않음, 많은 수의 목이나 스텁을 사용하면 코드에 문제가 있어도 테스트는 통과할 수 있음
    • 구현 세부 사항에 대해 더 독립적인 테스트를 할 수 있고, 여기서는 최종 결과에만 중점을 둠. 테스트 대상 코드의 동작이 변경되었을 때에만 실패하며, 구현 세부 사항이 변경될 때는 실패하지 않음

 

필자는 목을 사용한 것을 후회하는데, 왜냐하면 동작을 제대로 테스트하지 않았고 코드의 리팩터링을 매우 어렵게 만들었기 때문이다.

 

 

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

  • 테스트 주도 개발
    • 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하는 것을 지지함
    • 실제 코드는 테스트만 통과하도록 최소한으로 작성하고 이후에 구조를 개선하고 중복을 없애기 위해 리팩터링을 함
  • 행동 주도 개발
    • 이 철학의 핵심은 사용자, 고객, 비즈니스의 관점에서 소프트웨어가 보여야 할 행동(또는 기능)을 식별하는 데 집중함
    • 테스트는 소프트웨어 자체의 속성보다는 이러한 원하는 동작을 반영해야 함
  • 인수 테스트 주도 개발
    • 고객의 관점에서 소프트웨어가 보여줘야 하는 동작(또는 기능)을 식별하고 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 인수 테스트를 만드는 것을 수반함
    • TDD와 마찬가지로 실제 코드를 구현하기 전에 이러한 테스트를 생성해야 함
    • 이론적으로 합격 테스트가 모두 통과하면 소프트웨어는 완전한 것이며 고객이 수락할 준비가 된 것임

 

 

11. 단위 테스트의 실제


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

  • 함수당 하나의 테스트 케이스만 있으면 적절하지 않을 때가 많다
  • 해결책: 각 동작을 테스트하는 데 집중하라
    • 모든 동작이 테스트되었는지 거듭 확인하라
    • 오류 시나리오를 잊지 말라

 

 

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

  • 프라이빗 함수를 테스트하는 것은 바람직하지 않을 때가 많다
  • 해결책: 퍼블릭 API를 통해 테스트하라
  • 해결책: 코드를 더 작은 단위로 분할하라

 

 

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

  • 여러 동작을 한꺼번에 테스트하면 테스트가 제대로 안 될 수 있다
  • 해결책: 각 동작은 자체 테스트 케이스에서 테스트하라
  • 매개변수를 사용한 테스트

 

 

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

  • 부적합한 확인자는 테스트 실패를 잘 설명하지 못할 수 있다
  • 해결책: 적절한 확인자를 사용하라

 

 

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

  • 하드 코딩된 의존성은 테스트를 불가능하게 할 수 있다
  • 해결책: 의존성 주입을 사용하라

 

 

[ 11.7 테스트에 대한 몇 가지 결론 ]

  • 통합 테스트(integration test)
    • 한 시스템은 일반적으로 여러 구성 요소, 모듈, 하위 시스템으로 구성됨
    • 이러한 구성 요소와 하위 시스템을 서로 연결하는 프로세스를 통합(integration)이라고 함
    • 통합 테스트는 이러한 통합이 제대로 작동하는지 확인하기 위한 테스트임
  • 종단 간 테스트(end-to-end test)
    • 이 테스트는 처음부터 끝까지 전체 소프트웨어 시스템을 통과하는 여정을 테스트 함
    • 테스트하려는 소프트웨어가 온라인 쇼핑몰이라면, 웹 브라우저를 자동으로 구동하고 사용자가 구매를 완료하는 과정까지 거치면서 확인하는 것임
  • 회귀 테스트(regression test)
    • 소프트웨어의 동작이나 기능이 바람직하지 않은 방식으로 변경되지 않았는지 확인하기 위해 정기적으로 수행하는 테스트
    • 단위 테스트는 일반적으로 회귀 테스트에서 중요한 부분이지만 통합 테스트와 같은 다른 수준의 테스트도 포함될 수 있음
  • 골든 테스트(golden test) 또는 특성화 테스트(characterization test)
    • 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샷으로 저장한 것을 기반으로 함, 테스트 수행 후 코드가 생성한 출력이 다르면 테스트는 실패함
    • 이 테스트는 아무것도 변경되지 않았음을 확인하는 데 유용하지만 테스트가 실패한 경우 실패 원인을 파악하기 어려울 수 있음
    • 또한 어떤 경우에는 믿을 수 없을 정도로 취약하고 신뢰하기 어려움
  • 퍼즈 테스트(fuzz test)
    • 많은 무작위 값이나 “흥미로운” 값으로 코드를 호출하고 그들 중 어느 것도 코드의 동작을 멈추지 않는지 점검함

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함