본문 바로가기

책 리뷰/엘레강트 오브젝트

엘레강트 오브젝트 [2장 - (4)]

⌗ 엘레강트 오브젝트 책을 리뷰해보도록 하겠습니다.

책의 내용은 다양한 언어로 되어있지만, 저는 예시 코드로 Typescript 를 사용해 제 사족을 조금씩 붙일 예정입니다.

 

문서를 작성하는 대신 테스트를 만드세요

 

문서화는 유지보수에 있어 중요한 구성요소입니다.

구체적인 문서 작성이 중요하다기보단, 클래스나 메서드에 관한 추가 정보에 얼마나 쉽게 접근할 수 있는지가 중요합니다.

내가 작성한 코드를 누군가가 읽을 때 세부 사항, 추가 정보 등 부가 설명이 필요하기 때문인데요.

 

더 읽기 쉬운 코드를 만들기 위해선 코드를 읽을 사람이 거의 주니어 프로그래머라고 가정해야 합니다.

나쁜 프로그래머는 복잡한 코드를 짜고, 훌륭한 프로그래머는 단순한 코드를 짭니다. 이 말은 회사에서의 경험을 같이 생각해보면 참 공감되는 말인 것 같네요. 이상적인 코드는 스스로를 설명하기 때문에 어떤 추가 문서도 필요하지 않습니다.

 

const jeff: Employee = department.employee('Jeff');
jeff.giveRaise(new Cash(5000));

if (jeff.performance() < 3.5) {
  jeff.fire();
}

 

위 코드는 딱히 어떤 문서가 필요없을 수준의 코드입니다. 그 자체로 의미를 잘 전달하고 있기 때문이죠.

 

class Helper {
  saveAndCheck(x: number): number { ... }
  extract(text: string): number { ... }
  convert(value: number, extra: boolean): boolean { ... }
}

 

그에 반해 이런 코드는 클래스명부터가 코드 내에서 이곳저곳에 쓰일 것 같은 느낌을 내는데, 메서드 시그니처로 뭔가를 정확히 알기가 어렵다보니 주석부터 시작해서 어떠한 문서화가 필요하게 됩니다. 이러한 나쁜 설계가 문서화를 강요합니다.

 

코드를 문서화하는 대신 코드를 깔끔하게 만드는 것을 우선시하기 바랍니다.

여기서 깔끔하게 만드는 것은 단위 테스트도 함께 만드는 것을 포함합니다.

단위 테스트는 클래스의 일부이지 독립적인 개체가 아니며, 단위 테스트가 바로 문서화입니다.

 

describe('Cash test', () => {
  it('test summarizes', () => {
    const cash = new Cash(5);
    expect(cash.plus(3)).toMatchObject(new Cash(8));
  });

  it('test deducts', () => {
    const cash = new Cash(7);
    expect(cash.plus(-11)).toMatchObject(new Cash(-4));
  });

  it('test multiplies', () => {
    const cash = new Cash(2);
    expect(cash.mul(3)).toMatchObject(new Cash(6));
  });
});

 

저자가 얘기하는 훌륭하다고 생각하는 Cash 클래스의 단위 테스트를 jest 로 구현한 내용입니다.

단위 테스트를 올바르게 관리한다면 실제 클래스보다 단위 테스트를 훨씬 더 자주 읽게 될 것입니다.

스펙 코드 자체로 클래스나, 함수의 명세가 되어야 한다고 생각해왔었기 때문에 상당히 동의가 되는 내용이었습니다.

 

모의 객체(Mock) 대신 페이크 객체(Fake) 를 사용하세요

 

테스트를 할 때 전통적으로 많이 사용해왔던 방식은 모킹입니다. 대부분의 프로그래머들이 모킹에 대해 이미 잘 알고 있으나 모킹은 나쁜 프랙티스이며 최후의 수단으로만 사용해야 합니다. 모킹 대신, 페이크 객체를 사용해야 합니다.

 

const mockingObject: { [key: string]: number } = { ... };

jest.mock('../../../src/config', () => ({ configProps: { ... } }));
jest.mock('../../../src/lib/campaign', () => ({ getSpent: () => mockingObject }));

 

애초에 설계를 잘못해서 이렇게 모킹을 해야하는 수준이 된 것 같기도 합니다만, 대체로 jest 에서 모킹을 사용할 경우 위와 같은 코드가 생성됩니다. 이렇게 작업할 경우 모킹은 구현과 관련된 내부 세부사항을 테스트와 결합시킵니다.

반면 서로 계약에 의해 동작하도록 설계를 하고 페이크 클래스를 통해 테스트를 구현했다면, 구현과 관련된 세부사항을 알 필요가 없게되고 유지보수에 더욱 용이해집니다. 객체 내부의 구현 세부사항을 알면 테스트가 취약해지고 유지보수가 어려워지며, 근본 원인은 모킹입니다.

 

이 책에선 java 로 예시가 되어있기 때문에 여러분이 만드는 인터페이스부터 페이크 클래스를 함께 제공하라고 되어있지만, typescript 에선 말을 약간 바꿔 인터페이스에 의해, 즉 계약에 의해 동작하도록 설계를 변경하고 해당 인터페이스를 구현하는 페이크 클래스를 이용해 테스트 코드를 작성하시길 바랍니다.

 

다만 페이크 클래스를 만족하도록 테스트를 작성하지 말고 페이크 클래스가 테스트를 올바르게 지원하도록 만들어야 하며, 페이크 클래스를 만들다보면 필연적으로 여러 고민을 하게 되어 인터페이스를 개선하게 됩니다.