kok202
테스트 주도 개발로 배우는 객체 지향 설계와 실천

2022. 4. 4. 23:37[공부] 독서/테스트 주도 개발로 배우는 객체 지향 설계와 실천

http://www.yes24.com/Product/Goods/9008455

 

테스트 주도 개발로 배우는 객체 지향 설계와 실천 - YES24

TDD로 좀 더 탄탄한 객체 지향 개발을 이끄는 안내서 소프트웨어 개발의 여러 층위에서 TDD가 어떻게 작동하는지 보여주면서 테스트로 코드를 객체 지향적으로 구성하고 기능을 구현하며 목 객체

www.yes24.com

코드 레벨에 대한 설명이 많아 이는 생략하고 보편적인 가치 위주로 정리합니다.

(😛) 은 사견입니다.

 

앨런 케이 어록

  • 객체란 서로 메시지를 주고받는 생물학적 세포와 비슷해야한다.
  • 중요한 것은 메시지 전달이며 위대하고 성장 가능한 시스템을 만들 때의 핵심은 모듈 간의 의사소통에 있지 모듈의 내부 특성이나 작동 방식에 있지 않다.

 

품질

  • 외부 품질은 시스템이 고객과 사용자의 요구를 얼마나 잘 충족하는 가이다.
  • 내부 품질은 시스템템이 개발자와 관리자의 요구를 얼마나 잘 충족하는 가이다.

 

결합도

오디오 콤보  시스템템을  생각해해보자 오디오 콤보 시스템은 긴밀하게 결합돼있다. 아날로그 라디오를 디지털 라이도로 바꾸고 싶다면 전체 시스템을 다시 구성해야한다. 반대로 만약 컴포넌트를 갖고 시스템을 조립했다면 느슨한 결합도로 수신기만 교체했어도 됬을 것이다.

 

응집도

응집도가 높은 기능은 유지보수하기 쉬워진다.

 

선언적 정의

객체 구성을 관리할 목적으로 작성하는 코드를 객체망의 행위에 대한 선언적 정의라고 한다. 시스템을 선언적 정의 방식으로 구축하면 방법이 아니라 목적에 집중할 수 있어 시스템의 행위를 변경하기 쉽게 도와준다.

 

Value

  • Value 는 양이 고정된 불변 인스턴스다 값은 개별적인 식별자가 없으므로 두 값 인스턴스의 상태가 같다면 사실상 동일한 셈이다.

 

객체

  • 이 책에서는 객체라는 용어를 값에 사용하지 않으며 식별자와 상태, 처리 과정을 지닌 인스턴스를 가리킬 때만 사용한다.
  • 객체는 역할을 하나 이상 구현한 것이며 역할은 관련된 책임의 집합이다. 책임은 어떤 과업을 수행하거나 정보를 알아야하는 의무를 말한다. 협력은 객체나 역할의 상호 작용에 해당한다.
  • 우리가 생각하기에 도메인 모델은 의러한 의사소통 패턴에 속한다.
  • 객체의 호출자는 객체가 무슨일을 하고 무엇을 의존하는지 알고 싶어하지 어떻게 동작하는지를 알고 싶어 하지 않는다.
  • 객체를 부분적으로 생성하고 해당 객체의 프로퍼티를 설정하는 식으로 마무리하는 것은 불안정한 방법이다. 프로그래머가 필요한 의존성을 모두 설정하는 것을 기억해야하기 때문이다.

 

협업하는 객체

  1. 의존성: 한 객체가 자신의 역할을 수행할 수 있게 이웃하는 객체에게 요구하는 서비스다.
  2. 알림: 객체의 상태가 변경되었거나 중요한 활동을 할 때 이웃에게 알리는 경우다.
  3. 조정: 객체의 행위를 더 넓은 시스템의 요건에 맞게 조정하는 경우다.

 

디미터의 법칙

  • 묻지말고 말하라 스타일
  • 객체는 다른 객체를 탐색해 뭔가를 일어나게 해서는 안된다.

 

