kok202
TDD 안정감을 주는 코드 작성 방법

2021. 12. 18. 16:19[개발] 기록/TDD

해당 강의를 듣고 정리한 포스팅입니다

https://fastcampus.co.kr/dev_red_ygw

 

The RED : 이규원의 현실 세상의 TDD : 안정감을 주는 코드 작성 방법 | 패스트캠퍼스

그동안 우리나라에는 TDD를 제대로 다루는 책도 강의도 없었죠. 그래서 제가 개발 현장에서 활용하는 TDD를 정확하게 알려드리고자 강의를 만들었습니다. 이제 TDD에 대한 잘못된 인식은 버리고, '

fastcampus.co.kr

 

01. 좋은 코드

과학은 밝혀내고 엔지니어링은 해결한다.

 

엔지니어링은 결국 주어진 자원과 과학을 이용하여 문제를 해결하는 것이다.

적은 자원으로 어떻게 문제를 해결할지 결정해야한다. 따라서 이론을 이용하되 매몰되면 안된다.

 

클린 코드에대한 오해: 클린 코드란 어디에나 통용되는 보편적인 사실은 아니다.

 

패턴에대한 오해: 어디에나 통하는 은탄환은 없다.

 

어떤 코드를 작성하느냐 보다 어떤 목표를 align하고 있느냐가 중요하다.

 

02. 코드 기능 명세

입력과 출력을 기능 명세라고 부를 수 있다. 이러한 기능 명세가 있는 곳이 도메인이다.  소프트웨어는 문제를 푸는 도구이며 도메인은 소프트웨어가 풀어야할 문제가 정의되는 공간이다. 문제를 잘 이해해야 도구를 잘 만들 수 있다.

 

도메인 지식의 흐름

  1. 비즈니스 전문가의 요구사항
  2. 분석가 (PM, 기획자, 프로그래머)
  3. 프로그래머
  4. 컴퓨터

 

프로그래머가 컴퓨터에 요청할 때는 모든게 명확하게 결정된다. 따라서 프로그래머는 충분히 명확한 도메인 지식을 확보하기 위해 지식의 상류에 보강을 요청해야할 수 있다. (입력이 이럴땐 이런 예외가 발생해야한다. 같은 내용도 포함해서 요청해야한다.)

 

03. 테스트 기법

수동 테스트: 흔히 QA 라고 부르는 UI 를 이용한 기능 검증. 최종 사용자에 가까운 테스트를 하기 때문에 신뢰성은 높지만 실행 비용이 높다. 인수 테스트라고도 부른다. 수동 테스트를 통해 프로그래머가 받을 수 있는 피드백의 품질은 낮다. (현상이 파악될 수는 있어도 원인이 무엇인지는 알 수 없다.)

 

소프트웨어 회귀(regression): 원래 동작하던 기능이 동작하지 않게되는 경우를 의미한다.

 

시스템이 성장하면서 시간이 늘어날 수록 테스트 해야하는 회귀들이 계속 증가하게된다. -> 자동화된 테스트가 필요한 이유.

 

테스트 자동화: 기능 테스트를 코드가 자동으로 하도록 하는 것. 테스트 코드를 프로그래머가 작성해주어야하므로 코드 작성 비용에 대한 부담감은 증가한다. 대신 작성된 코드는 실행 비용이 거의 없다. 테스트 코드 작성과 관리는 프로그래머의 역량에 크게 영향 받는다.

 

단위 테스트: 시스템중 일부를 검증하는 테스트. 상대적으로 작성 비용이 낮고 프로그래머에게 높은 품질의 피드백을 제공해준다. 단 단위 테스트가 모두 성공한다해서 시스템이 온전하다는 것을 의미하지는 않는다. 따라서 시스템 이상 여부에 대한 신뢰도는 낮은 편이다.

 

04. 코드 분해

프로그래머가 다룰 수 있는 문제의 크기는 한계가 있기 때문에 큰 문제를 작은 문제로 분해해서 생각할 필요가있다. 분해된 문제들은 재사용이 될 수 있다. 코드를 재사용 할 때는 기존 코드에 수정이 없이 재사용하는 것이 가장 좋은데, 코드 수정이 이뤄지면 해당 수정의 영향이 어디까지 줄지 알 수 없기 때문이다.

 

잘 분해된 코드는 모듈화되었다고 할 수 있다. 모듈은 충분히 신뢰 할 수 있어야 사용자가 안심하고 사용할 수 있는데, 결국 모듈화에는 단위 테스트가 필요하다는 의미이다.

 

코드 분해: 큰 시스템은 더 작은 하위 시스템으로 분해하는 것

