GoodCode,BadCode

좋은 코드, 나쁜 코드(프로그래머의 코드 품질 개선법)
톰 롱 지음 | 차건회 옮김

📖 책을 고른 이유(동기)

2023년 아카데미 컨퍼런스 1회차 선정 책이다.

각 장마다 논의 사항이 있는 이유는 실제로 해당 내용으로 2주 간격으로 토론을 했기 때문에 논의 사항이 적혀있다.

읽으면서 많이 도움도 되고 눈을 뜨게 해준 좋은 책이며 다른 사람들에게도 추천해주고 싶다.

정리한 내용은 아래에 있지만 내용정리와 내 의견을 위주로 달아서 조금 어지러울 수 있다.

머리말

논의 사항

“협업은 다른 팀원과의 협력이기도 하지만, 미래의 나 혹은 과거의 나와의 협력입니다.”

  1. 항상 과거의 코드를 보게 되면 코드가 이해가 안가는 부분이 많습니다..! 다른 분들은 이러한 문제를 줄이는 방식이 궁금합니다.

책의 내용 및 정리

저자는 남들이 나의 코드를 수정해도 여전히 동작해야 한다고 말한다.

최근에 과거에 푼 알고리즘 문제 풀이를 보게 되었는데 문제를 읽어도 코드가 이해가 되지 않았다..

이유는 복잡한 문제에 인풋값을 알파벳 순서로 처리하고 주석 또한 존재하지 않아서 전혀 어떤 동작을 하는지 감이 안잡혔다.

물론 코딩테스트에서는 위의 방법처럼 풀겠지만 혼자 풀이하는 과정에서는 좋은 방법은 아니였던 것 같다.

기술의 습득에 있어서 매우 오랜시간이 걸린다고 한다.

그때 그때 임시변통으로 자신이 저지른 실수에서 배우거나(반면교사..) 함께 일하는 선임자, 동료로 부터 단편적인 배움을 얻는다.

좋은 코드를 한줄 쓰기 위해서 많은 경험들이 필요하다고 했을 때 이 책은 저자가 몇 십년간 배운 ‘좋은 코드’의 한 방법이다.

위에서 방법이라고 생각한 이유는 저자가 계속 강조하는 정답은 없다이다.

정답에 근접하기 위한 여러가지 방법은 존재하지만 인생도 그렇고 어디에나 정답은 없다고 생각한다.

이 책은 그러한 방법 중 ‘좋은 코드’로 가기 위한 좋은 방향 중에 하나라고 생각한다.

모든 예제가 의사코드로 되어 있고 열린 마인드..?로 하셨다고 하니.. 기대가 된다.

1장 코드의 품질

서두르지 않으면 더 빠르다.

논의 사항

  1. 10쪽에서 보여주는 극단적인 시나리오 A와 B이러한 절충점을 찾을 때는 프로젝트와 조직문화에 달려있다고 합니다.

이러한 극단적인 사례를 경험 해보셨는지 경험 해보셨다면 과정과 결과 그리고 회고가 궁금합니다.

책의 내용 및 정리

핵심 주제

  • 코드 품질이 중요한 이유
  • 고품질 코드가 이루고자 하는 네 가지 목표
  • 고품질 코드 작성을 위한 높은 수준에서의 여섯가지 전략
  • 고품질 코드 작성이 어떻게 중장기적으로 시간과 노력을 절약할 수 있는지

좋은 소프트웨어와 끔찍한 소프트웨어

생각해보니 사용하기 정말 불편했던 소프트웨어와 문제를 찾아볼 수 없고 편의성이 높았던 소프트웨어는 확연하게 구분된다.

게임을 예로 들자면 퀼리티나 편의성은 정말 사소한 디테일에서 나온다고 매번 생각한다.(게임에 한정되지 않지만)

간단한 기능이라도 예외를 두고 만들었다면 해당 기능이 예외가 발생했을 때의 사용자는 불만도가 급격하게 상승하는 것 같다..

반대로 편의적인 사소한 디테일을 경험할 때는 안정감을 느끼는 것 같다.

이러한 차이의 근본이 여기서 말하는 확장성이 좋은 고품질의 코드가 아닐까..

소프트웨어가 만들어지는 과정

  1. 코드베이스(codebase): 소프트웨어를 빌드할 수 있는 코드가 저장된 저장소(git, perforce 등) Version Control System
  2. 코드 제출(submitting code): 커밋, 풀리퀘라고 불리기도 한다. 로컬에 저장된 내용을 코드베이스에 제출을 의미
  3. 코드 검토(code review): 제출전에 다른 동료들이 변경된 내용을 검토한다
  4. 제출 전 검사(pre-submit check): 병합 전 훅, 병합 전 점검, 커밋 전 점검이라고 하기도 하며 테스트가 실패하거나 컴파일 오류 시 병합되지 않도록 관리한다.
  5. 배포(release): 소프트웨어는 코드베이스의 스냅숏을 기반으로 빌드한다. 특정 버전을 가져와서 배포하는 프로세스를 배포 브랜치 만들기(cutting release)라고 한다.
  6. 프로덕션(production): 소프트웨어가 서버나 시스템에 배포될 때, 테스트 환경과 같이 내부적으로 사용하는 것이 아닌 실제 서비스되는 환경을 가리킨다.

코드품질을 점검하는 방법

코드 자체를 고품질 저품질로 정의하는 것 자체가 주관적인 행동이지만 이러한 주관을 좋은 방향으로 다듬기 위해선 다양한 경험이 필요하다.

저자는 코드가 정말로 달성하려는 것이 무엇인지 생각해보는 것을 말한다.

목표를 달성함에 있어서 도움이 되는 코드는 높은 품질의 코드이고, 방해가 된다면 낮은 품질의 코드이다.

며칠 전 멘토링을 받으며 객체지향에 대해서 이야기 했을 때 멘토님이 클래스의 이름과 public method만 나열해서 본다면 해당 객체의 목적을 알 수 있다고 해주셨다.

마찬가지로 좋은 품질의 코드를 작성하기 위해선 목적과 존재의 이유가 명확해야 한다.

코드 작성의 베이스 룰(상위 수준)

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

코드 품질의 핵심 요소

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

재사용성과 일반화성의 차이

  • 재사용성(reusability): 문제를 해결하기 위한 것이 여러가지 다른 상황에서도 사용될 수 있음을 의미
  • 일반화성(generalizability): 개념은 유사하나 서로 미묘하게 다른 문제들을 해결할 수 있음을 의미

드릴의 경우 구멍을 뚫는 다는 기능이 있어서 여러 곳에 적용할 수 있다.(재사용성)
드릴이 앞에 파츠만 교체한다면 나서도 박을 때 사용이 가능하다.(일반화성)

느낀점

머리말에서 말했던 정답은 없다 이후에 상당히 조심스러운게 느껴져서 재밌다.

객체지향에 한정되긴 하지만 언어의 가능성을 열어두고 과학, 물리와 같은 법칙이 없음을 강조한다.

스스로 절충점과 선을 찾아서 선택과 집중을 해야하는 점..!

예를 들어서 보여주는 상황들이 전부 재밌는 내용이라 집중이 잘되는 것 같다.. 초코 브라우니

2장 추상화 계층

코드 작성의 목적은 문제해결이다.

논의 사항

함수부분과 클래스부분을 읽으며 좀 더 극단적으로 가게된다면 함수형 프로그래밍으로 이어질 것 같다는 생각이 듭니다.
명확한 차이점이 있겠지만 다른 분들이 느끼는 차이점이 궁금합니다.

책의 내용 및 정리

핵심 주제

  • 깔끔한 추상화 계층을 통해 문제를 하위 문제로 세분화하는 방법
  • 추상화 계층이 코드 품질의 요소를 달성하는 데 어떻게 도움이 되는지
  • API 및 구현 세부 사항
  • 함수, 클래스 및 인터페이스를 사용해 코드를 추상화 계층으로 나누는 방법

문제의 단위

상위 수준의 문제를 해결하기 위해선 여러개의 작은 하위 문제들이 필요하다.

범용적인 내용인 것 같다.

일을 처리함에 있어서 단위를 나누는게 마치 인터페이스로 세분화하여서 모듈화를 목적에 두는? 일상 생활도 마찬가지라는 생각이 든다.

멘토링에서 처리해야 하는 일에 대해서 시간 단위로 일을 처리함을 물어보았는데 일에 단위를 시간으로 계산하기 보다 난이도로 결정하는게 좋을 것 같다고 하셨다.

난이도를 [1 3 5 7]정도로 구분한다면 미리 쌓아둔 데이트를 기반으로 나누고 5, 7의 경우 다시 1 3 5정도로 세분화를 하는 것이다.

하루 일에 대한 형태가 대충 트리형태로 그려지는 방법이였는데 매우 좋은 방법인것 같다.

그렇다고 해서 극단적인 세분화또한 독인 것 같다..(스스로 1 3 5 7에 대한 설계가 되어야함,, 0.2 0.1 이런 수준 x)

추상화 계층 구축의 장점

  1. 가독성: 코드베이스의 모든 세부사항을 이해하는 것은 불가능하지만 몇가지 높은 계층의 추상화를 이해하고 사용하는 것은 상당히 쉽다. 깨끗하고 뚜렷한 추상화 계층은 적은 개념을 다루기만 할 수 있다.
  2. 모듈화: 추상화 계층이 하위 문제에 대한 해결책을 깔끔하게 나누고 구현 세부 사항이 외부로 노출되지 않도록 보장할 때, 다른 계층에 영향을 미치지 않고 계층 내에서만 구현을 변경하기가 쉬워진다.
  3. 재사용성 및 일반화: 하위 문제에 대한 간결한 추상화 계층으로 제시되면 하위 문제에 대한 재사용하기 쉬워진다. 그리고 문제가 적절한 추상적인 하위 문제로 세분화된다면, 해결책은 여러 가지 다른 상황에서 유용하게 일반화될 가능성이 크다.
  4. 테스트 용이성: 하위 문제에 대한 해결책이 견고한지 테스트를 해야하는데 코드가 추상화 계층으로 깨긋하게 분할되면 하위 문제에 대한 해결책을 완벽하게 테스트하는 것이 쉬워진다.

계속 같은 목적을 이야기 하니까 슬슬 어느정도 청사진이 그려지는 것 같다.. 좋은 코드, 클린코드를 가지기 위해서 좀 더 생각해보고 지금까지 만든 코드를 반면교사로 삼아야겠다..

API(application programming interface)

코드를 작성할 때 고려해야 하는 측면이 두가지가 있다.

  1. 코드를 호출할 때 볼 수 있는 내용
  2. 코드를 호출할 때 볼 수 없는 내용

볼 수 있는 내용은 공개수준을 따라간다.

  • 퍼블릭 클래스, 인터페이스 및 메서드
  • 이름, 입력 매개변수 및 반환 유형이 표현하고자 하는 개념
  • 코드 호출 시 코드를 올바르게 사용하기 위해서 알아야 하는 정보

반대로 볼 수 없는 내용은 구현 세부 사항이다.

private아래 가려진 내부 함수와 변수들은 구현 세부 사항에 속하며 의존 하는 것 또한 세부 사항이다.

++ public 메서드라고 해도 이름, 반환값, 인수, 문서를 제외한 함수 내부는 세부사항이다.

함수

정말 객체지향에서 함수가 가져야 하는 기본적인 성격에 대해서 쉬운 기준을 보여주며 설명하지만 막상 내가 할 땐 왜이렇게 어려운지 모르겠다.

지키겠다고 생각하지 않고 급하게 개발을 해서 결과물 위주의 코딩을 반복해서 그런 것 같다.

읽기 좋은 함수를 짜고 확장성에 용이하고, 모듈화, 재사용성/일반화가 높은 수준의 함수를 요구한다.

막상 그러한 함수를 짜더라도 어느정도 커플링이 존재하기 때문에 점점 거대해지는 것 같다..

시간이 아주 조금 더 걸리더라도 헬퍼클래스로 빼거나 함수를 분리해야하는 정신이 필요하다.

최종적으로는 글을 읽듯이 함수를 순차적으로 읽을 수 있게 되는게 현재로서는 가장 나에게 필요한 형태인듯 하다.

클래스

단일 클래스에 대한 크기는 정말 밭을 가는 행위인 것 같다.

초장에 쉽게 갈려고 대충 갈고 농작물을 심어도 이후에 2배 3배로 고생하니 추상적 관계에 대해서 다시 고민하게 된다.

위와 같은 과정을 이미 여러번 직면해보니 꼭 필요한 과정이라고 생각한다.

왜 처음부터 구조를 생각해야 하는지 그 프로젝트를 보고 그 중간점을 찾는게 경험에서 나오는 능력인 것 같다.

지금 진행중인 프로젝트를 열어보니 가장 긴줄이 700줄 정도가 되는 클래스라니..

이걸 처음부터 수정을 할 생각을 해보기도 했지만 멘토님이 그 비용을 잘 생각해보고 이후에 좋은 반면교사로 활용해서 들고다니면 좋을 것이라는 이야기를 듣고 사이드 이펙트나 문제가 될만한 부분만 수정하고 그대로 두고 프로젝트를 마감할 생각이다..

책에서 말하는 300줄의 경우 절대적인 지표가 될 수 없지만 경고의 역할이 좋아보인다.

자신의 코드를 비판적으로 바라볼 때 줄수또한 좋은 지표가 되지 않을까.. (그렇다고 닌자코드나 줄 수를 줄이라는 노력이 아닌 것을 안다. 설계단계의 기준)

한 클래스는 오직 한 가지 일에만 관심을 가져야 한다.
클래스는 응집력이 있어야만 한다.

위와 같이 좋은 코드를 위한 지표도 존재하지만 책에선 근본적인 핵심을 먼저 이행하길 권한다.

  1. 코드 가독성: 단일 클래스에 담겨 있는 개념이 많을수록 해당 클래스의 가독성은 저하된다. 인간은 기본적으로 멀티에 약하다. 자신도 이해하기 힘든 코드는 남들이 볼 땐 2~3배 읽기 힘들 것이라는 사실을 알아야 한다.

사람들이 3줄요약을 좋아하는 이유도 비슷한 이유일 것.

  1. 코드 모듈화: 클래스 및 인터페이스의 사용은 코드 모듈화를 위한 좋은 방법 중 하나이다. 하위 문제에 대한 해결책이 하나의 클래스로 구현되어 있고, 다른 클래스와의 상호작용을 준비된 퍼블릭 메서드로만 이루어진다면 해결책에 대한 문제가 발생했을 때 쉽게 교체가 가능해진다. 말그대로 부품정도로..

  2. 코드 재사용성 및 일반화: 어떤 문제를 해결할 때 두 가지 하위 문제를 해결해야 하는 경우, 두가지 문제에 대한 해결책을 한 클래스로 묶어 둔다면 이후에 한가지 문제가 발생해도 다른 한가지 해결책을 사용할 기회가 줄어든다..

  3. 테스트 용이성 및 적절한 테스트: 마찬가지로 하위단위로 세분화가 된다면 테스트의 용이성으로 이어진다.

인터페이스

인터페이스는 추상화 계층을 깔끔하게 구현하는 코드를 만드는데 있어 매우 유용한 도구이다.

주어진 하위 문제에 대해 두가지 이상의 서로 다른 구헌이 가능해지고 이를 통해 모듈화를 실현할 수 있다.

하나의 인터페이스 및 단일 구현

인터페이스를 구현하는 클래스가 유일하다고 해도 이는 매우 유리하게 작용한다.

장점

  1. public API를 명확하게 보여준다.

인터페이스를 상속받은 클래스가 인자로 사용하게 된다면 해당 클래스에 퍼블릭함수를 추가하더라도 상위 클래스는 인터페이스만을 의존하기 때문에 해당 퍼블릭함수는 노출이 되지 않는다.

  1. 한 가지 구현만 필요하다고 잘못 추측한 것일 수 있다.

원래 코드를 작성할 때는 또 다른 구현이 필요하지않을 것이라는 확신을 하더라도 한 두달 후엔 후회할 수 있다

  1. 테스트를 쉽게 할 수 있다.

구현 클래스가 복잡하거나 네트워크 I/O에 의존하는 경우 목이나 페이크객체로 대체할 수 있다.

  1. 같은 클래스로 두 가지 하위 문제를 해결 할 수 있다.

한 클래스가 두 개 이상의 서로 다른 추상화계층에 구현을 제공할 수 있다.

장점이 있다면 단점도 물론이다.

단점

  1. 더 많은 작업이 필요하다.

말 그대로 인터페이스를 정의할려면 코드를 더 작성해야 한다.(생각도 더 해야한다.)

  1. 코드가 복잡해질 수 있다.

다른 개발자가 코드를 볼 때 하위 문제를 이해하기 위해선 클래스가 아닌 인터페이스 먼저 봐야한다.(해당 인터페이스를 구현하는 클래스를 봐야댐)

인터페이스만을 위한 인터페이스를 작성해서는 안된다.

이러한 단점도 존재하지만 장점도 명확하기 때문에 이 또한 절충점(선)을 잘 찾아야 할 것 같다.

기본적인 스탠스로 클래스를 설계할 때 인터페이스를 붙이는 것이 어렵지 않게 설계해야 한다.

너무 비대한 계층 때문에 발생하는 문제는 너무 얇은 계층 때문에 발생하는 문제보다 더 심각하다. 확실하지 않은 경우에는 남용의 위협에도 불구하고 계층을 얇게 만드는 것이 좋다

느낀점

클래스를 많이 만드는 것에 대한 무서움을 줄여보자..(무작정 많이 만드는 것이 아닌 설계에 따라서)

과연 내가 많이 만드는 것에 대한 무서움을 느낀 것인지 그냥 귀찮아서 인지는 아직까지 잘 모르겠다.

읽다보니 많이 부끄러워지기도 하고 개발을 1년하고 그만두는게 아니니까.. 문제점을 제대로 직면했다는 것이 중요하다.