TDD 시작하기

  • TDD 를 시작하기 전에 최초 기능은 어떨까? 인수 테스트의 일환으로 테스트는 전 구간을 대상으로 실행되어 시스템의 외부 인터페이스에 관해 필요한 피드백을 줘야한다.
  • 자동화된 빌드, 배포, 테스트 주기 전체를 구현해야한다. 이렇게 하려면 첫 테스트가 실패하는 것을 보기도 전에 해야 할 일이 굉장히 많다.
  • 우선 동작하는 골격을 대상으로 테스트하고 동작하는 골격을 만드는 동안에 골격의 구조에 집중한다. 테스트가 최대한 표현력을 갖추게끔 테스트를 정리하는 일에 대해서는 크게 신경쓰지 않는다.
  • 첫 테스트의 맥락은 코딩을 시작하기 전에 전체 설계를 클래스와 알고리즘 수준까지 끌어내려 정교하게 다듬자는 것이 아니다.
    피드백 소스를 구축하고 불확실성을 일찍 드러내기 위함이다.

 

TDD 주기와 유지

  • 도메인 모델 객체에 단위 테스트를 수행한 다음 애플리케이션의 나머지 부분에 해당 객체를 끼워넣는 식으로 TDD 를 시작할 수 있다. 이 방식은 쉬워보이지만 나중에 통합할 때 골머리를 앓을 가능성이 높다. 불필요하거나 올바르지 않은 기능을 구현하느라 시간을 낭비하고 있는 것일 수도 있기 때문이다.
  • 경험상 코드가 테스트 하기 어렵다면 주로 설계 개선이 필요하기 때문이다.
  • 너무 큰 단위로 테스트를 수행하면 코드의 모든 가능한 경로를 시도하는 조합 폭발 현상으로 개발이 중단된다. 반면 테스트 단위를 너무 세밀하게 잡으면 테스트하기는 쉬워지지만 동작하지 않는 객체에서 유래하는 문제를 놓치고 말것이다.

 

설계

  • 우리는 작성하기 쉬운 코드보다 유지 보수하기 쉬운 코드를 높게 평가한다.
  • 전체는 부분의 합보다 단순해야한다. (복합 객체의 API 는 구성 요소의 API 보다 복잡해서는 안된다.)
  • 인터페이스에 선언되는 메서드 수가 적을수록 해당 메서드를 호출하는 객체의 역할이 명확해진다.
  • 우리는 클래스보다 인터페이스를 강조한다. 다른 객체에서 보는 것은 결국 인터페이스이기 때문이다.
  • 우리는 객체에 대한 클래스를 구현 세부 사항으로 본다.
  • 우리는 애플리케이션 개발을 위한 점진적 개발 단계를 파악할 수 있어야한다.

 

지나간 일은 눈에 잘 보이지 않는다.

'왜 진작 알아차리지 못했을까'하는 생각이 드는 순간들이 있다.  분명 설계에 시간을 더 들인다면 설계를 변경하지 않아도 됬을것이다. 정말 그럴까? 그럴 때도 있지만 경험상 설계를 직접 구현해 보는 것만큼 설계에 영향을 주는 것은 없다. 우리 주변에 설계를 늘 올바른 상태로 유지할 만큼 똑똑한 사람은 얼마 되지 않는다. 우리의 문제 해결 메커니즘은 코드의 핵심 영역으로 좀 더 일찍 파고들어 집단적인 사고를 바꾸는 것이다. 뭔가를 변경할 때 현재 기술로 작은 단계를 밟아가며 테스트해 실수를 막는다.

 

Context 독립

  • 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못하는 것을 의미한다.

 