코드 조립: 작은 시스템은 더 큰 상위 시스템에서 사용하도록 조립하는 것, 모듈 재사용, 라이브러리를 사용하는 것이 조립의 예시.

 

조립할 때는 사용자는 모듈의 인터페이스를 고려해서 개발하게된다.

 

05. 단위 테스트

테스트 코드 양이 줄어들었다고 무조건 좋은 건 아니다. (구글 소프트웨어 엔지니어링에서 말하길 DRY 보다는 DAMP 가 낫다.) for 루프를 사용해서 테스트하면 프로그래머가 받는 피드백이 줄어들 수 있다. 이런 경우 Parameterize 테스트를 좋다.

 

06. TFD: 테스트 우선 개발

테스트를 우선시 해서 개발하는 방법.

 

테스트를 우선시함으로 써 명확한 목표를 설정하게한다. 프로그래머가 풀어야할 문제를 구체적으로 이해할 수 있게된다. 프로그래머는 설정된 목표를 달성하기 위해 최소한의 개발만 하면된다.

 

07. 정리된 코드

수학적 의미의 factorization(인수분해)은 숫자나 식을 여러 요소로 더 작은 요소로 나누는 것을 뜻함.

 

즉 코드를 리팩토링한다는 것은 코드를 더 작은 객체나 함수로 나눈다는 것이다. 중요한 것은 형태를 바꾸더라도 코드의 의미가 변해선 안된다. 그럼 코드가 가진 의미를 유지한다는 것은 무엇을 의미하는가? regression 이 존재해선 안된다는 의미이다.

 

안타깝게도 대부분의 프로그래머는 리팩토링을 하면서 기존의 코드가 영향을 안준다는 것을 확인하지 않는다. 결국 제대로된 리팩토링을 위해선 테스트 코드가 필수적이다.

 

08. 테스트 주도 개발 Red-Green-Refactor

  • RED: 실패하는 테스트를 추가하는 단계. 구체적인 하나의 요구사항을 검증하는 하나의 테스트를 추가한다. 반드시 추가한 테스트가 실패하는지 확인해야한다. 성공한다면 잘못 작성한 것이다.
  • GREEN: 테스트를 통과시키는 최소한의 코딩을 하는 단계. 테스트 성공은 요구사항의 만족을 의미한다.
  • REFACTOR: 통과를 유지하면서 구현 설계를 개선하는 단계. 코드베이스를 정리하고 가독성, 적응성, 성능등을 고려해서 리팩토링한다. 리팩토링은 모든 테스트의 성공을 확인하면서 이루어져야한다.

 

켄트벡의 설계 규칙

  1. 테스트 통과
  2. 의도 노출 (코드의 의도를 노출해야한다.)
  3. 중복 제거
  4. 최소 요소 (불필요한 코드, 테스트가 없는 코드는 제거한다.)

단 중복 제거와 의도 노출은 충돌 될 수 있다. 프로그래머는 매번 어떤 가치를 선택할지를 결정해야한다.

 

09. 프로그래머 피드백

프로그래머가 받을 수 있는 다양한 피드백들이 존재한다.

  1. 사용자 피드백
  2. QA
  3. 프로그래머 테스트: 프로그래머가 준비한 피드백 장치를 통해 하는 테스트 (ex. 자동화된 테스트)
  4. 도구 피드백: 컴파일 오류, 정적 검사로 프로그래머가 사용하는 도구가 제공하는 피드백

아래로 내려갈 수록 빠르게 받을 수 있고 프로그래머 친화적으로 피드백을 받을 수 있다.

 

테스트 주도 개발은 가장 중요한 목표를 우선 달성하도록 유도한다. 모든 테스트가 성공했다면 오버 엔지니어링을 그만두고 다음 단계로 나갈 기회를 제공한다.

 

TDD 에서 제일 중요한 핵심은 피드백이다. 짧은 주기로 지속적으로 프로그래머가 피드백을 받도록 하는 것이다.

 

10. 인터페이스와 구현

협력과 계약: 협력에 필요한 것은 "어떻게" 보다 "무엇"을 하느냐다. 인터페이스에 프로그래밍 해야한다.

 

정보 숨김과 추상화: 대부분의 시스템 정보는 대부분의 프로그래머에게 숨겨지는게 도움이 된다. 효과적인 모듈화를 통해 변경의 여파를 최소화해야한다.

 

11. 환경 변화와 적응력

환경 변화에 경제적으로 대응하는 코드가 필요하다. 이때 환경 변화에 더 경제적으로 대응하는 코드를 적응력이 높다 말한다.

 

