본문 바로가기

책 리뷰/함수형 자바스크립트

함수형 자바스크립트 [6장]

※ 함수형 자바스크립트 라는 책을 한 장씩 리뷰해보도록 하겠습니다.

 

Chapter 6. 빈틈없는 코드 만들기

 

함수형 코드를 작성한 다음, 제대로 작동하는지 확인하기 위해선 테스트 코드를 작성해서 의도한 대로 움직이는지 알아봐야 합니다. 단위 테스트의 목적은 코드가 개발 명세를 충족하는지 확인하고 코드가 실패하는 모든 경계 조건 주변에 담장을 치는 것입니다.

이번 장에서는 대부분의 다른 패러다임에서 일부러 테스트하기 쉽게 코드를 설계하는 것과 달리, 함수형 코드는 왜 그자체로 테스트하기 좋은지 알아 볼 것입니다.

 

6.1 함수형 프로그래밍과 단위 테스트

 

테스트는 아래와 같이 세 가지로 분류합니다.

  • 인수 테스트 : 소프트웨어 패러다임과 무관
  • 통합 테스트 : FP 는 코드에 존재하는 여러 모듈을 합리적으로 통합할 수 있으므로 영향력이 조금 있음
  • 단위 테스트 : FP 는 코드베이스 하위 계층에 영향을 미치므로 영향력이 아주 큼

함수형 프로그래밍의 진짜 관심사는 함수 (코드를 모듈화하는 단위) 간의 소통입니다. 테스트에는 QUnit 이라는 라이브러리를 쓸 것이며, 기본 구조는 아래와 같습니다.

QUnit.test('사람 검색 기능을 테스트한다', function(assert) {
  const ssn = '444-44-4444';
  const p = findPerson(ssn);
  assert.equal(p.ssn, ssn);
});

 

6.2 명령형 프로그램 테스트의 어려움

 

단위 테스트 설계의 주요 원칙 중 하나가 격리 입니다. 이러한 원칙이 잘 지켜지지않은 명령형 코드의 문제점은 아래와 같습니다.

  • 식별은 물론 간단한 작업으로 분해하기 어려움
  • 결과를 들쭉날쭉하게 만드는 공유 자원에 의존
  • 평가 순서를 미리 정해야 함

6.2.1 작업을 식별하고 분해하기 어려움

 

분할을 감안하여 설계하지 않은 하나의 거대한 모놀리틱 프로그램은 직관적으로 알기 쉽게 나누기 어렵기에 모듈 단위 (비즈니스 로직을 캡슐화한 함수) 를 식별하기란 아주 어렵습니다.

하나의 함수에 검증, 외부 저장소로부터의 조회, DOM 변경, 에러 처리 등이 전부 묶여있는 코드가 이러한 경우이며 테스트 코드를 단순화하기 위해 이들을 각각의 함수로 분리해야 합니다.

 

6.2.2 공유 자원에 의존한 들쭉날쭉한 결과

 

안정된 테스트라면 나머지 다른 테스트와 독립적으로 돌아가야합니다.

var counter = 0; // (전역)

function increment() {
  return ++counter;
}

/* 테스트 코드 */
QUnit.test(" 0에서 1만큼 증가시킨다 ", function (assert) {
  assert.equal(increment(), 1)
}); // -> O

QUnit.test(" 0에서 1만큼 증가시킨다(반복) ", function (assert) {
  assert.equal(increment(), 1)
}); // -> X

위의 예시는 0에서 1만큼 증가시키는 아주 간단한 단위 테스트인데, 몇번을 돌리든 결과는 같아야 맞겠지만 전역 변수를 참조하고 있어 두 번째 반복 시에 실패합니다. 부수효과를 지닌 함수는 평가 순서로 인해 오류가 발생하기 쉽습니다.

 

6.2.3 평가 순서를 미리 정해야 함

 

단위 테스트는 일관성 차원에서 결합적이어야 합니다. 실행 순서를 바꿔도 결과가 달라져선 안됩니다. 위 케이스의 경우 이 원칙이 통하지 않기에 QUnit 같은 단위 테스트 라이브러리는 후속 테스트를 실행할 전역 테스트 환경을 구성, 정리하는 편리한 도구를 제공함으로써 이 문제를 해결합니다.

QUnit.test("0에서 1만큼 증가시킨다", function(assert) {
  assert.equal(increment(), 1)
});

QUnit.test(" 음수를 1만큼 증가시킨다 ", function(assert) {
  counter = -10; // 전역 컨텍스트 조작
  assert.equal(increment(), -9)
});