이번 챕터는 실제로 코드를 작성해보고 저자가 소개한 다양한 방법을 적용해보기 좋은 것 같다. 매우매우

일단 기억나는대로 룰을 지켜가며 코딩한 뒤 다시 가독성, 모듈화, 재사용/일반화, 테스트 용이성을 토대로 비판적인 시각으로 바라보면 답이 쉽게 나올 것 같다.

아직은 DI나 인터페이스자체를 상속, 인터페이스를 가지는 인터페이스등 다양하게 활용 경험이 없어서 조금 어려운 부분이 있다.

내용을 정리해보며 공부하고 다시 포스팅 예정

개념은 잡힌 상태지만 활용아직 미숙..

3장 다른 개발자의 코드 계약

자신이 작성한 코드를 다른 개발자가 작업해야 하고 반대로 다른 개발자가 작업한 코드를 자신이 작업해야 한다.

논의 사항

객체지향에 맞게 명확한 목적을 가진 클래스가 설계했다면 주석이 필요 없을까요?

하위단부터 추론이 가능한 1단계 이름부터 잘 설계되었다면 클래스를 거슬러 올라가는 불편함은 있겠지만 목적이 분명하다면 주석이 필요 없을 것 같다는 생각이 듭니다.

책의 내용 및 정리

핵심 주제

  • 다른 개발자들이 코드와 어떻게 상호작용하는지
  • 코드 계약과 코드 계약의 세부 조항
  • 세부 조항을 최소화하는 것이 어떻게 오용과 예측을 벗어나는 코드를 예방하는 데 도움이 되는지
  • 세부 조항을 피할 수 없다면 체크와 어서션을 어떻게 사용할 수 있는가?

자신의 코드와 다른 개발자의 코드

1장에서 설명한 예측 가능한 코드를 작성하라코드를 오용하기 어렵게 만들어라 이 두가지 원칙은 다른 사람과 상호작용할 때 일어날 수 있는 일이다.

따라서 협업의 경우에는 아래 규칙을 고려하는 것이 좋다.

  1. 자신에게 명백하다고 해서 다른 사람에게도 명백한 것은 아니다.
  2. 다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.
  3. 시간이 지남에 따라 자신의 코드를 기억하지 못한다.

자신에게 명백하다고 해서 다른 사람에게도 명백한 것은 아니다.

자신의 로직에 너무 익숙해져 모든 것이 분명해 보이기 때문에 과정에 대해서 망각하게 된다.

어느 시점부터는 다른 개발자가 작성한 코드와 상호작용하거나 변경할 수 있다는 점을 인지해야 한다.

그렇다고 코드에 주석을 많이 달라는 것이 아닌 코드 자체를 읽기 쉽게 만들어야 한다.(본인이든 남이든)

다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.

내가 작성한 코드는 생각보다 많은 의존성을 가지게 될 것이다.

또한 코드자체도 환경이나 요구사항으로 인해 많이 변화할 것이기 때문에 병합전에 이런 문제를 제대로 확인하고 넘어가야 한다.

의도지 않게 문제가 발생하게 되면 찾기 쉽지 않기 때문에..

시간이 지남에 따라 자신의 코드를 기억하지 못한다.

당장 저번달 작성한 코드도 기억안나고 읽히지도 않기 때문에..ㅜ

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

API 공개수준이외에도 내가 작성한 코드가 무슨 목적을 가지고 있는지 파악해야 한다.

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

1년이 지나면 자신이 작성한 코드라도 까먹기 때문에 미래의 자신은 본질적으로 다른 개발자라고 간주한다.

따라서 다른 개발자가 내가 작성한 코드를 알아내기 위해선 다음과 같은 추리를 한다.

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

이름 짓기의 경우 농담식으로 가장 오래걸리는 코딩작업이라고 한다.

그만큼 다른 개발자(미래의 나)가 볼 때 직관성 띄기 때문에 바로바로 추론이 가능하다.

멘토님의 말대로 클래스 이름과 퍼블릭 메서드만 나열해도 해당 클래스의 목적이 보여야 한다.

코드 계약

계약에 의한 프로그래밍(programming by contract) 또는 계약에 의한 디자인(design by contract)라는 용어를 접해본적이 있을 것이다.

나는 없다..

이 방법론은 서로 다른 코드 간의 상호작용을 마치 계약처럼 생각한다는 점이다.

계약은 3가지 범주로 나뉜다.

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

개발자가 계약의 일부 혹은 모든 조건을 알지 못하면 코드 계약에 문제가 발생한다.

따라서 계약내용이 무엇일지, 코드를 사용하는 사람이 계약을 파악하고 따라갈 수 있을지에 대한 생각을 해야한다.

코드를 오용할 수 있는 방법이 많을수록 실제로 오용되고 버그가 많이 있음을 시사한다.

따라서 방어적인 프로그래밍 자세를 취하는게 좋아보인다.

세부조항으로 문서나 주석을 통해 경고하는 것 보다 애초에 불가능하게 만드는 것이 좋다.

내부적으로 예외를 많이 두어서 다른 방향으로 오용된다면 다른 개발자는 버그라고 생각

책에서는 생성자를 가려서 애초에 원하는 방법으로 객체를 생성하게 막아둠

상태와 가변성

객체지향에서 상태는 객체가 담고있는 어떤 값이나 데이터를 말한다. 이러한 상태를 생성 이후에 수정이 가능하다면 가변적, 반대로 수정이 불가능 하다면 불변적이라고 칭한다.

느낀점

세부조항의 부분이 많이 생각하게 된다.

이전에 기본 c라이브러리 함수를 구현한 적이 있는데 완벽한 함수를 만들겠다고 null에대한 예외처리 등 들어오는 값에 대한 예외를 되게 크게 잡았다.

이후에 그게 틀렸다는 것만 알고 있었는데 이제와서 정리가 되는 것 같다.(의도하지 않은 동작, 오용)

지금 생각해보면 좋은 경험이였다.

뒤쪽의 체크, 어서션은 읽어도 이해가 잘 되지 않아서 어려웠다..ㅠ

4장 오류

코드가 실행되는 환경은 불완전하다.

논의 사항

실제로 예외를 구현해서 사용하는 경우가 많나요?

대분의 예외는 만들어져있다고 알고 있었습니다.

필요에 의해서 만들어진 예외는 어떤 경우가 있는지 궁금합니다.

책의 내용 및 정리

핵심 주제

  • 시스템이 복구할 수 있는 오류와 복구할 수 없는 오류의 구분
  • 신속하게 실패하고 분명하게 실패함
  • 오류를 전달하기 위한 다양한 기법과 선택을 위한 고려 사항

복구 가능성

소프트웨어에 대해 생각할 때, 특정 오류가 발생한 경우 복구할 수 있는 오류와 복구할 수 없는 오류가 있다.

즉, 오류가 발생했을 때 무엇을 할 것인지 결정하기 위해서는 자신의 코드가 어떻게 사용될 것인지 신중하게 생각해야한다.

  • 복구 가능한 오류

전화번호를 예로 유효하지 않은 입력값이 들어왔을 때 전체시스템 작동을 멈춘다면 문제가 될 수 있다.

따라서 오류메세지를 제공하고 다시 요청하는 것이 낫다.

이 내용을 읽고 처음 든 생각은 하나의 함수 자체에서 널값이 들어왔을 때 nullreferenceexception이 발생하고 예외처리를 따로 하지 않았다면 시스템자체가 멈추게 된다.

이러한 예외를 막고자 앞에서 말했던 방어적 프로그래밍으로 일어날 수 있는 예외를 사전에 미리 차단하는 것도 중요하다.

그렇다면 여기서 또 의문이다. 3장에서 말했던 광범위한 함수 내부에서 예외처리를 하게 된다면 함수를 오용하게 되고 함수 자체의 성격도 옅어지게 된다.

이러한 문제에서 절충점이 뭘까 생각하니 위에서 말한 함수내부에서 처리하지 않고 외부로 반환값을 줘서 해당 경우에 대한 예외레벨에서 처리하는 것이다.

이게 맞는 방법인지는 잘모르겠지만.. 지금의 생각은 이렇다..

  • 복구할 수 없는 오류

오류가 발생하고 시스템이 오류를 복구할 수 있는 합리적인 방법이 없을 때가 있다.

대부분 프로그래밍 오류때문에 발생한다.

  • 코드와 함께 추가되어야 하는 리소스가 없다.
  • 어떤 코드가 다른 코드를 잘못 사용한다.(잘못된 인수, 사전에 초기화 작업x)

개발자가 이러한 오류를 해결하는 방법인 일찍이 오류를 발견하고 해결하는 것..

신속한 실패

견고성과 실패

소프트웨어가 오류가 발생한다면 실패와 견고성중 하나를 선택해야 한다.

실패의 경우는 더 높은 계층이 오류를 처리하게 하거나 전체 프로그램의 작동을 멈추게 한다.

오류는 처리하고 계속 진행한다.

후자의 경우는 실행자체에 문제가 없으니 견고한 코드라고 볼 수 있지만 위험부담을 그 만큼 가지고 갈 수 있다.

신속하게 실패하라

말 그대로 실패를 두려워 하지 않고 오류를 훌룡하고 안전하게 복구할 수 있는 기회라고 생각하는 것이다.

이 또한 계층을 나누고 세분화를 통해 모듈화를 하게 되면 테스트에 용이하고 이 처럼 실패를 쉽게 만날 수 있다.

요란하게 실패하라

오류가 발생하는데도 불구하고 아무도 모르는 상황을 막고자 하는 것이다.

이를 위한 가장 명백한 방법은 예외를 발생해 프로그램이 중단되게 하는 것

오류를 서버에 기록하는 방식의 내용이 나왔는데 가끔 프로그램에서 오류가 발생하면 해당 로그를 보내달라는 메세지같이 오류를 수집하는 내용도 흥미로웠다..

오류를 숨기지 않음

오류를 숨겨서 프로그램의 동작이 문제가 없어 보여도 오류를 숨기는 것은 복구할 수 있는 오류와 복구할 수 없는 오류 두가지 문제를 모두 일으킨다.

기본값 반환

오류가 발생하고 함수가 값을 반환할 수 없을 때 기본값을 반환하는 것은 간단하고 쉬운 해결책처럼 보일 수 있다.

이는 오류를 숨기는데 있어서 호출하는 쪽에서는 모든 것이 정상처럼 작동한다.

당장 치명적인 오류를 가져올 수 있고 이후에 찾기 힘든 오류로 전락할 수 있다.

널 객체 패턴

개념적으론 기본값과 유사하지만 이것을 더 확장하여 더 복잡한 객체를 다룬다.

널 객체는 실제 반환값처럼 보이지만 모든 멤버 함수는 아무것도 하지 않거나 의미 없는 기본값을 반환한다.

아무것도 하지 않음

코드가 무언가를 반환하지 않고 단지 어떤 작업을 수행하는 경우, 문제가 발생할 때 가능한 한 가지 옵션은 오류가 발생했다는 신호를 보내지 않는 것이다.

오류 전달 방법

오류가 발생하면 일반적으로 더 높은 계층에게 알려야 한다.

오류로부터 복구할 수 없는 경우 일반적으로 훨씬 높은 계층에서 실행을 중지하고 오류를 기록하거나 프로그램의 종료하는 것을 의미한다.

  • 명시적 방법: 코드를 직접 호출한 쪽에서 오류가 발생할 수 있음을 인지할 수 밖에 없도록 한다.
  • 암시적 방법: 코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류를 신경쓰지 않아도 된다.

예외

예외라는 개념은 코드에서 오류나 예외적인 상황이 발생한 경우 이를 전달하기 위한 방법으로 고안되었다.

일반적으로 예외 또한 충분한 기능을 가진 클래스로 구현되며, 요구사항에 맞춰 예외를 정의하고 구현할 수 있다.

  • 명시적 방법: 검사 예외

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

함수 시그니처란? 함수 원형에 명시되는 매개변수 리스트를 가리킨다.

  • 암시적 방법: 비검사 예외

비검사 예외를 사용하면 다른 개발자들은 코드가 이 예외를 발생시킬 수 있다는 사실을 전혀 모를 수 있다.

이 경우에는 함수에서 어떤 예외를 발생시키는지 문서화를 하는 것이 바람직하지만 이 과정조차도 잊어버릴 때가 있다.

하지만 이러한 문서화 작업은 앞서 다룬 세부조항이기 때문에 신뢰할 방법이 아니다.

따라서 비검사 예외는 오류가 발생할 수 있다는 것을 호출하는 쪽에서 인지하리라는 보장이 없기 때문에 오류를 암시적으로 알리는 방법이다.

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

함수에서 널값을 반환하는 것은 특정값을 계산하거나 얻는 것이 불가능함을 나타내기 위한 효과적이고 간단한 방법이다.

https://learn.microsoft.com/ko-kr/training/modules/csharp-null-safety/

주로 사용하는 언어인 C#은 null 안정성에 대한 튜토리얼까지 만들어 두었다..(재밌다)

현재로는 내가 가장 많이 예외를 처리하는 방법인 것 같다.

내가 사용하는 처리방법에 대해 정리가 되고 다른 방법들도 소개가 되니 처음에는 읽기 힘들었는데 아는 내용과 상상가능한 내용이 나오니 다시 잘 읽어진다..

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

널값이나 옵셔널 타입을 반환활 때의 문제 중 하나는 오류 정보를 전달할 수 없다는 것이다.

호출자에게 값을 얻을 수 없음을 알릴 뿐만 아니라 값을 얻을 수 없는 이유까지 알려주면 유용하다.

이러한 경우에는 리절트 유형을 사용하는 것이 적절할 수 있다.

  • 명시적 방법: 아웃컴 반환 유형

어떤 함수들은 값을 반환하기보다는 단지 무언가를 수행하고 값을 변환하지 않는다.

어떤일을 하는동안 오류가 발생해 그것을 호출한 쪽에 알리고자 한다면 함수가 수행한 동작의 결과를 나타내는 값을 반환하도록 함수를 수정하는 것이 한 가지 방법이 될 수 있다.

  • 암시적 방법: 프로미스 또는 퓨처

비동기적으로 실행하는 코드를 작성할 때 프로미스나 퓨처를 반환하는 함수를 작성하는 것이 일반적이다.(대부분의 언어는 프로미스나 퓨처는 오류상태도 전달할 수 있다)

  • 암시적 방법: 매직값 반환

매직값은 함수의 정상적인 반환 유형에 적합하지만 특별한 의미를 부여하는 값이다.

매직값이 반환될 수 있다는 사실을 알려면 문서나 코드를 읽어야 하기 때문에 암시적인 오류 전달 기법이다.

복구할 수 없는 오류의 전달

앞서 말한 복구할 수 없는 오류가 발생한다면 신속하고 그리고 요란하게 실패하는 것이 최상의 방법이다.

이를 달성하기 위한 몇가지 방법이 있다.

  • 비검사 예외를 발생
  • 프로그램이 패닉이 되도록
  • 체크나 어셔선을 사용

이러한 경우 프로그램이 종료되는데, 이는 개발자들이 뭔가 잘못되었음을 알아차린다는 것을 의미한다.

이러한 즉발적인 오류를 좋은 영향으로 인지하는게 중요하다.

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

이에 대한 방법은 다양하게 존재하며 상당히 주관적임을 설명한다.

따라서 프로젝트 그리고 팀을 그대로 따라가는게 좋은 방법이다.

비 검사 예외 VS 명시적 기법

  • 비검사의 예외를 사용해야 한다는 주장
  1. 코드 구조 개선: 대부분의 오류 처리 코드의 상위 계층에서 이루어질 수 있기 때문에 비검사 예외를 발생시켜 코드 구조의 개선을 주장
  2. 개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함
  • 명시적 기법을 사용해야 한다는 주장
  1. 매끄러운 오류 처리: 비검사 예외를 사용한다면 모든 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기 어렵다.
  2. 실수로 오류를 무사할 수 없다: 어떤 호출자는 실제로 오류를 처리해야 하는 경우가 있을 수 있다.
  3. 개발자들이 무엇을 할 것인지에 대해서 실용적이어야 함(마찬가지로 적용 가능)

다양한 주장이 있지만 저자는 명시적 방식을 사용하라라고 말한다.

느낀점

경험이 부족하다 보니 단편적인 내용이나 상상으로 보는 부분이 있었다..

나중에 경험을 하고 다시 읽어보면 또 크게 와닿지 않을까?

읽다보니 오류를 예측하는 능력이 필요해보인다.

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

가독성은 본질적으로 주관적인 것이며 그것이 정확히 무엇을 의미하는지 확실하게 정의하기는 어렵다.

논의 사항

명명된 상수를 사용하는 것이 좋을까요,, 공급자 함수를 사용하는게 좋을까요?

개개인마다 스타일의 차이가 있을 수 있지만, 선호하시는 스타일이나 장점 단점이 있을까요?

++ 익명함수를 사용하신 적의 예를 알려주실 수 있나요?

책의 내용 및 정리

핵심주제

  • 코드가 그 자체로 설명이 되도록 하기 위한 방법
  • 다른 사람들에게 코드의 세부적 내용을 명확하게 함
  • 언어의 기능을 사용할 때 그에 합당한 이유를 가져야 함

서술형 멍칭 사용

이름을 붙이는 것은 그것이 스스로 설명되는 방식으로 언급함으로써 읽기 쉬운 코드 작성을 위한 기회이기도 하다.

책에서 주어지는 극단적인 예제처럼.. 코딩테스트용으로 짠 코드들은 다시 읽기 매우 힘들다는 단점이 있다.

해결방법중의 하나로 서술적인 이름을 짓는 것이다.

배워야하는 시선은 team.containsPlayer(playName)의 호출이 Team클래스를 확인하지 않더라도 동작을 이해할 수 있다는 점..

