CS공부(개념)/독후감

클린코드 4주차: 단위테스트, 시스템, 그리고 창발성

cantor 2023. 8. 28. 20:04

230829 클린코드 4주 차

클린코드 북스터디를 하며 작성한 독후감 겸 요약글입니다.

로버트 C. 마틴- 클린코드

이번주차에는 9장 단위테스트, 11장 시스템 그리고 12장 창발성 쳅터를 읽었습니다.

 

테스트 코드작성 행위에 대한 개념이 바뀌었습니다.

그동안은 테스트 코드라는 도구의 장점에 대해 점차 알게 되었다면,

이번 주에는 테스트 코드라는 것을 바라보는 다른 시각을 갖게 되었습니다.

 

현업에서 일하는 스터디원분들과 나눈 대화와 책의 내용을 통해 테스트 코드는

단순히 유용한 도구가 아니라, 개발과정에서 절대 빠질 수 없는 필수요소라고 생각하게 되었습니다.

그렇게 생각하는 이유는 다음과 같습니다.

1. 테스트 코드는 프로그램의 기획서나 설계도의 역할을 합니다.

 

한번 완성된 테스트 코드는 특별한 사건이 없는 한 ( 기획 변경 이라던가..), 거의 수정되지 않습니다.
또 나중에 리팩터링을 할 때에나, 개발 중인 코드의 기능을 확인할 때에도 테스트 코드가 기준이 됩니다.

 

2. 테스트 코드 작성행위 자체가 업무이자, 실행 코드를 위한 계획이기도 합니다.

테스트 코드를 작성하려면 내가 구현하려는 함수의 명세,
즉 입력과 출력을 명확하게 이해하고 있어야 합니다.

 

사전에 어떤 것을 입력받고 어떤것을 출력하는지에 대한 구상이 있어야
프로그램의 목표에 적합한 코드를 만들 수 있습니다.

 

테스트코드 통과 자체가 "구현 목표"로 여겨질 수도 있고요.

 

테스트 코드가 작성은 일종의 업무이며, 실제 프로그램 코드 작성을 위한

계획 업무라고 볼 수 있습니다.


책 본문 내용

9. 단위테스트

프로그램을 구성하는 코드들이 으레 그렇듯이
테스트 코드도 가독성이 중요하고, 빠른 구현을 위한 기법들이 있습니다.

 

이번 장에서는 테스트코드, 케이스를 작성하는 기법 및 원칙을 이야기합니다.

TDD 법칙

  1. 테스트 코드를 먼저 작성하라
    • 실제 코드를 작성하기 전에 실패하는 단위 테스트를 작성한다.
  2. 실행이 실패하는 정도로만 단위테스트를 작성하라.
    • 단, 컴파일은 실패하지 않아야 한다.
  3. 실제코드의 구현 진척도를 테스트코드와 맞추어라
    • 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드를 유지하라

위의 TDD 법칙을 따라 작업하면 테스트코드와 실제코드가 함께 개발되고,
테스트코드 작성 즉시 실행코드를 작성하게 됩니다.

저자의 경험에 따르면 개발과 테스트가 30초 주기로 묶인다고 하네요.

 

이렇게 되면 실행코드와 1:1로 대응하는 수많은 테스트 코드가 쌓이게 됩니다.

이런 테스트 코드도 다른 코드들처럼 유연성, 유지보수성, 그리고 재사용성을 가지고 있어야 합니다.

 

테스트 코드가 망가지면 실제 코드도 망가지기 때문에

테스트 코드를 관리하는 데 많은 노력을 기울여야 새로운 로직을 수월하게 추가할 수 있습니다.

 

  • 깨끗한 테스트 코드
    • 도메인에 특화된 언어 사용
    • 이중표준
      • 테스트 환경과 실제 제품 코드의 실행환경은 다릅니다.
      • 제품의 실행환경이 작은 임베디드 cpu라 해도, 테스트코드는 pc에서 실행됩니다.
      • 테스트를 위한 코드는 성능보다 가독성을 더욱 신경 써야 합니다.
      • 구체적인 예시로, 문자열에 알파벳을 누적하여 더하는 로직은 
        stringBuilder가 더 우수한 메모리 성능을 제공하나, 가독성을 생각한다면
        일반 string을 재생성하는 방식을 사용하는 게 좋습니다.
    • 테스트당 assert문 하나/ 개념하나만 포함하는 게 좋습니다.

F I R S T 규칙

  1. Fast: 테스트의 실행은 빨라야 합니다.
  2. Independent: 각 테스트는 의존성 없이 독립적으로 구현되어야 합니다.
  3. Repeatable: 테스트는 어떤 환경에서도 반복이 가능해야 합니다.
  4. Self-Validating: 추가적인 도구 없이, 테스트의 결과만으로 자가검증이 가능해야 합니다.
  5. Timely: 테스트는 실제 코드 작성 전, 적시에 구현되어야 합니다.

11. 시스템

 

이 장에서는 시스템의 깨끗함에 대해 이야기합니다.

 

그동안 이야기했던 깨끗함의 대상은 기능을 구현하는 작은 단위 코드들의 가독성과 재사용성

에 맞추어져 있었습니다. 이제 그 초점을 전체적인 시스템으로 돌립니다..

 

시스템 단위에서 깨끗함을 유지하기 위해선 객체나 함수의 구현 부분과 런타임 실행 부분을

다른 관심사로 보고 분리하는 것이 좋다고 이야기합니다.

 

이 쳅터를 읽으며  nest 프레임워크를 사용한 프로젝트 및 기타 여러 프로젝트의