하지만 이렇게 전역 상태를 예측하는 방법 또한 잘못 예측했을 시 순서만 바꿔도 테스트에 실패하게 됩니다.

 

6.3 함수형 코드를 테스트

 

FP 의 모든 함수는 입력 매개변수가 명확히 정의되어 있으므로 여러 가지 경계 조건 집합을 제공하여 코드를 구석구석 살피는 일은 어렵지 않습니다. 부수효과 측면에서도 모든 함수는 단순 명료하게 정의되며, 불순한 코드는 모나드로 안전하게 감쌀 수 있습니다.

또한 수동루프도 map, reduce, filter,  재귀 등의 고계 연산과 함수형 라이브러리에 위임하여 해결 가능합니다.

함수형 코드를 테스트했을때 오는 아래의 이점들을 하나씩 살펴보도록 하겠습니다.

  • 함수를 블랙박스처럼 취급
  • 제어 흐름 대신 비즈니스 로직에 집중
  • 모나드 격리를 통해 순수한 부분과 불순한 부분을 분리
  • 외부 디펜던시를 모의

6.3.1 함수를 블랙박스처럼 취급

 

함수형 프로그래밍에서는 느슨하게 입력값을 결합하는 함수를 독립적으로 작성합니다. 이런 함수는 부수효과가 없고 참조 투명하므로 임의의 순서로 몇 번을 실행하더라도 동일한 결과의 예측 가능한 테스트 작성이 가능합니다.

QUnit.test("0에서 1만큼 증가시킨다", function(assert) {
  assert.equal(increment(0), 1)
});

QUnit.test("0에서 1만큼 증가시킨다(반복)", function(assert) {
  assert.equal(increment(0), 1)
});

QUnit.test("10에서 1만큼 증가시킨다", function(assert) {
  assert.equal(increment(10), 11)
});

함수형으로 작성한 increment 함수는 위처럼 몇번을, 어떤 순서로 테스트하든 결과가 같습니다.

 

6.3.2 제어 흐름 대신 비즈니스 로직에 집중

 

함수형 코드 개발 시 가장 시간이 많이 걸리는 과정은 문제를 더 작은 부분으로 나누는 일입니다. 이후 분해된 함수를 서로 붙이는데에도 시간이 소요되는데, 이런 자바스크립트의 함수형 간극은 로대시JS, 람다JS 같은 라이브러리의 curry, compose 같은 함수로 메울 수 있습니다.

이렇게 소요된 시간은 테스트 단계에서 돌려받을 수 있는데, 개발자는 주요 프로그램 로직을 형성하는 개별 함수를 테스트하는 일만 책임지면 됩니다.

 

6.3.3 모나드 격리를 통해 순수/불순 코드를 분리

 

프로그램은 대부분 순수한 부분과 불순한 부분을 모두 갖고 있습니다. 이전 장에서 IO, Maybe, Either 와 같은 모나드를 사용해 부수효과는 제어 가능했지만 코드의 테스트 역시 잘 격리되어 있고 반복이 가능해야 합니다.

함수형 프로그래밍에선 IO 처럼 부수효과를 유발하는 작업은 최소한의 함수에 국한되므로 어플리케이션 로직 중 테스트 가능한 영역이 늘어나며 IO 테스트와도 분리됩니다.

함수형에서도 어쩔 수 없이 부수효과를 일으키는 부분은 모의 객체라는 기법으로 관리 가능합니다.

 

6.3.4 외부 디펜던시를 모의

 

모의 (모킹) 는 함수의 외부 디펜던시를 제어/단언 가능한 방향으로 모방하는 데 많이 쓰이며, 부수효과를 다루기 좋은 테스트 기법입니다. 모의 객체는 기대식을 충족하지 않을 경우 테스트를 불합격 처리합니다.

DB 객체 호출등을 모의하면 외부 자원을 마음대로 조정할 수 있어서 예측 가능하고 일관된 방향으로 테스트가 가능합니다. 모의에는 시논JS 라는 QUnit 모의 플러그인을 사용하겠습니다.

var studentStore, mockContext;

QUnit.module('모의 테스트',
{
  beforeEach: function() { // 모의 단위 테스트가 공유할 모의 콘텍스트 준비
    studentStore = DB('students');
    mockContext = sinon.mock(studentStore);
  },
  afterEach: function() { // 각 테스트가 끝나면 실행 (뒷정리)
    mockContext.verify(); // 모의 객체에 구성한 단언 조사
    mockContext.restore();
  }
});