호출 자체로 클래스를 유추하거나 이해하게 세분화되어 있거나 이름 자체가 서술적이면 보는 사람도 편하게 이해가 된다는 점이다.

주석문의 적절한 활용

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

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

주석을 사용하기 이전에 주의해야하는 점은 클래스같은 단위가 큰 코드가 무엇을 하는지 요약하는 높은 수준에서의 주석문은 유용하지만, 메서드,변수등 한줄 씩 주석을 다는 행위는 조심해야 한다.

앞서 다룬 서술적인 명칭을 사용한다면 해당 코드는 그 자체로 설명이 된다.

코드의 기능을 설명하기 위해서 낮은 층위에 주석을 많이 추가하는 행위 자체가 가독성을 떨어진다.

그럼에도 낮은 층위에서 사용해야하는 문제들은 적절하게 상식에 맞게 사용하는 것이 중요

중복된 주석문의 유해성

코드자체로 해석이 기능이 설명되는 코드가 있다고 했을 때 해당 코드에 주석을 달아놨다면 해당 주석은 중복된 것이라고 본다.

중복된 주석의 문제는 다음과 같다.

  • 개발자는 주석문을 유지보수해야 한다. 코드를 변경하면 주석문 역시 수정해야 한다.
  • 코드를 지저분하게 만들어서 가독성을 떨어트린다.

따라서 코드자체가 기능을 설명할 수 있도록 작성하는 것이 좋다.

주석문으로 가독성 높은 코드를 대체할 수 없다.

쉽게 함정에 빠질 수 있는 문제로 코드자체의 가독성이 부족해서 주석문이 필요할 수 있다.

하지만 더 좋은 접근법은 코드자체의 품질을 높이는 것이다.

주석문은 코드의 이유를 설명하는 데 유용하다.

코드자체로 설명하기 어려운 것에는 이유(왜)가 있다.

다른 개발자가 배경 상황없이 코드를 봤을 때 코드의 존재이유를 설명하기엔 주석이 매우 적절하다.

  • 제품 또는 비지니스 의사 결정
  • 이상하고 명확하지 않은 버그에 대한 해결책
  • 의존하는 코드의 예상을 벗어나는 동작

주석문은 유용한 상위 수준의 요약 정보를 제공할 수 있다.

코드가 무슨 일을 하는지 설명하는 주석문과 문서는 마치 책을 읽을 때 줄거리 같다.

  • 책의 모든 페이지 단락 앞에 줄거리가 있다먼 성가시고 읽기 어려운 책
  • 책의 목차부분이나 장의 시작부분에 간략한 요약을 보여주면 매우 유용할 책

아래의 경우가 주석을 상위수준에서 다루는 것은 다음과 같은 이점이 있다.

  • 클래스가 수행하는 작업 및 다른 개발자가 알고 있어야 할 중요한 세부 사항을 개괄적을 관리하는 문서
  • 함수에 대한 입력 매개변수 또는 기능을 설명하는 문서
  • 함수의 반환값이 무엇을 나타내는지 설명하는 문서

그럼에도 개발자들 대부분은 문서를 확인하지 않기 때문에 주석문에 의지하지 않는 편이 바람직하다.

코드 줄 수를 고정하지 말라

일반적으로 코드베이스의 코드 줄 수는 적을수록 좋다.

코드줄이 많다는 것은 코드가 지나치게 복잡하거나, 기존 코드를 재사용하지 않고 있음을 나타내는 신호일 수 있다.

또한 코드 줄이 많으면 읽어야하는 양이 늘어나기 때문에 인지부화가 올 수 있다.

그러나 이러한 주장때문에 줄 수를 극단적으로 줄여버리는 사례가 종종 발생한다.

코드 줄 수는 우리가 실제로 신경 쓰는 것들을 간접적으로 측정해줄 뿐이다.

따라서 1장에서 다룬 내용을 명심해야 한다.

  1. 이해하기 쉽다.
  2. 오해하기 쉽다.
  3. 실수로 작동이 안 되게 만들기 어렵디.

간결하지만 이해하기 어려운 코드는 피하라

일명 닌자코드..

코드의 길이를 극단적으로 줄이고 로직을 몰아넣음으로 전체적인 줄 수를 줄일 수 있지만 읽기는 매우 어렵다.

  • 다른 개발자는 이 단 한 줄의 코드에서 많은 세부사항을 도출하기 위해 많은 노력을 기울여야 한다.
  • 따라서 수정이 매우 힘들다.

클래스를 세분화하는 것과 마찬가지로 코드자체도 모듈화, 가독성을 위해 더 많은 줄이 필요하더라도 가독성이 높은 코드를 작성하라

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

문법적으로 올바른 문장을 쓰려면 지켜야 할 규칙이 있디. 이에 더해 잘 읽히는 문장을 쓰기 위해 따라야 할 문체에 관한 지침들도 있다.

다양한 언어에서 제공하는 스타일은 제각각이며, 회사나 팀마다 스타일이 다르다.

매번 다른 스타일을 사용해가며 일을 하게 되면 과거 작성한 다른 코드와 대소문자의 레벨차이가 발생할 수 있다.

깊이 중첩된 코드를 피하라

일반적으로 코드는 다음과 같이 서로 중첩되는 블록으로 구성된다.

  • 함수가 호출되면 그 함수가 실행되는 코드가 하나의 블록이 된다.
  • if문의 조건이 참일 때 실행되는 코드는 하나의 블록이 된다.
  • for 루프의 각 반복 시 실행되는 코드는 하나의 블록이 된다.
if (...)
{
  for (...)
  {
    if(...)
    {
      ... 
    }
  }
}

중첩이 깊은 경우

깊이 중첩된 코드는 읽기 어려울 수 있다.

if(...)
{
  return ...;
} else {
  if (...)
  {
    return ...;
  } else {
    return ...;
  }

  retur ...;;
}

중첩을 최소화하기 위한 구조 변경

앞선 코드의 중첩을 피해하기 위해 논리를 재구성하는 것은 쉬울 때가 많다.

if (...)
{
  return ...;
}
if (...)
{
  return ...;
}
if (...)
{
  return ...;
}
return ...;

중첩은 너무 많은 일을 한 결과물이다.

중첩이 너무 많은 코드의 경우 많은 일을 한 함수가 너무 몰아서 하고 있음을 의심해야 한다.

따라서 더 작은 함수로 나누면 문제를 해결할 수 있다.

더 작은 함수로 분리

2장에서 다룬 하나의 함수가 너무 많은 일을 하면 추상화 계층이 나빠진다는 점을 기억하라.

중첩이 없더라도 많은 일을 하는 함수를 더 작은 함수로 나누는 것은 여전히 바람직 하다.

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

어떤 함수의 이름이 잘 명명되면 그 함수가 무슨 일을 하는지 분명하지만 이름을 잘 지어졌더라도 함수의 인수가 무엇을 위한 것이고, 무슨 역할을 하는지 명확하지 않다면 함수 호출 자체가 이해되지 않을 수 있다.

많은 함수 인수

기본적으로 함수 호출은 인수의 개수가 늘어나면 이해하기 힘들어진다.

함수의 인수의 개수가 많다는 것은 근본적인 문제를 나타낸다.

추상화 계층을 적절하게 정의하지 않았거나, 코드가 모듈화되지 않았음을 의미

매개변수는 이해하기 어려울 수 있다.

sendMessage("hello", 1, true)

void sendMessage(String message, int priority, Boolean allowRetry){

}

함수 호출 시 각 인수의 값이 무엇을 의미하는지 알려면 함수 정의를 확인해봐야 한다.

함수 정의가 완전히 다른 파일에 있거나, 수백 줄 떨어져 있다면 이것은 상당히 힘든 작업일 수 있다.

명명된 매개변수 사용

다양한 언어에서 지원하는 명명된 매개변수 또는 선택적 매개변수를 활용하여 가독성을 높일 수 있다.

서술적 유형 사용

위의 정수형이나 불리언값은 어떤 종류의 값이라도 의미할 수 있기 때문에 서술적이지 않다.

서술적 유형을 위해서 특정 유형을 만들어서 매개변수들로 나타내는 바를 설명하는 것이다.

  • 클래스: 메시지의 우선순위를 클래스로 표현한다.
  • 열거형: 재시도 정책은 불리언 대신 두 가지 옵션이 있는 열거형을 사용한다.
class MessagePriority{
  ...
  MessagePriority(int priority) { ... }
  ...
}

enum RetryPolicy {
  ALLOW_RETRY,
  DISALLOW_RETRY
}

void sendMessage(
  String message,
  MessagePriority priority,
  RetryPolicy retryPolicy)
  {
    ...
  }
)

---  

sendMessage("hello", new MessagePriority(1), RetryPolicy.ALLOW_RETRY);

때로는 훌룡한 해결책이 없다.

사용하는 언어가 명명된 매개변수를 지원하지 않는경우, 사각형의 모서리위치를 매개변수로 받아야 한다면 호출 순서를 헷갈릴 가능성이 있다.

주석을 사용하여 생성자 아래에 크기를 적을 수 있지만 값이 수정되는 경우 주석또한 반영해야 하기 때문에 만족스러운 해결책이 아니다.

IDE는 경우

IDE에서 지원하는 기능은 백그라운드 작업을 통해 함수 정의를 미리 찾고 매개변수의 이름을 표시해준다.

// 실제 코드
sendMessage("hello", 1, true);

// IDE가 보여주는 코드
sendMessage(message:"hello", priority:1, allowRetry:true);

매우 유용한 기능이지만 가독성을 높이기 위해 IDE기능에 의존하는 것은 좋지 않다.

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

하드 코드로 작성된 값이 필요한 경우가 많이 있는데, 몇가지 일반적인 예가 다음과 같다.

  • 한 수량을 다른 수량으로 변환할 때 사용하는 계수
  • 작업이 실패할 경우 재시도의 최대 횟수와 같이 조정 가능한 파라미터 값
  • 어떤 값이 채워질 수 있는 템플릿을 나타내는 문자열

하드 코드로 작성된 모드 값에는 두 가지 중요한 정보가 있다.

  • 값이 무엇인지: 컴퓨터가 코드를 실행할 때 이 값을 알아야 한다.
  • 값이 무엇을 의미하는지: 개발자가 코드를 이해하려면 값의 의미를 알아야 한다. 이 정보가 없으면 코드르 이해할 수 없다.

값은 당연히 존재한다. 가장 중요한 점은 다른 개발자가 해당 값들을 명확하게 이해하도록 하는 것이 중요하다.

설명되지 않은 값은 혼란스러울 수 있다.

함수에 들어가는 다양한 리터럴 값이 있다.

코드에 설명되지 않는 값이 있으면 혼란을 초래하고 이로 인해 버그가 발생할 수 있다. 그 값이 무엇을 의미하는지 다른 개발자들에게 명확하게 알려주는 것이 중요하다.

잘 명명된 상수를 사용하라

함수 내부의 리터럴값들을 피하기 위해서, 수정에 간편함을 위해서 상수이름 자체를 통해 값을 설명하고 가독성을 높이는 것

잘 명명된 함수를 사용하라

코드의 가독성을 높이기 위해서 잘 명명된 함수를 사용하는 방법은 두 가지가 있다.

  • 상수를 반환하는 공급자 함수
  • 변환을 수행하는 헬퍼 함수

공급자 함수란, 개념적으로 상수를 사용하는 것과 동일하며, 단지 약간의 다른 방식으로 이루어진다.

private static Double kilogramsPerUsTon()
{
  return 907.1847;
}

헬퍼 함수란, 공급자 함수를 사용하는 것의 대안으로 수량의 변환을 하위 문제로 만들어 이 기능을 전문적으로 수행하는 함수를 작성하는 것이다.

일반적으로 정의한 값이나 헬퍼함수를 다른 개발자들이 재사용할 것인지 고려해볼 만한 가치가 있다.

즉, 유틸리티 클래스(헬퍼 클래스)를 따루 둬서 관리하는 것

이것 또한 한번에 몰아서 작성하는 것이 아닌 성격에 맞게 어느정도 분리하는 것이 좋다.

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

익명 함수란, 이름이 없는 함수이며, 일반적으로 코드 내의 필요한 지점에서 인라인으로 정의된다.

익명 함수는 간단한 로직에 좋다.

단 하나의 문장이면 충분하고, 해결하려는 문제는 간단한 내용은 익명함수로 다루는 것이 좋다.

논리가 단순, 자명

명명된 함수로 작성하는 것보다 이득인 경우와 아닌 경우가 항상 다르기 때문에 충분히 고민 후 결정하는 것이 좋다.

나중에 가면 어느정도 틀이 보이기 시작하지 않을까..

익명 함수는 가독성이 떨어질 수 있다.

앞 장에서 다룬 내용과 같이 함수의 이름은 그 함수가 무엇을 하는지 간결하게 요약해주기 때문에 코드의 가독성을 높이는 데 매우 유용하다.

하지만 익명 함수는 정의상 이름이 없기 때문에 코드를 읽는 사람에게는 제한된 정보를 제공한다.

따라서 익명 함수가 얼마나 간단한 것이든 함수의 내용이 자명하지 않다면 코드의 가독성은 떨어지기 마련이다.

따라서 익명함수를 쓰는것이 반드시 좋은 것은 아니다.

대신 명명 함수를 사용하라

익명대신의 명명함수의 단점은 더 많은 코드를 작성해야 한다는 것이다.

대신 익명함수는 항상 작성해야하는 문장을 작성하지 않아도 되기 때문에 코드를 줄이는 데는 뛰어나지만.. 이름이 없다는 단점이 있다.

따라서 간단하고 자명한 논리는 익명 함수, 복잡한 논리는 명몀 함수

익명 함수가 길면 문제가 될 수 있다.

함수형 프로그래밍이라고 해서 반드시 익명 함수를 사용해야 하는 것은 아니다.

2장에서 말한 읽고, 이해하고, 재사용하기 쉽도록 작고 간결하게 작성하는 것이 중요하다고 했다.

위의 규칙을 잊어버리고 너무 많은 논리, 익명 함수 중첩등 거대한 익명함수를 생성하곤 한다.

만약 익명함수가 2~3줄 이상으로 길어진다면 명명 함수로 수정하는 것이 좋다.

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

여전히 프로그래밍언어는 변화를 거듭하고 있다. 성장하는 개발자라면 변화하는 패러다임에 당연하게 관심을 갖기 마련이다.

다양한 기능들을 활용하기를 권한다.

but, 신기술을 억지로 적용하기 보다 적합한지 진지한 판단이 필요하다.

새 기능은 코드를 개선할 수 있다.

자바의 스트림기능, C#의 프로퍼티 등등 다양한 기능들은 가독성이 좋은 코드를 만들어준다.

예제의 내용이 Rx프로그래밍과 매우 닮은 듯한 느낌이다.

유니티에서 본 스트림을 사용한 프로그래밍과 비슷하다.

불분명한 기능은 혼동을 일의킬 수 있다.

프로그래밍 언어에서 제공하는 기능이 확실한 이점을 가지고 있다고 하더라도 다른 개발자들에게 얼마나 알려저 있는지 고려해볼 필요가 있다.

마찬가지로 도입에 대한 팀의견, 동료의 의견이 중요하다.

적용하기 애매하거나 반응이 냉소적이라면 사용하지 않는 것이 좋을 때도 있다.

작업에 가장 적합한 도구를 사용하라

예제와 비슷한 예로 프로그래밍 패턴을 공부할 때 필요가 아닌 학습을 위해 진행중인 프로젝트에 억지로 끼워넣은 적이 있다.

생각한 내용보다 가독성도 떨어지고 방대해지는 것을 보고 나중에 깨달았지만 처음부터 좀 더 고민하고 만들었다면 문제가 생길일이 없었다.

느낀점

이번 장은 당장 적용해볼 수 있는 내용과 머릿속에 있던 내용들을 한번에 잘 정리해준 장이라 매우 마음에 든 챕터다..

지금 읽고 있는 견습패턴과 관련이 있는 내용들이 많아서 나름대로의 해석이 가능한 점이 재밌다.

팀단위의 관리나 적용방법도 고민해볼 수 있는 것 같고 내용 자체도 어렵지 않아서 두고두고 읽으면 좋을 내용

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

우리는 다른 개발자가 작성한 코드에 기반하여 코드를 작성하고, 다른 개발자들은 다시 우리가 작성한 코드를 기반해서 코드를 작성한다.

논의 사항

테스트 코드에 대한 생각이 궁금합니다..!

저자가 생각하는 마지막 내용과 비슷한지.. 다른 견해를 가지고 계신지

책의 내용 및 정리

핵심 주제

  • 코드가 어떻게 예측을 벗어나 작동할 수 있는지
  • 소프트웨어에서 예측을 벗어나는 코드가 어떻게 버그로 이어질 수 있는지
  • 코드가 예측을 벗어나지 않도록 보장하는 방법

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

매직값은 함수의 정상적인 반환 유형에 적합하지만 특별한 의미를 가지고 있다.

매직값의 일반적인 예는 값이 없거나 오류가 발생했음을 나타내기 위해 -1을 반환하는 것이다.

매직값은 정상적인 반환 유형에 들어맞기 때문에 이 값이 갖는 특별한 의미를 인지하지 못하고, 이에 대해 적극적으로 경계하지 않는 이상 정상적인 반환값으로 오인할 수 있다.

매직값은 버그를 유발할 수 있다.

레거시 코드들의 일부분을 보면 매직값을 반환하는 함수들이 있다.

과거 오류전달 기법이나 널을 반환하는 것이 가능하지 않거나 실용적이지 못해 매직값을 반환하는 경우가 있다.

그러나 일반적으로 매직값을 반환하면 예측을 벗어날 위험이 있으므로 사용하지 않는 것이 가장 바람직하다.

책에서 주어진 예제처럼 작은 문제가 큰 문제를 만들 수 있기 때문에 매직값을 사용하지 않는 것이 좋다.