폴더 및 코드 구성에 대한 이해를 할 수 있었습니다. 이들의 대부분은 구현 부분과 실행 부분이 분리되어 있습니다.

 

아래의 코드는 제가 뺀질나게 가져오는 Open weather Map API를 사용하는 코드입니다.

이 코드는 하나의 파일에 쓰인 코드지만, 마찬가지로 구현과 실행부가 분리되어 있습니다.

const weatherElement = document.querySelector(`.weather__weather`);
const cityElement = document.querySelector(`.weather__city`);
const API_KEY = env.API_OPENWEATHERMAP

// 함수 구현부분
const paintWeather = (data) => {
}
const paintError = () => {
}
const fetchDayWeather =  async (latAndLong) => {
}
const fetchFiveDayWeather = async (latAndLong)=> {
}
const printFiveDayForecast = (data) =>{
}
const onGeoOk = (position) => {
}
const onGeoError = () => {
}
const loadCoords = async () => {
}

// 함수 실행부분: 런타임로직
const init =  async () => {
    try{
        const latAndLong = await loadCoords();

        const weatherData = await fetchDayWeather(latAndLong);
        paintWeather(weatherData);

        const forecastData = await fetchFiveDayWeather(latAndLong);
        printFiveDayForecast(forecastData);

    } catch (error){
        console.log(error);
        alert("failed to load");
        paintError();
    }
}

init();

 

express를 활용하는 app을 보면, 대부분 express app의 설정을 하는 곳과
실행하는 곳에 대한 파일이 분리되어 있습니다.

 

또 컨트롤러를 구현해 놓은 파일과, URI 라우팅을 통해 해당 컨트롤러들을 호출해 줄
라우터도 분리되어 있습니다.

 

nest를 사용하여 만든 프로젝트에서는 실제 비즈니스 로직을 구현하는 서비스객체와,
라우팅을 통해 비즈니스 로직을 실행해 줄 컨트롤러 객체,
그리고 저 객체들을 등록하는 모듈과 전체 nest 서버 실행 부분이 각각 다른 파일로
분리되어 있지요.

 

이러한 것들이 모두 자연스럽게 정해진 것이 아닌
체계적이고 탄탄한 시스템을 만들기 위한 고민에 의해 탄생한 구조라는 것을 알 수 있었습니다.

 

이 쳅터에서 다루는 세부적인 지식은 아직 잘 와닿지 않았지만, 언어 지식의 문제라

금방 배우게 될 것이라 생각합니다.

  • 자바, AOP (Aspect Oriented Programming) 관점지향 프로그래밍 등

main 분리

시스템 생성과 시스템 사용을 분리합니다.

팩토리

객체를 생성하는 정보를 제공하는 프로세스를
main이 실행하게 합니다.

 

새로운 객체가 필요한 시점에 해당 프로세스가 객체 정보를 전달케 하면,
프로세스는 객체 생성 방법과는 무관하게 생성 시점을 통제할 수 있습니다.

의존성 주입

클래스끼리 상속시키지 않고, 클래스 내부에 다른 클래스의 인스턴스를 생성하거나,
메서드의 인자로 다른 클래스 인스턴스를 받아 "설계의존성"을 역전시킵니다.

확장

시스템구성 요소의 관심사를 적절히 분리한 후 모듈화 시킨다면,
변화하는 요구사항에 맞춰 확장이 가능한 아키텍처를 설계할 수 있습니다.

12. 창발성

창발성의 뜻에 대해 먼저 이야기할 필요가 있는 것 같습니다.

 

창발성은 "emergence, 떠오름"에 대한 속성으로,
어떤 행위나 결과에 따라 의도치 않은 무언가가 자동으로 발생하는 성질을 의미합니다.

 

예를 들어, 밤에 잠이 오지 않아 미리 체력을 빼서 피곤하게 만들기 위해
오후에 운동을 하는 습관을 만든 사람이 있다고 합시다.

잠을 빨리 자려는 처음의 의도와는 상관없이 그 사람의 체력이 굉장히 좋아질 것입니다.

 

이러한 부가적 결과를 만들어내는 행위를 "창발성이 있다"라고 표현합니다.

 

이 장에서는 창발성이 있는 "규칙"들을 이야기합니다.

 

좋은 시스템을 설계하고 깔끔한 코드를 작성하기 위해 머리를 싸매고 고생하지 않아도 됩니다.

 

몇 가지 간단한 규칙을 따르면 좋은 품질의 프로그램 코드를 자연스럽게 얻을 수 있습니다.

규칙 1. 모든 테스트를 실행하라

테스트 케이스를 계속해서 만들고 실행하는 것으로
낮은 결합도와 높은 응집력을 갖는 시스템을 얻을 수 있습니다.

규칙 2. 리팩터링 하라

리팩터링 과정에서 발생하는 모든 로직 변경에서는
코드의 기능에 결함이 발생할 수 있습니다.

 

하지만 훌륭한 테스트 케이스가 준비되어 있다면,
안심하고 마음껏 리팩터링을 진행할 수 있습니다.

 

리팩터링을 통해 코드의 중복을 제거하고,
개발자의 의도를 분명히 표현하는 서술적인 코드를 작성하고,
클래스와 메서드의 수를 줄입니다.

요약

  1. 모든 테스트를 실행한다.
  2. 코드는 다음과 같이 리팩터링 (또는 작성) 한다.
    • 중복이 없어야 함
    • 프로그래머의 의도를 표현해야 함
    • 클래스와 메서드의 개수는 최소가 되어야 함.

위의 단순한 규칙을 지키는 것만으로 우수한 시스템이 설계될 것입니다.