QUnit.test('showStudent: findStudent 가 null 을 반환하는 경우', function(assert) {
  mockContext.expects('find').once().returns(null); // null 을 반환하는 쿼리 호출 모의
  const findStudent = safefetchRecord(studentStore);
  
  assert.ok(findStudent('xxx-xx-xxxx').isLeft); // 반환값을 Either.isLeft 로 감쌌는지 단언
});

QUnit.test('showStudent: findStudent 가 올바른 객체를 반환하는 경우', function(assert) {
  mockContext.expects('find').once().returns( // 올바른 결과를 내는 쿼리 호출 모의
    new Student('Alonzo', 'Church', 'Princeton').setSsn('444-44-4444')
  );
  const findStudent = safefetchRecord(studentStore);
  
  assert.ok(findStudent('444-44-4444').isRight); // 반환값을 Either.isRight 로 감쌌는지 단언
});

단언 이란 그 자체가 참조 투명성이 항상 유효한지 검증하는 행위이며, 함수형 코드가 명령형 코드보다 테스트성이 압도적으로 좋은 이유는 참조 투명성으로 요약할 수 있습니다.

 

6.4 속성 기반 테스트로 명세 담기

 

단위 테스트는 함수의 런타임 명세를 담고 문서화하는 용도로 쓸 수 있습니다.

QUnit.test('평균 학점 계산', function(assert) {
  assert.equal(computeAverageGrade([80, 90, 100]), 'A');
  assert.equal(computeAverageGrade([80, 85, 89]), 'B');
  ...
});
  • 학생의 평균 성적이 90점 이상이면 A 학점
  • 평균 성적이 80 ~ 89점이면 B 학점

위의 코드만으로도 간단히 함수의 요건을 위처럼 문서화할 수 있습니다. 훌륭한 명세는 사례에 기반을 두지않습니다. 일반적이고 보편적인 명세가 좋은 명세입니다. 명세가 참조 투명하면 함수 또한 참조 투명한 것이며, 이러한 함수를 통해 속성 기반 테스트라는 강력한 테스트가 가능합니다. 속성 기반 테스트는 함수에 어떤 입력을 넣으면 어떤 출력이 나와야 맞는지 성명합니다.

JSCheck 는 함수/프로그램의 참조 투명한 명세에 맞게 기술적인 응답을 생성하는 도구입니다. 엄청나게 많은 무작위 테스트 케이스를 만들고 함수의 모든 가능한 출력 경로를 빠짐없이 체크해봄으로써 어떤 함수의 속성을 검증하는 것입니다. 또한 프로그램 리팩토링 시 새로 추가한 코드가 뜻하지 않게 오류를 일으키지 않는지 검증함으로써 프로그램의 진화 과정을 제어/관리하는 용도로도 사용 가능합니다.

JSCheck 는 JSC 라는 전역 객체로 JSC.claim(이름, 술어, 특정자, 분류자) 이처럼 캡슐화 되어있으며 핵심은 주장과 결론을 만드는 것입니다.

  • 이름 : 주장을 서술
  • 술어 : 주장을 만족하면 true, 그렇지 않으면 false 결론을 내는 함수
  • 특정자 : 입력 매개변수의 형식과 무작위 자료 집합의 생성 기준을 서술한 배열
  • 분류자 (Optional) : 해당 없음 케이스를 버릴 때 쓰려고 각 테스트 케이스에 붙이는 함수

JSCheck.check 에 주장을 전달하면 무작위 테스트 케이스를 실행합니다.

JSC.clear(); // 초기화
JSC.on_report((str) => console.log(str));