TDD 의 가치

  • TDD 의 테스트 부문에서 얻을 수 있는 최고의 혜택은 코드를 망가뜨리지 않고도 변경할 수 있다는 자신감이다.
  • 테스트를 먼저 작성함으로써 총 세가지 가치를 얻을 수 있다.
  1. 어떻게를 고려하기 전에 달성하고자 하는 바가 무엇인지를 기술하게 만든다.
  2. 테스트 대상 컴포넌트의 규모가 너무 커서 좀 더 작은 컴포넌트로 쪼개야함을 말해준다.
  3. 단위 테스트를 위한 객체를 만들려면 해당 객체의 의존성을 전달해야 하는데, 이러한 의존성이 어디에 있는지를 알아야하게 만든다. 이를 통해 Context 독립성이 높아지는데, 단위 테스트를 구성하기 앞서 대상 객체의 환경을 구성할 수 있어야 하기 때문이다. 객체의 테스트가 어려우다면 의존성 정리가 필요하다는 신호다.

 

TDD 와 의존성

  • 테스트를 통해 우리는 의존하는 것이 무엇인지 알고 싶다.
  • 암시적인 의존성도 의존성이다. 전역 값을 사용해 캡슐화를 우회하는 것은 의존성을 감출수 있지만, 의존성이 사라진 것은 아니다. 단지 의존성에 접근할 수 없게 될 뿐이다.

 

TDD 팁

  • 서드 파티 API 를 사용하는 경우 어댑터 패턴을 사용할 수 있다. 어댑터 계층은 가능한 얇게 유지하라, 잠재적으로 불안정하고 테스트하기 어려운 코드양을 줄이기 위해서다. (😛 서드 파티 API 를 Mock 하지 말고 어댑터 계층을 Mock 하여 테스트하라.)
  • 코드를 컴파일 가능한 상태로 유지하라. 컴파일이 실패할 때 컴파일러를 통해 그러한 부분에 관해 알 수 없으면 변경 범위가 어떻게 되는지 확실할 수 없다. 컴파일 실패는 해당 브랜치로 체크인 할 수 없다는 의미이다.
  • 종종 테스트하기 어려운 기능을 발견하면 우리는 단순히 해당 기능을 어떻게 테스트하는지 고민하는게 아니라 왜 테스트하기 어려운지도 자문해본다.
  • 마법을 쓰지않고 대체할 수 없는 객체에 대해 Mock 객체를 적용해야한다.
  • (😛 생성자가 비대해진다면 둘 중 하나다. 1. 응집도가 낮거나, 2. 역할이 과다한 것이다.)
  • 객체가 역할이 너무 많다면 객체 자체의 규모가 너무 크다는 것을 의미할지 모른다.
  • then 구문을 조금만 작성하라. 너무 많은 예상구문이 필요하다는 것은 너무 큰 단위로 테스트하려는 것 일 수 있다.
  • 단위테스트는 1000줄 이상이어서는 안된다.
  • 지식의 초점이 특정 객체에 초점이 맞춰지도록 한다.
  • 데이터 대신 행위를 전달하라.

 

Bad case

  • 테스트 이름이 테스트의 의도를 설명하지 못한다.
  • 테스트 케이스가 여러 기능을 테스트한다.
  • 테스트 구조가 달라서 테스트를 보는 것만으로 의도를 이해할 수 없다.
  • 테스트를 준비하는 코드가 너무 많아 핵심 로직이 묻힌다.
  • 매직 넘버를 사용하지만 그 숫자가 무엇을 의미하는지는 명확하지 않다.

 

불안정한 테스트의 원인

  • 1. 테스트가 시스템에 관련 없는 부분과 테스트 대상 객체에 무관한 행위와 너무 긴밀하게 결합되어 있다.
  • 2. 테스트가 코드의 예상 행위를 과도하게 기술해서 필요 이상으로 제약한다.
  • 3. 테스트에서 동일한 제품 코드의 행위를 시험할 때 중복이 생긴다.

 

테스트의 핵심

  • 테스트의 핵심은 표현력이다. 테스트를 읽는 사람이 뭐가 중요한지 가늠할 수 있어야한다.
  • 테스트의 핵심은 통과가 아니라 실패에 있다. 프로젝트가 테스트를 통과하길 바랄 뿐만 아니라, 테스트가 실제로 존재하는 오류를 감지해 보고하게 만들수도 있어야한다.

 