다른 사람은 해당 함수를 활용할 때 매직값을 반환할지 모르기 때문에 위험할 수 있다.

널, 옵셔널 또는 오류를 반환하라

3장에서 다룬 코드계약에는 명백한 항목과 세부 조항이 포함된다는 점을 살펴봤다.

매직값의 문제점은 호출하는 쪽에서 함수 계약의 세부 조항을 알아야 한다는 점이다.

따라서 값이 없을 수 있는 경우 이것이 코드 계약의 명백한 부분에서 확인할 수 있도록 하는 것이 좋다.

가능한 널, 옵셔널을 반환하고 이를 통해 호출하는 쪽에서 값이 없을 수 있다는 점을 인지할 수 있게 헤야한다.

때때로 매직값이 우연히 발생할 수 있다.

개발자가 자신의 코드에 주어지는 모든 입력과 이러한 입력값들이 어떤 영향을 미칠 수 있을지에 대해 충분히 생각하지 않을 때도 매직값이 반환될 수 있다.

책에서 등장하는 예제와 같이 함수를 짤 경우 예외에 대한 생각을 하지 않는 경우 매직값이 반환되게 된다.

따라서 널값을 사용하는 것이 더 낫고, 호출하는 쪽에선 입력에 따른 값이 없을 수 있음을 알아야 한다.

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

널값이나 옵셔널을 반환하는 대신 널 객체 패턴을 사용할 수 있다.

널값대신 널 객체 패턴을 사용하는 이유는 널값을 반환하는 대신 유효한 값이 반환되어 그 이후에 실행되는 로직에서 널값으로 인해 시스템에 피해가 가지 않도록 하기 위함이다.

4장에도 잠깐 등장했지만 경고의 의미였다.

하지만 적절한 사용은 오히려 유리하게 작용함을 알려준다.

빈 컬렉션을 반환하면 코드가 개선될 수 있다.

책에서 소개하는 예제는 간단하게 null검사문과 NEP의 경우를 줄이기 위해 클래스에서 빈 문자열을 반환, 호출하는 쪽에선 합치기를 할 경우 조금 더 좋은 코드로 작동한다는 것이다.

하지만 복잡한 상황에서는 위험성이 커진다.

빈 문자열을 반환하는 것도 때로는 문제가 될 수 있다.

간단하게 문자의 역할에 따른 책임이 달라짐을 의미한다.

문자자체의 역할로 위에서 언급한 클래스의 특정 문자열을 추출 후 합치기 같은 로직에선 유리하게 작용하나 클래스전체를 돌며 의미가 있는 문자열 ex) ID같은 문자열에 null이 아닌 빈 문자열을 리턴할 경우이다.

이런 경우엔 null을 반환하는 쪽이 훨씬 유리하다.

더 복잡한 널 객체는 예측을 벗어날 수 있다.

앞선 문자 자체의 기능과 ID의 기능을 좀 더 깊게 생각해서 언어 자체의 Null과 비어있는 상태를 완벽하게 이해하는 것이 중요하다.

책에서 설명하는 비어있는 박스를 판매하는 일이 생길 수 있기 때문이다.

따라서 함수를 호출할 때 널 객체 패턴을 사용하는 것은 본질적으로 빈 상자를 파는 것과 같다.

널 객체 구현은 예상을 벗어나는 동작을 유행할 수 있다.

구현이 좀 더 심화된다면 추상화 계층에서 인터페이스의 상속을 받은 널 객체는 해당 인터페이스의 기능을 전부 무시해야한다.

하지만 반환 값이 있어야 하는 함수의 경우 0을 반환해야 하는 경우나 함수 자체의 반환 값을 ?을 사용하는 방법도 있지만 후자는 전부 고쳐야 하고.. null반환과 다른 점이 없다..

따라서 리터럴 default값을 넣는 경우가 많은데 이는 예상하지 못한 동작을 수행할 수 있다는 점이다.

여기서 생각나는 문제점은 인터페이스 기능이 추가될 때 마다 해당 널 객체는 점점 비대해지며 해당 오류를 예측하기 더욱 어려워질 것이라는..

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

부수 효과(Side Effect)란, 어떤 함수의 호출이 함수 외부에 초래한 상태 변화를 의미한다.

가장 많이 문제되는 싱글톤패턴, 전역변수등의 함수 외부에 크게 영향을 주는 경우 프로그램 전체에 부수적인 효과로 무너지는..

책에서 소개하는 일반적인 부수 효과 유형은 다음과 같다.

  • 사용자에게 출력 표시
  • 파일이나 데이터베이스에 무언가를 저장
  • 다른 시스템을 호출하여 네트워크 트래픽 발생
  • 캐시 업데이트 혹은 무효화

부수효과는 소프트웨어 작성 시 불가피 한 부분이다.. 코드의 일부에서는 일부분 부수 효과는 있어야 한다는 것을 의미한다.

분명하고 의도적인 부수 효과는 괜찮다

예제에서 주어지는 코드처럼 분명하고 의도적인 부수 효과로 함수로 메세지를 전달하고 해당 메세지로 캔버스를 업데이트한다.

이 처럼 의도적인 부수효과는 괜찮지만 예기치 않은 부수 효과는 문제가 된다.

예기치 않은 부수 효과는 문제가 될 수 있다

예상하지 못한 부수 효과로 getPixel()함수의 캔버스를 다시 그리는 동작(redraw())을 예로 든다.

비용이 많이 드는 함수이기 때문에 만약 Update에서 getPixel를 연속으로 호출하게 되면 화면이 매우 깜빡일 것이다.

예제에서 보여주는 예시처럼 다중 쓰레드 코드의 버그로 종종 일어나는 문제이다.

접근을 lock으로 막아두는 것이 아니라면 잘못된 데이터 값을 취할 수 있기 때문이다.

다중 쓰레드 문제와 관련된 버그는 디버깅과 테스트가 어렵기로 악명높다.

부수 효과를 피하거나 그 사실을 분명하게 하라

앞서 다룬 부수효과는 소프트웨어에서 빠질 수 없는 부분이긴 하다.

따라서 적절성 여부를 깊이 따져보고 정말 필요한 코드인지 파악해야한다.

그럼에도 가장 좋은 방법은 애초에 부수 효과를 일으키지 않는 것이다.

따라서 함수명에 부수효과에 대한 예측이 가능하도록 작성하는 것이 중요

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

함수내에서 입력 매개변수를 수정하는 것은 코드 및 버그의 흔한 원인이 될 수 있다.

입력 매개변수를 수정하면 버그를 초래할 수 있다.

책에서 소개하는 예제와 같이 객체를 다른 함수의 입력으로 넘기는 것은 책에 무슨 짓을 할지 넘겨주는 쪽에서는 알 수 없다.

변경하기 전에 복사하리

얇은 복사가 아닌 깊은 복사를 의미한다.

값을 변경하고 싶다면 얇은 복사를 통해 같은 값을 참조하여 변경하는 것이 아닌 새로운 메모리를 할당하여 예기치 못한 동작이나 버그를 방지하는 것

성능에 관련한 문제가 생길 수 있다.

코딩을 많이 해봤다고 생각하진 않지만 점점 경험하면서 느끼는 점은 책에서 말한 방어적이어야 한다는 부분이 조금 이해가 되는 것 같다.

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

개발자가 코드를 살펴볼 때 주로 인식하게 되는 코드 계약의 명백한 부분이 누락되게 되면 예기치 못한 결과를 초래할 수 있다.

중요한 입력이 누락되었을 때 아무것도 하지 않으면 놀랄 수 있다.

대략적인 이해는 가지만.. 코드나 내용을 다시 읽어도 눈에 들어오지 않는다.. ㅠ

중요한 입력은 필수 항목으로 만들라

호출하는 쪽에서 호출하기 전에 널값여부를 확인할 필요가 없다.

앞선 내용에서 이해가 안된 부분이 함수 내부에서 입력에 대한 범위가 커서 생긴 문제라는 걸 알았다.

null값도 넣을 수 있기 때문에 해당 함수에선 이중 처리를 통해 아무것도 하지 않았지만 해결책에선 입력을 강제하고 고지사항을 항상 표시하도록 만들었다.

그렇다면 애초에 null값인 경우 default값을 출력하도록 만들면 되는게 아닌지 궁금해진다..

중복성이나 가독성, 책임이 많아지는 문제, 코드 계약에 관련된 문제일 수 있을지..

미래를 대비한 열거형 처리

지금까지 우리가 작성한 코드를 사용하는 쪽에서 코드가 수행하는 일이나 반환값이 그들의 예상을 벗어나지 않도록 하는 데 초점을 맞추었다.

우리의 코드에 의존하는 코드가 올바르고 버그가 없도록 하기 위함이었다.

책에선 열거형에 대한 다양한 개발자의 의견을 말해준다.

열거형을 자주 사용하는 입장으로 별다른 생각을 안해봤지만 저자는 좋고 나쁨을 떠나서 사용할 가능성에 대해 말한다.

미래에 추가될 수 있는 열것값을 암묵적으로 처리하는 것은 문제가 될 수 있다.

예제에서 주어지는 예시처럼 열거형은 이후에 추가되는 값들에 대한 문제가 발생할 수 있다.

미래에 값이 추가된다고 해서 해당 값에 의존되는 코드들이 전부 에러나 경고를 주지 않기 때문에 잠재적인 문제가 생길 수 있고 이는 큰 문제로 굴러올 수 있기 때문이다.

모든 경우를 처리하는 스위치문을 사용하라

맨 처음 예제를 보자마자 스위치문으로 처리하면 크게 문제 없지 않나..? 라고 생각했던 해결책이 바로 나온다.

if문으로 명시적이 아닌 암시적인 방법으로 처리하기 때문이다.

이를 위한 해결방법은 모든 경우를 다 처리하는 스위치 문을 사용하는 것이다.

예제에서는 비검사 예외를 발생하기 때문에 단위테스트에서 오류를 잡을 수 있다.

기본 케이스를 주의하라

else문과 마찬가지로 스위치문의 default케이스를 주의하라

암시적인 방법이기 때문에 잠재적으로 예기치 않은 문제와 버그가 발생할 수 있다.

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

예상에 벗어나는 코드를 방지하기 위한 코드 품질 향상 노력에 반대하는 주장을 하는 사람들이 가끔 있다.

테스트가 이러한 문제를 잡아낼 것이기 때문에 이런 노력은 시간 낭비라는 것이다.

이것은 현실에서는 별로 효과가 없는 다소 이상주의적인 주장이다..!

멘토님이나 다른 분들이 많이 이야기 하시는 테스트의 중요성을 다시한번 생각하게 되는 것 같다..

사실 아직 테스트의 중요성을 많이 못 느끼는 참인데 장의 마무리에서 저자가 이렇게 까지 확정지어서 이야기하니 테스트 코드를 몇번 만들어보는 경험이 필요할 것 같다.

느낀점

1장 2장에서 언급된 코드의 품질에 관련된 내용을 다 풀어서 설명해주니 이해도 잘되고 5장과 마찬가지로 당장 활용해보기도 좋고 적어놓고 다시 읽어보기도 매우 좋은 것 같다.

널 객체 패턴을 적절하게 사용하는 것이 좋다고 했는데 단점을 많이 알려줘서 그냥 쓰지말라고 경고하는 것 같은 기분이다..

예상하지 못하는 동작에 대해 많이 설명해줘서 좋았다.

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

비합리적이거나 애매한 가정에 기반해서 코드가 작성되거나 다른 개발자가 잘못된 일을 하는 것을 막지 못할 때 코드는 오용되기 쉽다.

논의 사항

시간 부분에서 강력한 외부라이브러리에 대해 언급하는데 실제 회사 프로젝트에 외부 라이브러리가 많이 들어가나요?

책의 내용 및 정리

핵심 주제

  • 코드 오남용으로 인해 버그가 발생하는 방식
  • 코드를 오용하기 쉬운 흔한 방식
  • 코드를 오용하기 어렵게 만드는 기술

코드를 잘못 사용할 수 있는 몇 가지 일반적인 경우는 다음과 같다.

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

오용하기 어려움에 대한 일상적인 예제를 봤을 때 C#의 인터페이스, where키워드가 생각났다.

앞서 다룬 예외의 부모예외로 잡아서 처리하는 것이 좋지 않는다는 맥락과 일치하는 것 같다.

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

객체가 생성된 후에 변경될 수 없다면 이 객체는 불변(불가변성)이다.

불변성이 적합한 이유를 이해하기 위해선 그 반대인 가변객체의 문제를 생각해보면 된다.

  1. 설정함수를 같는 가변 클래스에서 어떻게 잘못된 설정이 쉽게 이루지는 문제
  2. 입력 매개변수를 변경하는 함수가 어떻게 예상을 벗어나는 동작을 초래되는 문제
  3. 가변 객체는 추론하기 어렵다.
  4. 가변 객체는 다중 스레드에서 문제가 발생할 수 있다.

가변 객체는 추론하기 어렵다.

간단하게 배달음식의 봉인씰을 생각하면 된다.

치킨을 시켰다면 치킨이 도착해야 하는 앞서 다룬 에제중에 비슷한 예가 있었는데 예측가능한 객체는 불변성을 가져야 한다.

가변 객체는 다중 스레드에서 문제가 발생할 수 있다.

멀티 스레드에서 가장 많이 발생하는 문제라고 할 수 있다.

C#의 lock기능을 활용하면 좋지 않을까..?

하지만 객체를 불변으로 만드는 것은 항상 가능하지 않고, 적절한 것도 아니다..

필연적으로 만들어야 하는 상황이 오기 때문에 기본적으로 불변객체를 만들되 필요한 곳에서만 가변적으로 관리하는 것이 중요..

가변 클래스는 오용되기 쉽다

클래스를 가변적으로 만드는 가장 일반적인 방법은 Setter함수를 제공하는 것이다.

책에서는 TextOptions클래스의 문제점을 지적한다.

다른 가변클래스에서 해당 setter함수에 접근하여 값을 변경하기 때문이다.

처음에는 setter로 매핑해서 디버깅의 방어도 되고, 온전히 messageBox.renderTitle()메서드의 문제라고 생각했지만 책에서는 방어하지 못한 즉, 오용되게 만든 책임을 찾는다.

멘토링에서 이 책에 대한 이야기를 잠깐 했는데 이 책은 정말 회사의 프로젝트, 큰 규모의 프로젝트를 기준으로 설명하기 때문에(이러한 기준으로 설명하는게 당연하지만..)관점을 다르게 해야한다는 이야기를 듣고나니 이해가 된다.

객체를 생성할 때만 값을 할당하라

생성시에만 값을 할당하는 방법은 매우 유용하다..

c#에서는 대부분 readonly키워드를 사용하여 값을 초기화하거나 프로퍼티를 get만 열어서 값을 열어둔다.

하지만 어느 순간 재정의가 필요한 순간이 올 수 있는데 그땐, 빌더 패턴이나 쓰기 시 복사 패턴을 사용하면 유용하다.

불변성에 대한 디자인 패턴을 사용하라
  • 빌더 패턴

가변 클래스는 오용하기 쉽기 때문에 불변의 객체를 만들기 위해 필드를 const, readonly로 둬서 생성시에만 초기화가 가능하도록 하는 것이 좋다.

빌더 패턴은 클래스를 두개로 나누어 빌더 클래스, 불변 클래스로 구분한다.

값을 설정하기 위한 빌더 클래스는 가변적이지만 빌더 클래스를 호출한 클래스의 인스턴스는 불변적 속성을 가진다.

구현 내용

  • 쓰기 시 복사 패턴

이 패턴의 핵심은 앞서 다룬 messageBox.renderTitle()의 객체의 필드 값을 건드리기 때문에 애초에 새로운 불변 클래스를 생성하여 반환하는 것이다.

애초에 생성 시 기본 생성자에선 필수로 필요한 필드만 초기화를 하고 가변되어 생성될 필드는 null 초기화한다.

이후 메서드를 통해 사이즈나 폰트를 매개변수로 넘겨 새로운 객체를 반환하는 것

원래 생성한 객체는 불변성을 유지할 수 있게 된다.

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

앞선 조언들을 따라서 불변성의 이점을 지키더라도 무심코 클래스가 가변적으로 될 수 있는 미묘한 경우를 간과하기 쉽다.

이는 일반적으로 깊은 가변성때문이다.

깊은 가변성은 오용을 초래할 수 있다

예제에서 처럼 참조로 인한 깊은 가변성이 발생할 수 있다.

TextOptions클래스는 참조를 가지고 있기 때문에 다른 코드에서 해당 참조를 가지고 있다면 변경에 따른 영향을 받을 수 있다.

실제로 경험해본 문제라 찾기도 힘들었고 참조에 대한 문재점도 다시 생각해본 예제이다.

두가지 시나리오지만 결국 동일한 참조를 가지고 있다는 점이 핵심이다.

방어적으로 복사하라

동일한 참조를 가지는 것이 의도되었다면 유연한 기능이지만 이 처럼 오용될 수 있다면 치명적이다.

따라서 얕은 복사가 아닌 깊은 복사를 통해 유일성을 확보한다.

객체를 생성할 때 생성자로 리스트에 대한 참조를 받지만 생성 단계에서 copy하여 가지고 있고, getter을 통한 접근은 새로운 객체를 생성하여 반환한다.

getter로 접근할 때 마다 새로운 객체가 생성되는 점에서 약간 위험할 수 있다는 생각이 든다..

readonly라고 해서 불변성이 보장된다는 것이 아니라는것에 명심해야 한다.

불변적 자료구조를 사용하라

앞서 말한 복사본을 생성하지 않아도 되는 경우가 자료구조 자체가 불변적이라면 객체자체를 전달할 수 있다.

따라서 불변적인 자료구조를 사용하는 것은 클래스가 깊은 불변성을 갖도록 보장하기 좋은 방법 중 하나다.

지나치게 일반적인 대이터 유형을 피해라