JSC.test(
  '평균 학점 계산',
  function (verdict, grades, grade) { // 확인할 조건을 정의한 verdict 객체를 술어 함수에 전달
    return verdict(computeAverageGrade(grades) === grade;
  },
  [
    JSC.array(JSC.integer(20), JSC.number(90, 100)), // A 학점을 받는 기준에 관한 서명 또는 특정자 배열
    'A'
  ],
  function (grades, grade) { // 분류자 함수를 테스트마다 실해앟여 데이터를 리포트에 덧붙임
    return '평균 ' + grade + ' 학점에 관한 테스트: ' + grades;
  }
);

/* 결과 */
평균 학점 계산 : 100 classifications, 100 cases tested, 100 pass

평균 A 학점에 관한 테스트:
  90.042, 98.828, 99.359, ... pass 1
평균 A 학점에 관한 테스트:
  90.084, 93.199, pass 1
...
Total pass 100, fail 0

무작위 테스트 케이스를 기본으로 100개 생성해 위와 같은 보고서를 얻을 수 있습니다.

JSCheck 는 순수 프로그램에만 사용할 수 있습니다. 프로그램 전체를 테스트할 순 없어도, 각 컴포넌트의 개별 테스트는 가능합니다. 이를 이용한 속성 기반 테스트의 장점은 코드가 정말 참조 투명한지의 여부를 확인할 수 있는 점입니다.

 

6.5 코드 실행률로 효율 측정

 

단위 테스트의 효율은대상 함수에 관한 테스트 코드 실행률을 나타내므로 적절한 도구 없이 측정하기란 상당히 어렵습니다. 코드 실행률은 프로그램을 단위 테스트했을 때 실제로 실행된 코드 라인 수의 비율로 측정합니다.

이 수치는 자바스크립트용 코드 실행률 도구인 블랭킷JS 로 계산할 수 있습니다.

아래는 블랭킷JS 의 실행 3단계 입니다.

  • 소스 파일을 읽음
  • 추적기 라인을 추가해 코드를 장착
  • 테스트 실행기에 걸어 상세 실행률 정보를 산출

위 도구를 활용해 구문-실행률 분석 시 명령형 코드보다 함수형 코드가 테스트성이 훨씬 높다는 결론이 나옵니다.

 

6.5.1 함수형 코드 테스트의 효율 측정

 

- 입력이 올바른 경우

 

명령형 코드에서 입력이 올바른 경우 null 혹은 에러 처리 구문이 실행되지 않아 대체로 실행률은 75 ~ 80% 정도입니다.

함수형 코드의 경우 에러 처리 로직이 있지만, 코드의 모나드가 빈 값 또는 Either.Left, Maybe.Nothing 형태로 전파하기 때문에 함수는 실행되고 로직은 건너뛰어 실행률이 100% 가 나옵니다.

 

- 입력이 잘못된 경우

 

명령형 코드의 경우 많은 if-else 블록 탓에 제어 흐름이 여러 갈래로 흩어져 40% 선에 그칩니다.

함수형 코드의 경우 null 케이스 처리는 여러 갈래로 흩어지지 않고 그 값에 의존하는 로직만 건너뛰기 때문에, 전반적인 구조를 유지한 상태에서 끝까지 성공적으로 실행/테스트를 마칩니다.

터무니없는 값이 들어오더라도 전체 코드 영역을 무작정 건너뛰지 않고 안전하게 모나드를 통한 전파를 하므로 실행률은 대체로 80% 에 달합니다.

 

6.5.2 함수형 코드의 복잡도 측정

 

프로그램의 복잡도를 측정하려면 제어흐름을 잘 들여다보아야 합니다. 우선 보기에도 코드가 따라가기 쉬워야 합니다.

가능한 한 함수를 단순 람다 표현식으로 축약하고 합성과 모나드로 조합해서 쓰는게 FP 의 철학 요체입니다.

순환 복잡도는 함수의 선형 독립적인 경로의 개수를 측정하기 위한 정량적인 소프트웨어 지표이며, 노드와 간선이 등장하는 간단한 그래프 이론을 기반으로 합니다.

  • 노드 : 더 이상 나눌 수 없는 코드 블록
  • 코드 블록 두 개가 있고 2번 블록이 1번 이후에 실행 가능할 때 방향성 있는 간선으로 연결

수학적으로 프로그램의 복잡도를 M 이라 할때 M = E - N + P 로 계산합니다. (E : 간선개수 / N : 노드나 블록의 개수 / P : 출구 있는 노드의 개수)

 

대체로 명령형 코드의 경우 조건문에 따라 그 경로가 늘어나므로 복잡도 또한 증가합니다. 함수형 코드에선 루프나 조건문 대신 고계함수, 함수 조합기 등의 추상화 장치를 쓰기 때문에 순환 복잡도는 대개 1에 가까운 값이 나옵니다.

 

6.6 Summary

 

  • 아주 단순한 함수들을 결합하는 추상화로 프로그램을 모듈화 가능
  • 순수함수에 기반을 둔 모듈적인 코드는 테스트하기 쉽고, 속성 기반 테스트처럼 더 엄격한 테스트 방법론 적용 가능
  • 테스트 가능한 코드가 되려면 제어 흐름이 직관적이어야 함
  • 제어 흐름을 단순화하면 전체 프로그램의 복잡도가 줄어드며, 복잡도는 각종 지표를 통해 정량적 측정 가능
  • 복잡도가 줄면 프로그램을 읽고 이해하기 쉬움

 

 

출처 : 함수형 자바스크립트(책)