테스트 데이터 빌더

도메인 객체를 만들때 준비해야할 게 많다면 빌더를 이용할 수 있다. 빌더는 아래와 같은 역할을 한다.

  1. 새로운 객체 생성시 문법적으로 지저분한 부분을 감춰준다.
  2. 기본적인 경우에도 단순하게하고 특별한 테스트일 때도 복잡하게 만들지 않는다.
  3. 객체의 구조적 변화로 부터 테스트 케이스를 보호한다.
  4. 빌더를 이용하면 매개변수의 용도를 밝힐 수 있다.

 

명령(Command)과 질의(Query)

  • Command 는 객체의 외부 세계를 바꾸기 위해 부수 효과를 낼 가능성이 있는 호출을 말한다. 메소드 호출 횟수에 따라 시스템 상태가 달라진다.
  • Query 는 세계를 바꾸지 않아서 n 번 이상 호출할 수 잇는 것을 말한다. (n >= 0)
  • 명령과 질의를 구분하여 테스트를 분리하는 것이 도움이된다.
  • (😛 책에서는 JMock 을 이와 관련하여 예제가 몇개 나오는데, 이게 진짜로 효용성이 있는지는 긴가민가하다. 부록에나오는 코드도 몇가지 기법은 Interaction Testing 에 가까운 것 같아서... 대신 명령과 질의를 분리하라는 얘기는 꾸준히 다른 책에서도 언급되는 내용이라 굉장히 중요한 가치로 여겨진다.)

 

패턴과 타입명

책에서는 Repository 나 DAO 패턴을 이용하여 영속성을 구현하는데, 책에서는 이러한 용어를 적극적으로 사용하지 않는다. 이러한 용어를 사용하면 영속화에 대한 지식이 응용 도메인으로 새어나가는 것이기 때문이다. 이는 '포트와 어댑터' 아키텍처를 위반하는 것이다. 객체는 영속화 기술에 관해 알 필요가 없어야한다. 중요한 것은 인터페이스 및 클래스와 시스템 내의 다른 클래스와의 관계이다. 어떤 패턴을 쓰고 있는지가 궁금한 것이 아니다. 따라서 타입명에 Data, Object, Access 같은 일반화된 단어는 가급적 사용을 주의한다. 대신 도메인 개념을 나타내고 애플리케이션과 도메인을 이어주는 방법을 나타내는 이름을 부여하기위해 노력하라.

 

비동기 테스트 할 때의 유의사항

  • 부하 테스트는 스레드 안전을 보장하지 않는다. 안심할 수 있을 정도만 보장한다.
  • 비동기적인 테스트의 경우 테스트 대상 시스템과 협력하는 것에 관해서는 신중을 기해야한다. 그렇지 않으면 테스트가 불안정해져서 시스템이 동작할 때 테스트가 간헐적으로 실패하거나 시스템이 망가졌을 때도 테스트가 통과할 수 있다.
    테스트에서 시스템을 관찰하는 방법은 크게 두 가지가 있다. 관찰 가능한 상태를 샘플링하는 방법과 보내야할 이벤트를 기다리는 방법이다.
  • 비동기 호출이 loop 를 돌며 시스템의 값을 변경시킬 경우, polling 방식으로 하는 검증은 시스템의 변화를 놓칠 수 있다. 비동기 호출이 history 를 추적할 수 있게하여 호출의 결과를 확인할 수 있어야한다.
  • 몇몇 시스템에서는 자체 이벤트를 내부에서 일으키도 하는데 가장 흔한 것이 타이머를 이용한 스케줄링이다. 이럴 때 사용할 수 있는 유일한 해결책은 타이머를 자체 스케줄링에서 분리해 시스템을 결정적으로 만드는 것이다. 이렇게하면 테스트에서는 스케줄러인 척 하고 이벤트를 결정적으로 생성해서 시스템이 각 행위를 거치게 할 수 있다.