정수, 문자열 및 리스트 같은 간단한 데이터 유형은 코드의 기본적인 구성 요소 중 하나다.

쉽게 사용가능하고 다재다능하지만 정수나 리스트와 같은 유형으로 표현이 가능하다고 해서 그것이 반드시 좋은 방법이 아니다.

설명이 부족하고 허용하는 범위가 넓을수록 코드 오용은 쉬워진다.

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

이 내용의 핵심은 앞 장에서 설명한 다른 개발자가 읽자마자 알수있어야 한다는 점이다.

위치를 사용할 때 List<int, int>가 아닌 Position이라는 새로운 클래스나 구조체를 사용하는 것 처럼 오용되기 쉬운 일반적인 데이터를 피해야 한다.

함수자체에서 문서나 명명된 인수를 사용할 수 있지만 좋은 방법은 아닌 것 같다..

페어 유형은 오용하기 어렵다

페어 유형자체를 처음 봤다.

튜플과 비슷한 것 같다..?

내용의 핵심은 마찬가지로 문서가 필요하고 여전히 의미를 이해하기 어렵다는 점이다.

전용 유형 사용

새로운 클래스나 구조체를 정의하는 것이 많은 노력이 들어가고 블필요해보일 수 있지만 보기보다 쉽고 버그의 가능성을 줄여준다.

예제와 같이 위도와 경도를 나타내는 클래스를 정의하여 사용

마찬가지로 같은 자료형이라고 Vector를 사용하거나, Position클래스를 다시 사용하는게 아닌 새로 정의하는 것이다.

시간 처리

시간을 다룰 때 코드를 잘못 사용하고 혼동을 일으킬 여지가 굉장히 많다.

  • 절대적인 시간과 상대적인 시간의 표현법
  • 시간의 양을 표현
  • 표준시간대, 윤년등 다양한 시간 개념
정수로 시간을 나타내는 것은 문제가 될 수 있다

시간을 나타낼 때 일반적으로 정수나 큰 정수를 사용한다.

이것으로 한순간을 의미하는 시각과 시간의 양을 표현한다.

  • 순간으로서의 시간은 유닉스 시간(unix epoch)인 1970년 1월 1일 00:00:00 UTC 이후 몇 초로 표현한느 경우가 많다.
  • 양으로서의 시간은 초 혹은 밀리초 단위로 표시할 때가 많다.

정수는 매우 일반적인 유형이기 때문에 시간을 나타내는 데 사용하는 경우 코드가 오용되기 쉽다.

한순간의 시간인지, 아니면 시간의 양인가?

책에서 보여주는 예처럼 매개변수로 시간을 전달할 때 해당 시간이 순간인지 양인지는 정수로 표현하면 알 수 없다.

문서를 작성하는게 되면 세부조항이 늘어나고 좋은 방법이 아니다.

코드에서 사용하는 초만 따져도 millisecond, second, microsecond와 같은 단위도 사용한다.

함수이름이나 매개변수, 주석문으로 방어한다고 해도 여전히 코드를 오용하기 쉽다..

시간대 처리 오류

예제와 같이 같은 시간이라도 다른 표준 시간대로 설정한 경우에는 잘못된 값을 받을 수 있다.

적절한 자료구조를 사용하라

우리가 알 수 있듯이 시간을 다루는 것은 복잡하고 까다로운 일이며 혼란의 여지가 많다.

C#의 Noda..? 관련 링크..

이러한 강력한 외부 라이브러리를 사용하여 알맞은 자료구조를 사용하면 나타내고자하는 바가 명백하다.

물론 필요에 의해 제작할 수 있지만, 대부분 시간에 관한 내장라이브러리나 외부 라이브러리를 활용하자..

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

코드에서 숫자, 문자열, 바이트 스트림과 같은 종료의 데이터를 처리하는 경우가 많다.

데이터는 종종 두 가지 형태로 제공된다.

  • 기본 데이터: 코드에 제공해야 할 데이터, 코드에 이 데이터를 알려주지 않고는 코드가 처리할 방법이 없다.
  • 파생 데이터: 주어진 기본 데이터에 기반해서 코드가 개선할 수 있는 데이터

간단한 예제로 은행계좌시스템에 대변금액과 차변금액은 기본 데이터, 계좌 잔액은 파생데이터로 대변에서 차변을 뺀 금액이다.

기본데이터는 프로그램의 진실의 원천이 된다.

또 다른 진실의 원천은 유효하지 않은 상태를 초래할 수 있다

다른 예제로 Vector3라는 구조체가 x, y, z의 값을 들고 있을 때 x, y, z는 기본 데이터가 되고 이에 대한 방향 벡터를 구한다고 한다면

기본데이터의 연산을 통해 구해야 정확할 것이다.

하지만 해당 값을 매번 계산하는 것이 아닌 데이터에 담아두게 된다면 맞니 않는 값을 갖게 되고 이것이 두개의 진실의 원천을 같는 예이다.

public float magnitude { get { return Mathf.Sqrt(x * x + y * y + z * z); } }
public MyVector normalized => new MyVector(x / magnitude, y / magnitude, z / magnitude);

파생 데이터는 매번 계산하는 것이 바람직하다.

기본 데이터를 유일한 진실의 원천으로 사용하라

예제와 같이 생성자로 불변성을 확보한 필드를 각각 계산하여 파생데이터를 반환한다.

하지만 데이터의 계산에 비용이 많이 드는 경우에는 그 값을 지연계산한 후 결과를 캐싱하는 것이 좋다.

동작과정이 Linq와 비슷하다는 생각이 들었다..

Linq또한 값을 확인 할 때 동작한다고 알고 있는데 연관이 있는지 궁금하다..

마찬가지로 데이터가 불변적이 아니라면 더욱 복잡해지기 때문에 앞서 다룬 객체를 불변적으로 다뤄야 한다는 것 의미한다.

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

진실의 원천은 코드에 제공된 데이터에만 해당되는 것이 아니라 코드에 포함된 논리에도 적용된다.

논리에 대한 진실의 원천이 여러개 있으면 버그를 유발할 수 있다

이런 경우가 있을지 모르겠지만 대규모 프로젝트의 경우에는 발생할 수 있는 문제인 것 같다..

Json파일을 직렬화 역직렬화하는 과정에서 서로 다른 유틸리티를 사용하거나.. CSV를 통한 Read에서 다른 논리를 사용한다면 문제가 될 수 있다.

진실의 원천은 단 하나만 있어야 한다

예제와 같이 동일한 진실의 원천을 가질려면 동일한 클래스를 가지는 것이다.

하위문제를 담당하는 클래스를 상위 수준 문제에서 동일한 로직으로 가지게 된다면 코드는 더욱 견고해진다.

느낀점

코드를 오용하기 어렵게 만드는 방법을 읽고 느낀점은.. 어렵고 불편하게 만들어야 단단하고 가독성이 올라가는 것 같다.

시작할 때 나온 말처럼 결국 가장 빠른길이 가장 느린길이였다는 생각이 드는 파트..

다양한 방법들이 있지만 전부 명백해야 하고 어려운 구조를 가져야 한다.

유일성

8장 코드를 모듈화하라

요구사항이 어떤 식으로든 바뀐다는 점은 확신할 수 있다.

논의 사항

데이터 객체에 대한 생각이 궁금합니다.

저는 데이터객체를 사용하는 것이 바람직하다고 생각은 들지만 만약 소프트웨어의 덩치가 커짐에 따라 데이터 객체가 많이 세분화 되거나 하나의 데이터객체가 점점 커져서 필요 이상의 덩치를 가지게 될 것 같다는 생각이 듭니다.

그대로 진행하게 되면 너무 세분화된 데이터 클래스가 많아서 헷갈리게 되면 결국 문서화로 이어지게 되고 한가지 데이터 객체가 너무 많은 부담을 받아서 문제가 발생한 경우에는 수정하기 어려움도 커질 것 같습니다.

책의 내용 및 정리

핵심 주제

  • 모듈화된 코드의 이점
  • 이상적인 코드 모듈화가 되지 않는 일반적인 방식
  • 코드를 좀 더 모듈화하기 위한 방법

모듈화의 주된 목적 중 하나는 향후에 어떻게 변경되거나 재구성될지 정확히 알지 못한 상태에서 변경과 재구성이 용이한 코드를 작성하는 것이다.

이를 달성하기 위해선 각각의 기능이 코드베이스의 서로 다른 부분에서 구현되어야 한다는 것이다.

이것이 달성된다면 요구 사항 중 하나가 변경된다면, 코드 베이스에서 그 요구 사항이나 기능과 관련된 부분만 수정하면 된다.

즉, 추상화 개념이 핵심이며 하위 문제에 대한 해결책의 자세한 세부사항들이 독립적이고 서로 밀접하게 연관되지 않도록 하는 것(디커플링)으로 귀결된다.

이렇게 하면 적응성이 뛰어난 코드가 될 뿐만 아니라 소프트웨어 시스템에 대한 추론을 쉽게 해준다.

모듈화된 코드는 재사용과 테스트에 더욱 적합하기 때문에 많은 이점을 가진다.

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

일반적으로 클래스는 다른 클래스에 의존한다.

높은 수준의 문제를 하위 문제들로 나눠서 해결하는 방법을 알아봤다.

여기서 하위 문제에 대한 해결책이 항상 하나만 존재하는 것은 아니므로 하위 문제를 재구성할 수 있는 방식으로 코드를 작성하는 것이 유용할 수 있다.

DI(dependency Injection)

DI 정리글

하드 코드화된 의존성은 문제가 될 수 있다

하드코드로 구현된 종속성코드의 예제로 나름 인터페이스를 사용해서 클래스를 구현했지만 생성자에서 의존성을 따로 주입하지 않았기 때문에 코드 자체에 하드코딩이 들어갔다.

이렇게 구현할 일이 있을까 싶다가 앞서 말한 요구사항을 생각하지 않고 급하게 만들면 이렇게 할수도 있겠다는 생각이..

의존성 주입을 사용하라

의존성 주입을 사용하게 되면 일단 디커플링이 이뤄낼 수 있고 다형성을 가져갈 수 있다.

인터페이스로 추상화까지..

또한 의존성 주입을 통한 팩토리함수를 사용하여 입구와 출구를 하나로 만들어서 쉽게 관리할 수 있다.

여기 나온 내용이 저번 아카데미 컨퍼런스에서 끝나고 이야기를 나눴던 내용인 것 같다..

Factory Method Pattern 정리글

그나저나 CreateDefaultNorthAmericaRoutePlanner()메서드 명이 진짜 길다.

필수로 들어가야하는 내용은 잘 들어간 것 같은데 이 정도면 길다고 말할 수 없는건지 궁금하다.

의존성 주입 프레임워크

의존성 주입은 클래스를 좀 더 변경할 수 있게 해주는 장점이 있지만, 생성하는 부분의 코드는 더 복잡해진다는 단점이 있다

이를 해결하기 위해 팩토리 함수를 작성할 수도 있지만 그 또한 대응되는 클래스를 만들어야하고 반복적으로 작성하는 코드가 많아질 수 있다.

제네릭을 사용한다면..?

아직 의존성 주입 프레임워크를 많이 접해보지 못해서 어떤 형태로 존재하는지 모르겠다..

검색해도 안나오다니..

의존성 주임을 염두에 두고 코드를 설계하라

코드를 작성할 때 의존성 주입을 사용할 수 있다는 점을 의식적으로 고려하는 것이 유용할 때가 있다.

코드를 작성하다 보면 나중에는 의존성 주입을 사용하고 싶어도 사용이 거의 불가능한 코드가 짜여질 수 있기 때문이다.

처음부터 정적함수에 의존하는 형태로 작성한 예를 보면 의존성 주입을 넣고 싶어도 넣지 못하는 문제가 발생한다.

하위 문제를 해결할 때 그것이 문제에 대한 유일한 해결책이라고 생각하기 쉽다.

이러한 문제를 해결할 때 쉽게 정적함수로 해결하면 쉽게 해결되기 때문에 별 생각없이 정적 매달림에 빠질 수 있다.

정적함수는 대부분 Util성격의 함수로 구성된 하나의 해결책만 있는 근본적인 하위 문제를 다루는 것이 좋다.

인터페이스에 의존하라

의존할꺼면 인터페이스에 의존해라..!!

앞서 다룬 예제와 같이 모듈화를 위해선 인터페이스를 정말 잘 활용해야 한다.

구체적인 구현에 의존하면 적응성이 제한된다

앞서 다룬 안좋은 예제의 버전 2다..

생성자 자체를 클래스로 가져가기 때문에 (추상클래스 제외)적응성이 제한되게 된다.

가능한 경우 인터페이스에 의존하라

구체적인 구현 클래스에 의존하면 인터페이스를 의존할때 보다 적응성이 제한되는 경우가 많다.

인터페이스는 하위 문제를 해결하기 위한 추상화 계층을 제공하는 것으로 생각할 수 있다.

클래스가 인터페이스를 상속받아 구현하고 인터페이스가 필요한 동작을 정의한다면 이것은 곧 다른 개발자가 해당 인터페이스에 대해 다르게 구현한 클래스를 작성할 수 있다는 것을 강력하게 시사한다.

의존성 역전 원리: 보다 구체적인 구현보다는 추상화에 의존하는 것이 낫다는 생각의 핵심

클래스 상속을 주의하라

대부분 객체지향 프로그래밍 언어의 핵심 기능 중 하나는 한 클래스가 다른 클래스를 상속할 수 있다는 것이다.

클래스 상속은 확실히 쓸모가 있고 때로는 적합한 도구이다.

두 가지 사물이 진정한 is-a 관계를 갖는다면 상속이 적절할 수 있다.

상속은 강력한 도구지만, 몇 가지 단점이 있고 상속이 야기하는 문제가 치명적일 수 있기 때문에 한 클래스가 다른 클래스를 상속하는 코드를 작성하는 것에 대해서는 신중하게 생각해야 한다.

상속을 사용할 수 있는 많은 상황에서 구성을 상속 대신 사용할 수 있다.

클래스를 확장하기보다는 인스턴스를 가지고 있음으써 한 클래스를 다른 클래스로부터 구성할 수 있다.

구성도 멘토님과 추상클래스에 대해서 조언을 구할 때 알려주신 방법으로 상속이 익숙하지 않다면 먼저 해볼 것을 추천해주셨다.

구성은 컴포넌트 패턴과 같은 개념인 것 같다..

Component 정리글

처음 한 10개월 전에 C#을 공부하기 시작하면서 “상속은 좋은 거구나..!”하며 뭔갈 해볼려고 상속을 무작정사용하던 생각이 난다..

지금도 상속을 잘 사용하고 싶지만 어렵지만 반드시 상속을 해야하는게 아니라는 것을 알게된 것 같아서 좋다.. 그래도..

클래스 상속은 문제가 될 수 있다

상속의 예로 많이 나오는 Animal이나 Vehicle예제는 상속의 의미자체는 잘 전달하지만 개발자들이 만나게 되는 함정을 설명하기엔 너무 추상적이다.

상속에서 발생하는 현실적인 문제들을 다룬 예를 살펴보자

쉼표로 구분된 정수를 가지고 있는 파일을 열어서 정수를 하나씩 읽어 들이는 클래스를 작성해야 한다고 가정해보자.

이 문제에 대한 하위 문제는 다음과 같다.

  • 파일에서 데이터를 읽는다.
  • 쉼표로 구분된 파일 내용을 개별 문자열로 나눈다.
  • 각 문자열을 정수로 변환한다.

지금보니 하위문제를 추출하는 방법으로 상위문제에 대한 하위문제를 이 처럼 글로 나열하는 방법이 다시한번 좋다는 걸 알았다.

상속에서 가장 강조되는 부분은 확장의 개념이다.

상속은 추상화 계층에 방해가 될 수 있다.

한 클래스가 다른 클래스를 확장하면 슈퍼클래스의 모든 기능을 상속한다.

이러한 기능은 유용할 때가 있지만, 원하는 것보다 더 많은 기능을 도출할 수도 있다.

이로 인해 추상화 계층은 복잡해지고 구현 세부 정보가 드러날 수 있다.

물론 추상화 클래스를 잘 설계하여 상속 받아서 만든다면 문제가 세부정보를 가릴 수 있다.

하지만 요구사항이 변동되거나 데이터를 예측하기에 어렵고, 순수한 추상클래스를 만들기 어렵다.

상속은 적응성 높은 코드의 작성을 어렵게 만들 수 있다.

요구사항이 변동되어 쉼표뿐만 아니라 세미콜론으로 구분된 값도 읽을 수 있어야 한다고 가정해보자.

그런데 이미 세미콜론으로 구분된 값을 읽을 수 있는클래스가 있다는 것을 알게 됐다.

따라서 쉼표구분클래스 대신 세미콜론클래스를 상속하도록 바꾸면 될 것 같지만 그렇지 않다.

유일한 해결방법은 새 클래스를 만들어 상속받아 구현하는 것이다.

하지만 만들고 본다면 중복되는 코드들이 눈에 보일 것이고.. 유지보수 비용과 버그 발생 가능성을 높이기 때문에 바람직하지 않다.

구성을 사용하라

상속을 원래 사용한 동기는 쉼표로 구분하여 읽는 클래스의 일부 기능을 재사용하는 것이었다.

앞서 발견한 몇 가지 단점이 존재하기 때문에 쉼표 클래스의 기능을 재사용하는 방법으로 구성을 사용하는 것이다.

즉, 클래스를 확장하기보다 해당 클래스에서 인스턴스를 가지고 있음으로써 하나의 클래스를 다른 클래스로부터 구성한다는 것을 의미한다.

8.15예제 정말 깔끔하다ㅠ..

더 간결한 추상화 계층

상속을 사용할 때 서브 클래스는 슈퍼클래스의 모든 기능을 상속하고 외부로 제공한다.

이것은 외부로 노출하고 싶어하지 않는 API까지 만들어진다.