OOP 핵심은 뭘까? 일반적으로 아래 4가지를 OOP의 핵심이라고들 함.

  1. 추상화 ⇒ OOP에서만 사용하는 것은 아님
  2. 상속 ⇒ 일부 OOP 언어는 아예 상속을 지원하지 않음
  3. 캡슐화
  4. 다형성

 

OOP를 처음 언급한 Alan kay가 말하는 OOP

  1. 메시징
  2. 지역 보존 상태
  3. 프로세스의 보호와 숨김
  4. 극도로 느린 지연 바인딩

지연 바인딩 때문에 인터페이스로 통신해야함. 

 

OCP 개방 폐쇄 원칙

Bertrand meyer: "확장 가능한 경우 모듈은 열려있다고 말한다" "다른 모듈에 사용될 때 모듈은 닫혀있다고 말한다" (빌드된 상태로 제공되어야한다.)

 

Testability: 테스트하기 얼마나 쉬운가?

 

12. 입력과 출력

입력 논리 출력을 처리하는 방식

  1. 직접 입력과 직접 출력: 공개된 인터페이스를 통해 입출력하기 때문에 다루기 간단함
  2. 간접 입력과 간접 출력: 입력된 인터페이스를 통해 입출력하기 때문에 다루기 어려움

 

사이드 이펙트: 인터페이스 설계에 드러나지 않는 출력. 대표적인 사이드 이펙트에는 실패(에러), 지연(Disk, network I/O) 간접 출력 등이 있다.

 

13. 테스트 대역

운영 환경에서 사용하는 코드를 대신하는 대역 코드를 테스트 대역이라고 부름 (test double)

 

DOC: dependended on component 의존 구성 요소

 

실제 코드가 아닌 대역을 사용해야하는 경우: DOC 를 준비하는데 드는 비용이 큰 경우, (구동에 많은 자원이 필요한거나 환경 제어가 어려울 때) SUT 의 계약을 준수하는 대역 코드를 사용할 수 있다. 

 

단 대역을 사용한다는 것은 대역이 DOC 와 동일한 계약을 준수한다는 가정을 하고 테스트하는 것이기 때문에 실제 코드와 거리감이 있을 수 있음을 유의해야한다.

 

SUT : 테스트하려는 대상이 되는 시스템 (System under test)

  • Dummy: 인수를 채워주기 위해 전달하는 값이지만 사용되지 않는 객체
  • Stub: 미리 준비된 답을 출력하는 대역
  • Spy: 데이터를 기록하고 있다가 입력이 있으면 실제 코드 처럼 흉내내서 준비된 값을 출력하는 객체
  • Mock: 자가 검증 능력을 가진 대역, SUT 내부의 행위를 검증한다. (테스트 더블과 동일한 의미로 사용되기도 함)
  • Fake: 의존성 계약을 준수하는 가벼운 구현체

 

14. Mockists vs Classicists

Sociable 테스트: 단위 테스트가 시스템을 테스트할 때 의존 대상을 실제로 사용한 것

Solitary 테스트: 실제 의존성과 분리시키고 테스트 대역을 시켜서 단위 테스트를 돌리는 것

 

테스트 대역으로 생기는 가정을 얼마나 믿을수 있을까? 를 고민해봐야함

 

Mock 의 위험성

  1. SUT 내부의 동작을 검증하기 때문에 상태를 검증하기보다는 행위 검증(어떤 메소드를 실행했는지 확인)하게 되려는 경향이 생김
  2. 정보 숨김을 위배
  3. 테스트가 SUT 구현에 의존하게만듬

결과적으로 리팩토링을 할 때 고통스럽고 불안한 리팩토링을 하게 만듬

 

Mockists

Mock 을 테스트에 적극적으로 사용하는 개발자들

→ TDD 를 하게 됨에도 불구하고 구현에 집착하게됨

 

Classicists

Mock 을 가급적 사용하지 않으려는 개발자들

 

15. Private 메소드를 테스트해야하는가?

http://shoulditestprivatemethods.com/

private 메소드를 테스트하려는 욕구 자체가 책임이 제대로 할당되어있는지를 확인해봐야한다는 것을 시사한다.

 

비공개 모듈 테스트를 하면 안되는 이유: 비공개 모듈의 작성과 사용은 공개 모듈의 구현에 해당하는 부분.  비공개 모듈 테스트는 구현에 의존하는 것을 뜻한다. 즉 테스트가 시스템 코드와 강하게 결합된다는 것을 의미한다.

 