상속 대신 구성을 사용하면 클래스가 전달이나 위임을 사용하여 명시적으로 노출하지 않는 한 해당 클래스의 기능이 노출되지 않는다.

적응성이 높은 코드

여기서 쉼표나 세미콜론에 해당되는 인터페이스를 기능으로 구분하여 제공하면 좀 더 적응성이 높은 코드로 이어진다.

진정한 is-a 관계는 어떤가?

앞 서 두 클래스가 진정한 is-a 관게를 맺고 있다면 상속이 타당할 수 있다고 언급했다.

Car is a Vehicle의 관계는 명확하기 때문에 확장할 수 있다.

하지만 앞서 다룬 예제는 is-a관계라고 보기 어렵다.

FileHandler -> 관계의 파생은 괜찮아 보인다.

내 생각은 기능적인 부분이기 때문에 인터페이스를 통한 (각각의 명확한 기능을 명시) 구성이 제일 좋아보이는데 책에서 말하는대로 답은 없으며 주어진 상황과 작업중인 코드에 따라 다르다.

진정한 is-a관계라도 상속하는 것에 대한 여전히 문제가 될 수 있음을 알아야 한다.

다음과 같이 몇가지 주의할 점이 있다.

  • 취약한 베이스 클래스 문제

서브클래스가 슈퍼클래스에서 상속되고 해당 슈퍼클래스가 나중에 수정되면 서브클래스가 작동하지 않을 수도 있다.

  • 다이아몬드 문제

일부 언어는 두 대 이상의 슈퍼클래스를 확장할 수 있는 다중 상속을 지원한다. 여러 슈퍼 클래스가 동일한 함수의 각각 다른 다른 버전을 제공하는 경우에 어떤 슈퍼클래스로부터 함수를 상속받아야 하는지 모호하기 때문이다.(죽음의 다이아몬드라고 알고 있었다.)

  • 문제가 있는 계층 구조

많은 언어가 다중 상속을 지원하지 않으므로 클래스는 오직 하나의 클래스만 직접 확장할 수 있다. 이를 다중상속이라고 하며 다른 유형의 문제가 발생할 수 있다.

Car라는 클래스와 AirCraft라는 클래스가 있다고할 때 FlyingCar는 어떤 클래스를 상속받아야 할까?

따라서 단일 상속만 가능한 경우 논리적으로 둘 이상의 클래스에 속할때 문제가 발생할 수 있다.

때로는 계층 구조를 피할 수 없는 경우도 있다.

클래스 상속에 숨어 있는 많은 함정을 피하면서 계층 구조를 달성하기 위해선 다음과 같은 것을 할 수 있다.

  • 인터페이스를 사용하여 계층 구조를 정의한다.
  • 구성을 사용하여 코드를 재사용한다.

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

모듈화의 핵심은 목표 중 하나는 요구사항이 변경되면 그 변경과 직접 관련된 코드만 수정한다는 것이다.

이는 단일 개념이 단일 클래스 내에 완전히 포함된 경우라면 이 목표를 달성할 수 있다.

어떤 개념과 관련된 요구 사항이 변경되면 그 개념에 해당하는 단 하나의 클래스만 수정하면 된다.

이것과 반대되는 상황이 하나의 개념이 여러 클래스에 분산되는 경우다.

해당 개념과 관련된 요구 사항을 변경하려면 관련된 클래스를 모두 수정해야 한다.

이 때 개발자가 관련 클래스 중 하나를 잊어버리고 수정하지 않으면 버그가 발생할 수 있다.

클래스가 다른 클래스의 세부 사항에 지나치게 연관되어 있을 때 이런 일이 흔히 일어날 수 있다.

다른 클래스와 지나치게 연관되어 있으면 문제가 될 수 있다

책에서 주어진 예제와 같이 책과 챕터를 나타내는 클래스가 있다고 할 때 책클래스에 챕터 클래스가 구성되어 있다고 해서 구성된 클래스만을 다루는 코드를 책 클래스 내부에 만들게 되면 문제가 될 수 있음을 나타낸다.

자신의 기능에만 충실한 클래스를 만들라

코드 모듈화를 유지하고 한 가지 사항에 대한 변경 사항이 코드의 한 부분만 영향을 미치도록 하기 위해, Book과 Chapter클래스는 가능한 란 자신의 기능에만 충실하도록 해야 한다.

따라서 예제의 경우는 Book클래스에서 Chapter클래스의 함수를 사용하여 값을 반환한다.

즉, 장의 단어를 세는 논리는 챕터클래스가 기능에 충실하게 가지고 있고 책은 해당 컨테이너의 역할로 존재

디미터의 법칙: 한 객체가 다른 객체의 내용이나 구조에 대해 가능한 최대한으로 가정하지 않아야 한다는 소프트웨어 공학 원칙이다.

클래스는 서로에 대한 어느 정도의 지식을 필요로 할 때도 있지만, 가능한 한 이것을 최소화하는 것이 좋을 때가 많다.

이를 통해 코드 모듈화를 유지할 수 있으며 적응성과 유지 관리성을 크게 개선할 수 있다.

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

2장에서 다룬 한 클래스가 대한 책임이 많아질수록 야기될 수 있는 문제점을 살펴보았다.

너무 많은 것들을 한 클래스에 두지 않도록 주의해야 하지만 한 클래스안에 함께 두는 것이 합리적일 때는 그렇게 하는 것의 이점을 놓쳐서도 안 된다.

서로 다른 데이터가 서로 밀접하게 관련되어 있어 그것들이 항상 함께 움직여야 할 때가 있다.

이 경우에는 클래스로 그룹화하는 것이 합리적이다.

이렇게 하면 코드는 여러 항목의 세부사항을 다루는 대신, 그 항목들이 묶여 있는 단일한 클래스가 제공하는 상위 수준의 개념을 다룰 수 있다.

이를 통해 코드는 더욱 모듈화하고 변경된 요구사항을 해당 클래스에서만 처리할 수 있다.

캡슐화되지 않은 데이터는 취급하기 어려울 수 있다

책에서 보여주는 예처럼 displayMessage()함수의 경우 uiSettings의 값을 읽어와서 전달한다.

여기서 생기는 문제점은 새로운 renderType이 필요할 경우 매개변수를 하나 늘려서 UserInterface를 수정해야 하는 것을 의미한다.

모듈화의 핵심은 요구사항에 발맞춰서 해당 부분만 수정해야 하지만, 서로 다른 데이터를 다룰 때 현재 메서드처럼 연관되어 있다면 전부 따라 올라가며 수정을 해줘야 한다.

지금은 하나의 예지만 실제 프로젝트에선 더욱 다양한 값들을 추척해야 할것이다.

책에선 이러한 상황을 세부사항, 세부적인 내용으로 다룬다.

간단한 예시로 택배기사, 웨이터등 캡슐화된 데이터를 전달할 때는 해당 데이터가 뭔지 몰라야한다.

하지만 지금의 형태는 displayMessage()함수는 전달하는 내용을 정확하게 알고 있어야 한다.

관련된 데이터는 객체 또는 클래스로 그룹화하라

TextOptions 캡슐화 클래스를 만들어서 텍스트 사이즈, 폰트 등등을 캡슐화하여 인스턴스로 전달할 수 있다.

이렇게 만들어 displayMessage()함수는 캡슐화된 데이터만 전달해주는 택배기사와 동일하다.

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

간결한 추상화 계층을 가지려면 각 계층의 구현 세부 정보가 유출되지 않아야 한다.

구현 세부 정보가 유추되면 코드의 하위 계층에 대한 정보가 노출될 수 있으며, 향후 수정이나 재설정이 매우 어려워질 수 있다.

코드에서 구현 세부정보를 유출하는 일반적인 형태 중 하나는 해당 세부 정보와 밀접하게 연결된 유형을 반환하는 것이다.

반환 형식에 구헌 세부 사항이 유출될 경우 문제가 될 수 있다

말 그대로 반환 형식에 세부 구현 사항이 유출될 경우 문제가 될 수 있다.

이는 앞서 다룬 참조에도 해당되는 이야기이며 세부 사항은 세부사항으로 만든 이유가 있기 때문이다.

추상화 계층에 적합한 유형을 반환해라

외부로 노출할 개념을 최소화하는 유형을 새로 정의해 사용하면 좀 더 모듈화된 코드와 간결한 추상화 계층을 얻을 수 있다.

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

구현 세부 정보가 유출될 수 있는 또 다른 일반적인 경우는 예외를 발생할 때다.

예외 처리 시 구현 세부 사항이 유출되면 문제가 될 수 있다

비 검사 예외의 핵심 기능 중 하나는 예외가 발생하는 위치나 시기, 코드가 어디에서 그 예외를 처리하는지 등에 대한 그 어떠한 것도 컴파일러에 의해 강제되지 않는다는 것이다.

인터페이스를 구현하는 클래스가 반드시 인터페이스가 규정하는 오류만 발생시켜야만 하는 것은 아니다.

추상화 계층에 적절한 예외를 만들라

구현 세부 사항의 유츨을 방지하기 위해 코드의 각 계층은 주어진 추상화 계층을 반영하는 오류 유형만을 드러내는 것이 이상적이다.

느낀점

모듈화에 대한 지식이 뽝..!

가장 신경써서 읽은 챕터중에 하나로 뒷 부분의 8.7 예외부분은 이해하기 어려웠고 중간에 서버관련 반환값도 조금 어려웠다.

다른 예제는 직관적이고 이미 지키고 있거나 비슷한 예가 많아서 이해가 더 잘된 것 같다.

오용과 마찬가지로 전체적으로 말하고자 하는 줄기는 비슷한 것 같다.

앞의 내용들을 좀 더 세분화하여 예제로 보여주는 것 같았다.

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

간결한 추상화 계층을 만들고 코드를 모듈화하면 하위 문제의 해결책이 서로 느슨하게 결합하는 코드로 나뉘어지는 경향이 있다.
이렇게 되면 보통 코드를 재사용하고 일반화하기 휠씬 더 쉽고 안전해진다.

논의사항

실제 대형 프로젝트에는 전혀 전역상태가 없는 것인지 궁금합니다.

흔히 말하는 싱글톤 == 안티패턴의 경우처럼 몇 백명이 모이면 확실히 없어야 안전하다는 생각은 들지만 진짜로 없는지 우회하거나 제한을 거는지 궁금합니다아

책의 내용 및 정리

핵심 주제

  • 안전하게 재사용할 수 있는 코드 작성 방법
  • 다양한 문제를 해결하기 위해 일반화된 코드를 작성하는 방법

2장은 상위수준으 문제를 해결하기 위해 하위 문제로 세분화하는 방법에 대해 알아봤다.

프로젝트를 연이어 하다 보면 종종 동일한 하위문제들이 계속해서 나오는 것을 발견한다.

다른 개발자가 이미 주어진 하위 문제를 해결했다면, 해당 문제에 대한 해결책을 재사용하는 것이 바람직 하다.

그렇다고 해서 항상 재사용할 수 있는 것은 아니다.

다른 개발자가 구현한 해결책이 자신의 사례와 맞지 않는 가정을 하거나, 그 해결책에 자신에게 필요 없는 다른 기능과 함께 구성된 경우 이러한 문제가 발생할 수 있다.

가정을 주의하라

코드를 작성 시 가정을 하면 코드가 더 단순해지거나, 더 효율적으로 되거나, 둘 다일 수도 있다.

그러나 이러한 가정으로 인해 코드가 더 취약해지고 활용도가 낮아져 재사용성하기에 안전하지 않을 수 있다.

이러한 점을 고려할 때, 코드 작성 시 가정을 하기 전에 그 가정으로 초래될 비용과 이점을 생각해봐야 한다.

코드 단순화 또는 효율성의 명백한 이득이 미미하다면 늘어난 취약성으로 인한 비영이 장점을 능가하기 때문에 가정을 하지 않는 것이 최선일 수 있다.

범용적인 클래스를 만들고 싶어서 여러가지 가정을 하거나 예외를 생각하지 않고 짧고 간결하게 짜고 싶은 생각에서 오는 문제..

가정은 코드 재사용 시 버그를 초래할 수 있다

가정하여 상황에 맞게 코드를 제작하고 동작에 문제가 없었지만 코드를 재사용한다면 이야기가 달라진다.

코드를 일반화, 재사용하기 위해선 낮은 수준의 추상적인 문제들로 구성되어야 안전하고 쉽다.

하지만 예제와 같은 코드는 재사용하기 전에 문제가 발생하지 않았다고 해서 좋은 코드가 아니다.

요구사항이 변경되거나, 다른 쪽에서 재사용될 때 발생할 수 있는 문제이다.

불필요한 가정을 피하라

따라서 비용과 이익을 따져가며 가정을 하는 것 자체가 버그를 불러올 수 있다는 점을 알아야 한다.

섣부른 최적화

섣부른 최적화를 피하려는 열망은 소프트웨어 공학과 컴퓨터 과학 내에서 잘 정립된 이론이다.

최적화를 거듭할 수록 생산성은 떨어지고 생산성이 올라갈수록 최적화와 거리가 멀어진다.

따라서 큰 효과가 없는 코드 최적화를 하느라 애쓰기보다는 코드를 읽을 수 있고, 유지보수 가능하며, 견고하게 만드는 데 집중하는 것이 좋다.

프로그램에 의심될 만한 깃발들을 세워두고 해당 기준이 넘거나 큰 효과를 볼 수 있을 때 최적화 작업을 해도 무방하다.

가정이 필요하면 강제적으로 하라

때로는 가정이 필요하거나 가정으로 얻는 이득이 비용을 초과할 정도로 코드가 단순해질 수 있다.

코드에 가정이 있을 때, 다른 개발자들이 그것을 여전히 모를 수 있다는 사실을 염두에 두어야 한다.

그래서 우리가 상정한 가정으로 인해 다른 개발자들이 무의식중에 곤란을 겪지 않도록 하기 위해 가정을 강제적으로 시행해야 한다.

  • 가정이 ‘깨지지 않게’ 만들라

가정이 깨지면 컴파일 되지 않는 방식으로 코드를 작성할 수 있다면 가정이 항상 유지될 수 있다.

  • 오류 전달 기술을 사용하라

가정이 깨는 것이 불가능하게 만들 수 없는 경우에는 오류를 감지하고 오류 전달 기술을 사용하여 신속하게 실패하도록 코드를 작성할 수 있다.

전역 상태를 주의하라

전역변수는 프로그램 내의 모든 콘텍스트에 영향을 미치기 때문에 전역변수를 사용할 때는 누구도 해당 코드를 다른 목적으로 재사용하지 않을 것이라는 암묵적인 가정을 전제한다.

이전 절에서 봤듯이 가정에는 비용이 수반된다.

전역 상태는 코드를 매우 취약하게 만들고 재사용하기도 안전하지 않기 때문에 일반적으로 이점보다 비용이 더 크다.

전역 상태를 갖는 코드는 재사용하기에 안전하지 않을 수 있다

어떤 상태에 대해 프로그램의 여러 부분이 공유하고 접근할 필요가 있을 때 이것을 전역변수에 넣고 싶은 마음이 들 수 있다.

이렇게 하면 코드의 어느 부분이라도 그 상태에 접근하기가 아주 쉽다.

그러나 방금 언급했듯이 이렇게 하면 코드를 재사용하는 것이 안전하지 않을 수 있다.

두개의 코드가 동일한 전역 상태를 읽고 수정한다면 의도하지 않은 결과를 초래한다..

공유 상태에 의존성 주입하라

의존성 주입을 통한 객체의 공유는 같은 장바구니를 공유한다.

이렇게 의존성 주입을 사용하여 공유한다면 재사용성 또한 올라간다.

전역상태를 의존성주입 형태로 관리하는게 귀찮고 간편한 전역을 사용하고 싶어할 수 있다.

그렇지만 재사용성 측면과 다른 개발자와 코드계약에 있어서 안전하지 않고 쉽게 사이드 이펙트를 유발한다.

프로그램간의 서로 다른 상태를 공유해야 할 경우, 의존성 주입을 사용해 보다 통제된 방식으로 수행하는 것이 안전하다.

읽으면서 다양한 생각을 해봤는데 게임의 경우 싱글톤을 사용하여 전체적인 게임을 관리하는 매니저를 자주 사용하게 된다.

게임의 흐름을 관리하기 위해선 내 생각에 필수적인것 까진 아니더라도 퍼포먼스나 개발 속도에선 매우 좋은 방법이라고 생각한다.

인디게임이나 적은 규모의 팀이라면 적극 사용할 것 이다.

물론 모든 공유를 매니저로 처리하는 것이 아닌 공유되는 인벤토리같은 기능은 위와 같은 방법을 사용하는 방법이 있다.

책에서 말하는 것처럼 장점과 이점을 항상 생각하고 제한적으로 만든다면 입구나 출구를 하나로 둘 수 있기 때문에 관리에 더욱 용이해진다.

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

합리적인 기본값은 사용자 친화적인 소프트웨어를 만들기 위한 좋은 방법이다.

워드의 예로 사용자의 편리를 위한 기본값은 유용하다. 생성자의 오버로딩, 선택적 매개변수의 예이다.

기본값을 제공하려면 종종 다음과 같은 두 가지 가정이 필요하다.

  • 어떤 기본값이 합리적인지
  • 더 상위 계층의 코드는 기본값을 받든지 명시적으로 설정된 값을 받든지 상관하지 않는다.
낮은 층위의 코드의 기본 반환값은 재사용성을 해칠 수 있다

낮은 층위의 코드는 근본적인 하위 문제 해결에 더 많이 사용되고 따라서 상위 계층의 문제를 해결하기 위해 재사용될 가능성이 높다.

즉, 하위 문제를 해결하기 위해 생각한 가정이 많은 상위 계층에 양향을 미친다.

상위 수준의 코드에서 기본값을 제공하라

간단한 방법으로 하위 수준에서는 null을 반환하고 상위 수준에서 해결하는 것이다.

기본값 또한 캡슐화 클래스로 만들어 의존성 주입을 통해 값을 가지고 있는다.

상위 수준에서는 해당 값이 널이라면 기본값으로 만들어둔 캡슐화 클래스를 반환한다.

C#의 ??를 사용하여 반환 값이 null이라면 디폴트 객체를 반환한다.

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

함수가 데이터 객체나 클래스 내에 포함된 모든 정보가 있어야 하는 경우에는 해당 함수가 객체나 클래스의 인스턴스를 매개변수로 받는 것이 타당하다.

이렇게 하면 함수의 매개변수의 수가 줄어들고 캡슐화된 데이터의 자세한 세부 사항을 처리해야 하는 코드가 필요 없다.

그러나 함수가 한두 가지 정보만 필요로 할 때는 객체나 클래스의 인스턴스를 매개변수로 사용하는 것은 코드의 재사용성을 해칠 수 있다.

필요 이상으로 매개변수를 받는 함수는 재사용하기 어려울 수 있다

예제와 같이 필요 이상의 매개변수를 받게 되면 재사용하기 어려울 수 있음으로 해당 getter함수로 필요한 인자값만 넘기고 필요한 매개변수만 받으면 된다.

제네릭의 사용을 고려하라

클래스는 종종 다른 유형 혹은 클래스의 인스턴스나 참조를 갖는다.

대표적인 예로 리스트 클래스로 문자열과 정수를 저장하기 위한 완전히 다른 별개의 리스트 클래스를 가진다면 상당히 번거로울 것이다.

제네릭,템플릿

다른 클래스를 참조하는 코드를 작성하지만 그 클래스가 어떤 클래스인지 신경 쓰지 않는다면 제네릭의 사용을 고려해야 한다.

제네릭을 사용하면 아주 적은 양의 추가 작업이 있긴 하지만 코드의 일반화가 크게 향상된다.

특정 유형에 의존하면 일반화를 제한한다

RandomizedQueue클래스는 string에 대한 의존도가 높기 때문에 다른 유형을 저장하는 데는 사용하지 못한다.

제네릭을 사용해라

예제에서 제공된 클래스는 제네릭을 도입하기 아주 쉬운 구조이다.

하지만 만약 게임의 요구사항의 변경되어 문자열을 반환하기 전에 출제자의 난이도 또한 올려서 문자열에 단어 몇가지만 삭제된 상태로 반환된다고 한다면 제네릭은 사용할 수 없다.

즉, 해당 자료형에 대한 성격이 확실하다면 도입하기 어렵고 제네릭 자체가 사용하기 어려운 것 같다.

좀 더 생각해봐서 위의 경우처럼 삭제후 반환까지 제네릭을 도입한다면 RandomizedQueue클래스에 반환 전 삭제에 관련된 기능을 다루는 인터페이스를 의존성 주입 후 삭제 부분에서 다룬다면 9.21예제와 같이 사용도 가능하다..

느낀점

가정부분도 생각도 못하고 있던 부분이라 도움이 되었고

공유를 다루기 위해 의존성 주입또한 고질적인 문제점을 해결하기 좋아보인다.

++ 상위 계층에서 기본값을 처리하기도 굳굳

10장 단위 테스트의 원칙

“딱 한줄 고쳤는데요?”

논의 사항

작업을 하다 테스트 코드를 작성하게 되는 계기가 궁금합니다.

ex) 코드를 짜 놓고 보니 문제가 될 것 같아서 테스트 코드를 만들어 둔다 / 테스트 코드를 먼저 짜고 맞게 코드를 작성한다.. 등등

책의 내용 및 정리

핵심 주제

  • 단위 테스트의 기본 사항
  • 좋은 단위 테스트가 되기 위한 조건
  • 테스트 더블
  • 테스트 철학

단위 테스트에 대한 정확한 정의는 없다.

하지만 단위 테스트를 구성하고 있는 것이 무엇인지, 그리고 정확한 정의가 없음에도 일부러 정의를 고안해 내고 자신이 자신이 작성하는 테스트가 그 정의에 정확하게 부합하는지에 대해 너무 집착하지 않는 편이 좋다.

단위 테스트 기초

  • 테스트 중인 코드(code under test): 실제 코드라고도 하고 테스트의 대상이 되는 코드를 의미한다.
  • 테스트 코드(test code): 단위 테스트를 구성하는 코드를 가리킨다.
  • 테스트 케이스(test case): 테스트 코드의 각 파일에는 일반적으로 여러 테스트 케이스가 있고, 각 테스트 케이스는 특정 동작이나 시나리오를 테스트한다.(테스트케이스가 단순한 케이스가 아니라면 세가지로 분류된다.)
    • 준비(arrange): 테스트할 특정 동작을 호출하려면 먼저 몇 가지 설정을 수행해야 하는 경우가 많다. (의존성을 설정, 인스턴스 설정 등)
    • 실행(start): 테스트 중인 동작을 실제로 호출하는 코드를 나타낸다.
    • 단언(assert): 테스트 중인 동작이 실행되고 나면 실제로 올바른 일이 발생했는지 확인한다.
  • 테스트러너(test runner): 이름에서 알 수 있듯이 테스트 러너는 실제로 테스트를 실행하는 도구다.

테스트 코드의 중요성은 다른 책들을 봐도 쉽게 알 수 있다.

요즘 대부분의 전문적인 소프트웨어 개발 환경에서는 거의 모든 실제 코드에 단위 테스트가 동반되는 것으로 생각한다.

단위 테스트라고 해서 어려운 개념이라고 생각했는데 10.1의 그림같은 구조라면 구조가 걱정될 때 비슷하게 제작한 적이 종종 있다.

훌룡한 테스트를 하기 위해서는 테스트만 있다고 되는 것이 아니라, 좋은 테스트가 필요하다.

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

좋은 단위 테스트가 간단해 보일일지 모른다. 실제 코드가 작동하는지 확인하기 위해 테스트 코드를 작성하기만 하면 된다고 생각할 수 있지만.. 수년 동안 많은 개발자가 쉽게 단위 테스트를 잘못된 방식으로 작성해 왔다.

이를 위해 좋은 단위 테스트가 가져야 할 5가지 주요 기능을 정의한다.

  • 훼손의 정확한 감지: 코드가 훼손되면 테스트가 실패한다. 그리고 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.
  • 세부 구현 사항에 독립적: 세부 구현 사항을 변경하더라도 테스트 코드는 변경하지 않는 것이 이상적이다.
  • 잘 설명되는 실패: 코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.
  • 이해할 수 있는 테스트 코드: 다른 개발자들이 테스트 코드가 정확히 무엇을 테스트하기 위한 것이고 테스트가 어떻게 수행되는지 이해할 수 있어야 한다.
  • 쉽고 빠르게 실행: 개발자는 일상 작업 중에 단위 테스트를 자주 실행한다. 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.
훼손의 정확한 감지

단위 테스트의 가장 명확하고 주된 목표는 코드가 훼손되지 않았는지 확인하는 것이다.

즉, 의도된 대로 수행하며 버그가 없다는 것을 확인하는 것이다.

테스트 중인 코드가 어떤 식으로든 훼손되면 컴파일되지 않거나 테스트가 실패해야 한다.

이것은 매우 중요한 두 가지 역할을 수행한다.

  • 코드에 대한 초기 신뢰를 준다

아무리 신중하게 코드를 작성해도 실수는 있기 마련이다.

새로운 코드나 코드 변경 사항과 함께 철저한 테스트 코드를 작성하면 코드가 코드베이스로 병합되기 전에 이러한 실수를 발견하고 수정할 수 있다.

  • 미래의 훼손을 막아준다

어느 시점에 다른 개발자가 코드를 변경또는 병합하는 과정에서 실수로 코드를 훼손할 가능성이 크다.

이것에 대한 유일한 효과적인 방어 방법은 코드가 컴파일을 중지하거나 테스트가 실패하는 것이다.

어떤 것이 고장 났을 때 코드가 컴파일을 멈추도록 하는 것은 불가능하므로 모든 올바른 동작을 테스트를 통해 확인하는 것은 절대적으로 중요하다.

코드 변경으로 인해 잘 돌아가던 기능이 동작하지 않는 것을 회귀라고 한다.

이러한 회귀를 탐지할 목적으로 테스트를 실행하는 것을 회귀 테스트라고 한다.

정확성의 또 다른 측면을 고려하는 것도 중요하다.

테스트 대상 코드가 실제로 훼손된 경우에만 테스트가 실패해야 한다.

위의 내용처럼 당연히 그렇게 될 것 같지만 실제로는 그렇지 않은 경우가 많다.

논리적 오류를 경험한 사람이라면 누구나 알겠지만, 코드가 훼손되면 반드시 실패한다는 것이 반드시 코드가 훼손될 때만 테스트가 실패한다는 것을 의미하는 것은 아니다.

테스트 대상 코드가 정상임에도 불구하고 때로는 통과하고 때로는 실패하는 테스트를 플래키(flakey)라고한다.

이것은 보통 무작위 성, 타이밍 기반 레이스조건, 외부 시스템에 의존하는 등의 테스트의 비결정적동작에 기인한다.

플래키테스트의 위험성을 피하기 위해선 코드에서 어떤 부분이 훼손될 때 그리고 오직 훼손된 경우에만 테스트가 실패하도록 하는 것은 매우 중요하다.

세부 구현 사항에 독립적

일반적으로 개발자가 코드베이스에 가할 수 있는 변경은 두 가지 종류가 있다.

  • 기능적 변화: 이것은 코드가 외부로 보이는 동작을 수행한다. 예를 들어 새로운 기능 추가, 버그 수정, 에러 처리등이 있다.
  • 리팩터링: 이것은 큰 함수를 작은 함수로 분할하거나 재사용하기 쉽도록 일부 유틸리티코드를 다른 파일로 옮기는 등의 코드의 구조적 변화를 의미한다.

첫 번째는 코드를 사용하는 모든 사람에게 영향을 미치므로 변경을 하기 전에 코드를 호출하는 쪽을 신중히 고려해야 한다.

기능적인 변경은 코드의 동작을 수정하기 때문에 테스트도 수정해야 할 것으로 기대하고 예상한다.

두 번째 경우는 코드를 사용하는 사람에게 영향을 미치지 않아야 한다.

하지만 코드의 구조만 변경하는 것인데 그 과정에서 실수로 코드의 동작을 변경하지 않았다고 확신할 수 있을까?

따라서 단위 테스트에서 리팩터링의 문제를 올바르게 발견하기 위해선 세부 구현 사항에는 독립적이어야 한다.

++ 기능 변경과 리팩터링을 같이 하지 말라: 구분하기 어려움

잘 설명되는 실패

테스트에 대한 실패가 발생했을 땐 실패가 발생되는 부분을 알아야한다.

하지만 테스트 실패가 무엇이 잘못됐는지 알려주지 않는다면 그것을 알아내기 위한 많은 시간을 낭비한다.

테스트 실패가 잘 설명되도록 하는 좋은 방법 중 하나는 하나의 테스트 케이스는 한 가지 사항만 검사하고 각 테스트 케이스에 대해 서술적인 이름을 사용하는 것이다.

이해 가능한 테스트 코드

다른 개발자는 새로운 요구 사항을 충족하기 위해 코드의 기능을 의도적으로 수정할 수 있다.

이러한 경우는 의도적이며, 변경을 수행하는 개발자는 변경된 결과가 안전한지 확인한 후에는 새로운 기능을 반영하기 위헤 테스트 코드도 수정해야 한다.

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

이를 위해서는 서로 다른 테스트 케이스가 무엇을 테스트하는지 그리고 어떻게 테스트하는지 이해하고 있어야 한다.

개발들이 테스트 코드를 일종의 사용설명서처럼 사용하기 때문에 이해 가능하게 작성해야 한다.

쉽고 빠른 실행

단위 테스트의 중요한 기능 중 하나는 잘못된 코드가 코드베이스에 병합되는 것을 방지하는 것이다.

따라서 많은 코드베이스에서 관련 테스트를 통괴해야만 병합이 가능한 병합 전 검사를 수행한다.

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

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

앞서 구현 세부사항에 구애받지 않는 것이 중요한 이유에 대해 설명했다.

코드는 퍼블릭 API와 구현 세부 사항으로 나눌 수 있다.

따라서 우리는 퍼블릭 API만을 사용해서 테스트해야 한다는 것을 의미한다.

퍼블릭 API에 초점을 맞추면 세부 구현사항이 아닌 코드 사용자가 궁극적으로 신경 쓸 동작에 집중할 수 밖에 없게 되는데, 세부 사항은 목적을 이루기 위한 수단일 뿐이다.

이렇게 하면 실제로 중요한 사항만 테스트 하는 데 도움이 되며, 테스트 과정에서 구현 세부 사항에 상관없이 테스트에 집중할 수 있다.

중요한 동작이 퍼블릭 API 외부에 있을 수 있다

처음에는 읽고 이해하기 조금 어려웠다.

테스트코드의 형태를 안봐서 그런가 했는데 그림을 보니 이해가 된다.

테스트는 가능하다면 퍼플릭 API를 사용해서만 테스트하는 것을 목표로 해야한다.

하지만 원하는 부수효과를 확인하기 위해서 테스트가 공용 API의 일부가 아닌 종속성과 상호작용해야 하는 경우가 많다.

테스트 더블

의존성을 실제로 사용하는 것에 대안으로 테스트 더블(test double)이 있다.

테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.

세가지 유형의 테스트 더블 목, 스텁, 페이크에 대해 살펴본다.

테스트 더블을 사용하는 이유
  • 테스트 단순화

일부 의존성은 테스트에 사용하기 까다롭고 힘들다.

의존성은 많은 설정이 필요하거나 하위 의존성을 설정해야 할 수도 있다.

이러면 테스트는 복잡하고 구현 세부 사항과 밀집하게 결합될 수 있다.

의존성을 실제로 사용하는 대신 테스트 더블을 사용하면 작업이 단순해진다.

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

일부 의존성은 실제로 부수 효과를 발생한다.

코드의 종속성 중 하나가 실제 서버에 요청을 전송하거나 실제 데이터베이스에 값을 쓰게 되면, 사용자가 비지니스에 중요한 프로세스에 나쁜 결과를 초래할 수 있다.

이러한 상황에서 테스트 더블을 사용하면 외부 세계에 있는 시스템을 테스트의 동작으로부터 보호할 수 있다.

  • 외부로부터 테스트 보호

외부 세계는 비결정적일 수 있다.

다른 시스템이 데이터베이스에 쓴 값을 의존성 코드가 읽는다면 이 값은 시간이 지남에 따라 변경될 수 있다.

이 경우 테스트 결과를 신뢰하기 어려울 수 있다.

반면 테스트 더블은 항상 동일하게 결정적 방식으로 작동하도록 설정할 수 있다.

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

함수가 호출될 때 인수에 제공되는 값을 기록한다.

테스트 대상 코드가 의존성을 통해 제공되는 함수를 호출하는지 검증하기 위해 목을 사용할 수 있다.

따라서 목은 테스트 대상 코드에서 부수 효과를 일으키는 의존성을 시뮬레이션하는 데 가장 유용하다.

스텁

스텁은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션 한다.

이를 통해 테스트 대상 코드는 특정 멤버 함수를 호출하고 특정 멤버 함수를 호출하고 특정 값을 반환하도록 의존성을 시뮬레이션할 수 있다.

그러므로 스텁은 테스트 대상 코드가 의존하는 코드로부터 어떤 값을 받아야 하는 경우 그 의존성을 시뮬레이션 하는 데 유용하다.

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

페이크(fake)는 클래스의 대체 구현체로 테스트에서 안전하게 수용한다.

페이크는 실제 의존성의 공개 API를 정확하게 시뮬레이션하지만 구현은 일반적으로 단순한데, 외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다.

페이크의 요점은 3장에서 다룬 코드 계약에 관련된 내용으로 실제 의존성이 동일하기 때문에 실제 클래스가 특정 입력을 받아들이지 않는다면 페이크도 마찬가지라는 것이다.

따라서 실제 의존성에 대한 코드를 유지보수하는 팀이 일반적으로 페이크 코드도 유지보수해야 하는데, 실제 의존성에 대한 코드 계약이 변경되면 페이크의 코드 계약도 동일하게 변경되어야 하기 때문이다.

목에 대한 의견
  • 목 찬성론자: 개발자는 단위 테스트 코드에서 의존성을 실제로 사용하는 것을 피해야 하고 대신 목을 사용해야 한다고 주장한다.
  • 고전주의자: 목과 스텁은 최소한으로 사용되어야 하고 개발자는 테스트에서 의존성을 실제로 사용하는 것을 최우선으로 해야한다.

목을 사용한 테스트는 상호작용을 하는 반면, 고전주의 방법을 사용한 테스트는 코드의 결과 상태와 의존성을 테스트하는 경향이 있다는 점이다.

이러한 의미에서 목 접근법은 대상 코드를 어떻게 하는가를 확인하는 반면, 고전 주의 접근법은 코드를 실행하는 최종 결과가 무엇인지 확인하는 경향이 있다.

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

책의 앞부분에서도 계속 나온 내용처럼 테스트에 대한 철학과 방법론의 내용처럼 이런 내용에 정답은 없다.

스스로 판단하여 옳다고 생각하는 바를 실천할 자유가 있다.

  • 테스트 주도 개발(test-development, TDD): TDD는 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하는 것을 지지한다.

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

  • 행동 주도 개발(Behavior-driven devlopment, BDD): BDD는 사람마다 조금씩 다른 의미를 가질 수 있지만 이 철학의 핵심은 사용자, 고객, 비즈니스의 관점에서 소프트웨어가 보야야 할 행동을 식별하는 데 집중하는 것이다.

  • 수용 테스트 주도 개발(Acceptance test-driven developnent ATDD): 고객의 관점에서 소프트웨어가 보여줘야 하는 동작을 식별하고 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락 테스트를 만드는 것을 수반한다.