켄트벡의 설계 규칙

  • 테스트를 통과
  • 의도 노출
  • 중복 제거
  • 가장 적은 요소

 

이 중 "테스트를 통과", "가장 적은 요소"가 private 메소드를 테스트하지 않아도 되는 이유가 됨. private 메소드 테스트들은 이후 설계 변경에 영향을 주게된다.

 

16. 테스트 주도 설계

테스트는 인터페이스 설계에 의존하므로 인터페이스 설계 품질이 낮으면 테스트 작성이 어렵다. 따라서 테스트가 어렵다는 것은 설계가 잘못된 건 아닐까? 하고 고민하게 만들어준다.

 

반대로 테스트가 있다면 리팩토링을 할 때 두려움 없이 구현 설계를 과감하게 개선하게 도와준다.  따라서 단위테스트는 설계에 분명한 영향을 준다. 하지만 단위테스트에 의지한다해서 인터페이스 설계가 무조건 잘됬다 할 수는 없다.

 

상세한 단위 테스트가 좋은 설계를 보장하지 않는 이유

  1. 단위 테스트는 낮은 응집에 대한 피드백을 주지는 않는다.
  2. 단위 테스트는 일관된 설계를 사용하지 않는다.
  3. 단위 테스트가 의도 노출을 요구하지 않는다.

 

단위 테스트에 의지하는 구현 설계를 하게되면

  1. 단위 테스트는 책임 분산을 유도하지 않는다.
  2. Mockists가 되게 만든다.
  3. private 메소드 테스트를 하고 싶게 만든다.
  4. 테스트가 쉽더라도 구현이 간단하지 않을 수 있다.

 

17. TDD 의 한계

TDD가 적합하지 않는 상황도 분명이 존재한다. TDD는 매력적인 도구이지만 남용을 주의해야한다. 무리하게 TDD 를 적용하려하면 안된다.

 

TDD 를 적용하기 힘든 경우

  1. 요구사항이 빈번히 변경되는 경우 (프로그래머가 개발을 할 때 모든 코드가 목표가 명확할 수 있는 것은 아니다. ex. 신규 비즈니스) 
  2. 환경제어가 힘든 경우.
    1. 단위 테스트는 빠를 수록 좋은데, 어떤 코드의 동작이 매우 느리다면 TDD 를 적용하기 까다로워진다. 대역을 사용하기 시작하면 가정이 계속 누적되므로 시스템의 신뢰성을 보장하고 있다고 말하기 어려워진다.
    2. 의존하는 하위 시스템이 또 다른 하위 시스템에 의존하고 있고, 이 중 일부가 입출력을 예측하기 힘들 경우 테스트자체가 비결정적이게된다. 
  3. 이미 낮은 코드 적응력을 갖고 있는 프로젝트일 경우. 과감한 리팩토링이 힘들어지므로 코드 기반의 적응력을 높이는 것자체도 어려울 수 있음. 단위 테스트 작성 비용이 너무 높아진다.
  4. 프로젝트의 목표 시간이 빨리 끝나서, TDD 의 이점을 얻기 시작하는 손익 분기점을 넘기지 못할 경우

 

18. 인터페이스와 테스트

인터페이스: 한 객체가 상호 작용하는 지점, 계약을 제공하고 사용자는 계약을 사용한다.

 

API: 한 시스템이 협력시스템에 제공되는 코드 친화적인 소통 수단. 테스트 자동화 비용이 낮지만 사람이 사용하긴 힘드므로 인수 테스트 비용이 높다.

 

UI: 인간 친화적인 소통 수단이지만 변경이 잦다. API 보다 테스트 자동화 비용이 높음.

 

19. 인수 테스트 주도 개발

인수 테스트 : 시스템이 운영 환경에 거의 유사할 경우. 사용하는 방법

 

인수 테스트는 UI, API 를 대상으로 할 수있다.

 

인수 테스트는 상대적으로 비결정적이고 느리지만, 안정감이 높고 최종 클라이언트 관점에서 테스트를 해볼 수 있다. 아쉽게도 피드백 품질이 낮다. (어떤 문제 때문에 테스트가 실패했는지 알 수 없다)

 

인수 테스트와 자동화된 테스트를 적절히 잘 섞어서 사용하는 것이 좋다. 

 

20. 코딩 계획

  1. 어떤 가치를 위해 코드를 작성하는지 목표를 명확히 기술한다. 사용자 스토리나 테스트 케이스는 목표를 표현하기 위한 좋은 수단이다.
  2. 전체 작업을 하위 작업으로 분리한다. 하위 작업 역시 목표를 명확히 기술할 필요가 있다.