느낀점

뒤의 테스트 더블 부분은 정말 정말 이해하기 어려웠다..

테스트코드자체를 작성해본적이 없어서 뭐라도 해볼려고 UnityTestCode를 작성해보고 공부했지만.. 테스트 더블은 이해하기 어려웠다.

Unity TestRunner

Unity에서는 TestRunner를 통해 TDD를 가져가는 것 같다.

게임쪽에서 한번도 자동으로 테스트되는 경우를 만들어보지 못해서 한번 만들어 보고 싶다.

코드베이스에 병합을 시도하면 병합에 문제가 없다면 -> 테스트 전부 실행, 문제가 없다면 -> 자동 빌드

Game Ci

젠킨스처럼 CI/CD를 위한 github action을 활용한 기능도 있다.

이런 흐름을 가져가면 개발할 맛도 나고 좋을 것 같은데 CI/CD의 대해서 한번 알아보고 간단하게 미니프로젝트에 도입해볼 생각..

11장 단위 테스트의 실제

논의 사항

실제로 책에서 나온 대로 테스트 코드를 짜기 위해서 코드 구조가 변경되는 경우가 많나요?

책의 내용 및 정리

핵심 주제

  • 코드의 모든 동작을 효과적이고 신뢰성 있게 테스트하기
  • 이해하기 쉽고 실패가 잘 설명되는 테스트 코드의 작성
  • 의존성 주입을 사용하여 테스트가 용이한 코드의 작성

이번 장에서 설명할 많은 기법의 동기는 이 특징을 직접 따르는데, 다시 한번 상기하자면 다음과 같다.

  • 코드의 문제를 정확하게 감지한다: 코드에 문제가 있으면 테스트는 실패해야 한다. 그리고 테스트는 코드에 실제로 문제가 있는 경우에만 실패해야 한다.
  • 구현 세부 정보에 구애받지 않는다: 구현 세부 사항을 변경하더라도 테스트 코드에 대한 변경은 필요 없는 것이 이상적이다.
  • 실패는 잘 설명된다: 코드에 문제가 있는 경우 테스트 실패는 문제에 대한 명확한 설명을 제공해야 한다.
  • 테스트 코드가 이해하기 쉽다: 테스트가 정확히 무엇이고 테스트가 어떻게 수행되는지 다른 개발자가 이해할 수 있어야 한다.
  • 테스트를 쉽고 빠르게 실핼할 수 있다: 개발자는 일상적인 작업에서 단위 테스트를 꽤 자주 실행해야 한다.

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

코드를 테스트하는 것은 할 일 목록을 만들어 작업하는 것과 약간 비슷하다.

테스트 대상 코드가 수행하는(코드를 작성하기 전에 테스트를 작성하는 경우라면 수행할 예정인) 작업이 여러 가지 있으며 이 각각의 작업에 대해 테스트 케이스를 따로 작성해야 한다.

클래스에 함수가 2개 있으면 함수마다 하나의 테스트 케이스를 작성하는 식이다.

10장에서 코드가 보이는 중요한 행동을 모두 테스트해야 한다는 것을 살펴봤다.

각 함수들 테스트하는 데만 집중할 때의 문제점은 한 함수가 종종 여러 개의 동작을 수행할 수 있고 한 동작이 여러 함수에 걸쳐 있을 수 있다는 점이다.

함수별로 테스트 케이스를 하나만 작성하면 중요한 동작을 놓칠 수 있다.

함수당 하나의 테스트 케이스만 있으면 적절하지 않을 때가 많다

예제에서 주어지는 퍼블릭 API의 케이스만 테스트하기 때문에 assess함수가 올바른 방식으로 동작하는지 확인하기에 충분치 않다.

각 동작을 테스트하는 데 집중하라

함수와 동작 사이에 일대일로 연결이 안 되는 경우가 많다.

함수 자체를 테스트하는 데만 집중하면, 정작 실제로 신경 써야 할 중요한 동작을 검증하지 않는 테스트 케이스를 작성하기가 매우 쉽다.

테스트 코드의 양이 실제 코드의 양보다 많지 않다면, 모든 동작이 제대로 테스트되고 있지 않음을 나타내는 경고 표시일 수 있다.

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

일반적으로 코드의 공개 API는 public함수로 이루어진다.

클래스는 퍼블릭 함수 외에도 프라이빗 함수를 갖는 것이 일반적인데 이들은 클래스 내에서만 사용할 수 있다.

프라이빗 함수는 구현 세부 사항이며 클래스 외부의 코드가 인지하거나 직접 사용하는 것이 아니다.

때로는 이러한 프라이빗 함수 중 일부를 테스트 코드에서도 접근할 수 있도록 만들어 직접 테스트하고자 할 수 있다.

그러나 이는 좋은 생각이 아닐 때가 많다.

구현 세부 사항과 밀접하게 연관된 테스트가 될 수 있고, 궁극적으로 우리가 신경 써야 하는 코드의 동작을 테스트하지 않을 수 있기 때문이다.

프라이빗 함수를 테스트하는 것은 바람직하지 않을 때가 많다

좋은 단위 테스트는 궁극적으로 중요한 행동을 테스트해야 한다.

이렇게 하면 테스트는 코드의 문제점을 정확하게 감지할 가능성을 극대화하며 구현 세부 사항에 독립적으로 된다.

퍼블릭 API를 통해 테스트하라

구현 세부 사항이 아닌 실제로 중요한 동작을 테스트하도록 개발자에게 가이드를 제공하는 것이다.

프라이빗 함수여야 하는데도 퍼블릭으로 표시되어 외부로 공개되어 있다면, 이 원칙을 깨뜨리는 경고 신호로 봐야 한다.

실용적으로 하라: 테스트를 위해 프라이빗 함수를 퍼블릭으로 만들어 외부로 보이게 하는 것은 대부분의 경우 구현 세부 사항을 테스트한다는 것을 보여주는 경고 신호이다.

비교적 간단한 클래스의 경우 퍼블릭 API만을 사용하여 모든 동작을 테스트하기가 매우 쉽다.

이렇게 하면 코드의 문제점을 보다 정확하게 감지하고 구현 세부 사항에 얽매이지 않는 더 나은 테스트를 수행할 수 있다.

그러나 클래스(또는 코드 단위)가 더 복잡하거나 많은 논리를 포함하면 퍼블릭 API를 통해 모든 동작을 수행하는 것이 까다로울 수 있다.

이 경우는 코드의 추상화 계층이 너무 크다는 것을 의미하기 때문에 코드를 더 작은 단위로 분할하는 것이 유리하다.

코드를 더 작은 단위로 분할하라

퍼블릭 API를 통해 이러한 모든 복잡성과 모든 코너 사례를 테스트하는 것이 상당히 어려워 보인다.

사실 이런 경우는 대부분 추상화 계층이 너무 비대하기 때문에 퍼블릭 API만으로 모든 것을 완벽하게 테스트하기 어려워 보인다.

코드를 테스트하기 위해 프라이빗 함수를 퍼블릭으로 만든다면, 이것은 실제로 신경 써야 하는 행동을 테스트하지 않는다는 경고 신호로 받아들여야 한다.

이미 공개된 함수를 사용해 코드를 테스트 하는 것이 대부분은 더 바람직하다.

이것이 어렵다면 클래스가 너무 크기 때문에 하위문제를 해결하는 더 작은 클래스(또는 단위)로의 분할을 고려해봐야 하는 시점에 이르렀음을 의미한다.

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

주어진 코드에 대해 테스트해야 하는 동작은 여러 가지가 있다.

많은 경우에 각각의 동작을 테스트하려면 약간 다른 시나리오를 설정해야 하므로, 각각의 시나리오는 그에 해당하는 별도의 테스트 케이스로 테스트하는 것이 가장 자연스럽다.

그러나 때로는 하나의 시나리오로 여러 동작을 테스트하도록 만드는 방법이 있다. 하지만 다 좋은 것은 아니다.

여러 동작을 한꺼번에 테스트하면 테스트가 제대로 안될 수 있다

가능한 방법 한가지는 테스트 케이스를 하나만 작성하고 이 안에서 함수의 모든 동작을 한 번에 테스트하는 것이다.

한꺼번에 테스트하면 테스트 케이스가 정확히 무엇을 하고 있는지 이해하기 어렵다는 점이다.

이러한 문제는 이해하기 쉬운 테스트 코드에서 벗어난다.

각 동작은 자체 테스트 케이스에서 테스트하라

휠씬 더 나은 접근법은 잘 명명된 테스트 케이스를 사용하여 각 동작을 개별적으로 테스트하는 것이다.

각 동작을 개별적으로 테스트하고 각 테스트 케이스에 적절한 이름을 사용하면 테스트가 실패할 경우 어떤 동작이 실패했는지 잘 알 수 있다.

매개변수를 사용한 테스트

테스트 프레임워크 중에 매개변수를 사용해 테스트할 수 있는 기능을 제공하는 프레임워크도 있다.

매개변수를 사용한 테스틑 많은 코드를 반복하지 않고도 모든 동작을 테스트할 수 있는 좋은 도구다.

언어마다의 프레임워크가 다 다르기 때문에 프로젝트의 성격에 맞게 장단점을 생각해 적용하는 것이 좋다.

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

테스트 케이스는 의존성을 설정하거나 테스트 데이터 저장소에 값을 채우거나 다른 종류의 상태를 초기화하는 등 어느 정도의 설정이 필요할 때가 있다.

이런 설정을 하려면 시간과 노력이 상당히 많이 들어가고 리소스도 많이 필요할 수 있기 때문에 많은 테스트 프레임워크에서 테스트 케이스 간에 이 설정을 쉽게 공유할 수 있는 기능을 제공한다.

상태 공유는 문제가 될 수 있다

일반적으로 테스트 케이스는 서로 격리되어야 하므로 한 테스트 케이스가 수행하는 모든 조치는 다른 테스트 케이스의 결과에 영향을 미치지 않아야 한다.

테스트 케이스 간에 상태를 공유하고 이 상태가 가변적이면 이 규칙을 실수로 위반하기 쉽다.

상태를 공유하지 않거나 초기화하라

가변적인 상태를 공유하는 데서 오는 문제점을 해결하기 위한 가장 분명한 방법은 애초에 공유하지 않는 것이다.

변경 가능 상태의 공유를 피하기 위해 가능한 또 다른 방법은 10장에서 설명한 테스트 더블을 사용하는 것이다.

전역 상태: 테스트 케이스 간 상태 공유가 테스트 코드를 통해서만 되는 것은 아니라는 점에 유의할 필요가 있다. 테스트 대상 코드가 전역 상태를 유지한다면 테스트 케이스마다 이 전역 상태를 확실하게 초기화해야 한다.

테스트 케이스 간에 가변적인 상태를 공유하는 것은 이상적이지 않다.

피할 수 있다면 일반적으로 공유하지 않는 것이 바람직하다.

피할 수 없다면 각 테스트 케이스 간에 상태를 초기화해야 한다.

이를 통해 한 테스트 케이스가 다른 테스트 케이스에 악영향을 미치지 않도록 해야 한다.

설정 공유는 문제가 될 수 있다

테스트 케이스 간 설정을 공유하는 것은 상태를 공유하는 것만큼 위험해 보이지는 않지만 설정을 공유하면 테스트가 효과적이지 못할 때가 있다.

설정을 공유하는 것은 코드의 반복을 피하는 데는 유용하지만 일반적으로 테스트 케이스에 중요한 값이나 상태는 공유하지 않는 것이 최선이다.

설정을 공유하면 어떤 테스트 케이스가 어떤 특정 항목에 의존하는지 정확하게 추적하는 것은 매우 어려우며, 향후 변경 사항이 발생하면 테스트 케이스가 원래 목적했던 동작을 더 이상 테스트하지 않게 될 수 있다.

중요한 설정은 테스트 케이스 내에서 정의하라

모든 테스트 케이스에 대해 반복해서 설정을 하는 것이 어려워 보일 수 있지만 테스트 케이스가 특정 값이나 설정 상태에 의존한다면 그렇게 하는 것이 더 안전한 경우가 많다.

보통 헬퍼 함수를 사용해 이 작업을 좀 더 쉽게 할 수 있기 때문에 코드를 반복하지 않아도 된다.

테스트 케이스의 결과가 설정값에 직접 영향을 받는 경우 해당 테스트 케이스 내에서 설정하는 것이 가장 좋다.

설정 공유가 적절한 경우

이전 하위 절에서는 테스틑 설정 공유를 주의해야 하는 이유를 설명했지만 그렇다고 테스트 설정을 절대 공유해서는 안된다는 의미는 아니다.

필요하면서도 테스트 케이스의 결과에 직접적인 영향을 미치지 않는 설정이 있을 수 있다.

오히려 이런 경우는 불필요한 코드 반복을 피하고 좀 더 뚜렷한 목적을 갖고 이해하기 쉬워진다.

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

어서션 확인자는 보통 테스트 통과 여부를 최종적으로 결정하기 위한 테스트 케이스내의 코드이다.

테스트 케이스가 실패하면 어서션 확인자는 실패 이유를 설명하는 메세지를 생성한다.

각각의 어서션 확인자는 자신들의 목적에 따라 각자 다른 실패 메세지를 생성한다.

테스트가 실패하는 경우 그 이유가 잘 설명되어야 하는 것은 단위 테스트가 갖는 주요 특징이다.

부적합한 확인자는 테스트 실패를 잘 설명하지 못할 수 있다

코드에 문제가 있을 때 테스트가 확실하게 통과되지 못하도록 하는 것은 필수적이지만, 이것만이 고려 사항의 전부는 아니다.

코드에 정말로 문제가 있을 때에만 테스트가 실패하고 실패의 이유가 잘 설명돼야 한다.

적절한 확인자를 사용하라

대부분의 최신 테스트 어서션 도구는 다양한 확인자를 무수히 많이 가지고 있다.

리스트가 순서에 관련 없이 특정 항목을 포함하고 있는지 검증할 수 있는 확인자가 제공될 수도 있다.

코드에 문제가 있을 때 테스트가 반드시 실패해야 한다는 점 외에도 테스트가 어떻게 실패할지에 대해 생각해보는 것도 중요하다.

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

앞선 장들에서 의존성 주입의 장점에 대해 알아봤는데 테스트의 용이성에서도 크게 향상된다는 장점이 있다.

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

테스트 용이성은 모듈화와 밀접한 관련이 있다.

서로 다른 코드가 느슨하게 결합하고 재설정이 가능하면, 테스트는 훨씬 더 쉬워지는 경향을 띤다.

의존성 주입은 코드를 좀더 모듈화하기 위한 효과적인 기술이며, 따라서 코드의 테스트 용이성을 높이기 위한 효과적인 기술이기도 하다.

테스트에 대한 몇 가지 결론

소프트웨어 테스트는 방대한 주제이고 마지막 두 장에서 다룬 내용은 빙산의 일각에 불과하다.

이 장에서는 개발자가 일상적으로 하는 작업에서 가장 자주 접하는 테스트 수준인 단위 테스트를 살펴봤다.

  • 통합 테스트(integration test)

한 시스템은 일반적으로 여러 구성 요소, 모듈, 하위 시스템으로 구성된다.

이러한 구성 요소와 하위 시스템을 서로 연결하는 프로세스를 통합이라고 한다.

통합 테스트는 이러한 통합이 제대로 작동하는지 확인하기 위한 테스트이다.

  • 종단 간 테스트(end-to-end test): 이 테스트는 처음부터 끝까지 전체 소프트웨어 시스템을 통과하는 여정(또는 작업 흐름)를 테스트한다.

테스트하려는 소프트웨어가 온라인 쇼핑몰이라면, E2E 테스트의 예로는 웹 브라우저를 자동으로 구동하고 사용자가 구매를 완료하는 과정까지 거치면서 구매가 잘 이루어지는지 확인하는 것이다.

여러 수준의 테스트 외에도 다양한 유형의 테스트가 있다.

이들 테스트에 대한 정의는 서로 겹칠 때도 있고, 개발자들이 이들 테스트에 대해 언급할 때 의미하는 바가 항상 일치하는 것은 아니다.

  • 회귀 테스트(regression test): 소프트웨어의 동작이나 기능이 바람직하지 않은 방식으로 변경되지 않았는지 확인하기 위해 정기적으로 수행하는 테스트이다.

  • 골든 테스트(golden test): 특성화 테스트라고도 하며, 일반적으로 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샵으로 저장한 것을 기반으로 한다.

테스트 수행 후 코드가 생성한 출력이 다르면 테스트는 실패한다.

  • 퍼즈 테스트(fuzz test): 퍼즈 테스트는 무작위 값이나 흥미로운 값으로 코드를 호출하고 그들 중 어느 것도 코드의 동작을 멈추지 않는지 점검한다.

소프트웨어를 테스트하기 위해 개발자들이 사용할 수 있는 다양한 기술이 많다.

소프트웨어를 높은 기준으로 작성하고 유지하려면 이러한 기술을 혼용해서 사용해야 하는 경우가 많다.

단위 테스트가 가장 흔한 테스트 유형이지만, 단위 테스트만으로 테스트의 모든 요구 사항을 충족할 수 없기 때문에 다양한 테스트 유형과 수준에 대해 알아보고 새로운 툴과 기술에 대한 최신 정보를 유지하는 것이 좋다.

느낀점

10장과 마찬가지로 아직 테스트 코드의 대해서 미숙하다 보니 검색해보며 많이 찾아봤지만

게임쪽에서 테스트 코드에 대한 정보가 많이 적은 것 같다.

예제나 작성된 파일도 적어서 이해하기 많이 어려웠다.

그래도 테스트 코드에 대한 기본적인 지식이 많이 채워진 느낌..

나중에는 테스트 코드를 작성하는 방법에 대한 책을 읽고 싶다.

댓글남기기