식별할 수 있는 동작(Observable behavior) - 테스트 대상 코드를 구분하는 기준

4 minute read

구현 세부 사항(Implementation Details)을 테스트하지 말아야 한다. 리팩터링하면 쉽게 깨지는 테스트가 되기 때문이다. 테스트 코드는 리팩터링을 안전하게 할 수 있게 지켜주는 방어막인데, 장애물이 되어 버린다. 리팩터링했는데, 고쳐야 하는 테스트가 많다면 구현 세부 사항을 테스트하고 있다는 나쁜 신호이다.

그렇다면 어떤 코드를 테스트해야 할까? ’단위 테스트 (블라디미르 코리코프, 2021)’ 책 5장에 좋은 설명이 있어서 정리했다.

좋은 유닛 테스트는 리팩터링 내성을 가진다

좋은 단위 테스트의 두 번째 특성은 리팩터링 내성이다. 이는 테스트를 ’빨간색(실패)’으로 바꾸지 않고 기본 애플리케이션 코드를 리팩터링할 수 있는지에 대한 척도이다.

단위 테스트 (블라디미르 코리코프, 2021) 4장

단위 테스트가 리팩터링을 잘 견딜 수 있어야 한다. 리팩터링 내성이 없는 테스트가 가득이라면? 구현 세부 사항을 고치는데 테스트가 잔뜩 깨진다. 잘못 고쳤구나. 테스트가 잘못 고친 것 체크해주네. 이 맛에 테스트를 짜는 거지. 어디 보자. 뭐야 제대로 동작하는데 테스트를 잘못 짠 거잖아. 왜 이렇게 ’구현 세부 사항(Implementation Details)’을 테스트하는 거지? 뭐 좀 고쳤다고 하면 테스트가 잔뜩 깨져서 리팩터링 비용이 커진다. 원래 테스트가 리팩터링을 도와줘야 하는데, 리팩터링 비용만 증가시킨다.

리팩터링 내성이 없는 테스트가 많은 건 안 좋은 신호다.

그래. ’구현 세부 사항’을 테스트하면 안 된다. 알겠다. 그럼 어떤 걸 테스트해야 하나? 부르는 용어라도 있나?

’식별할 수 있는 동작(Observable Behavior)’이란 무엇인가?

모든 제품 코드는 2차원으로 분류할 수 있다.

  • 공개 API 또는 비공개 Api
  • 식별할 수 있는 동작 또는 구현 세부 사항

단위 테스트 (블라디미르 코리코프, 2021) 5장

공개 API가 식별할 수 있는 동작이면 베스트지만 우리가 항상 그렇게 설계를 잘하는 건 아니다. 실수할 때가 있다. 구현 세부 사항을 공개 API로 노출할 수도 있다. 즉, 공개 API가 무조건 식별할 수 있는 동작이 되는 건 아니다.

코드가 시스템의 식별할 수 있는 동작이려면 다음 중 하나를 해야 한다.

  • 클라이언트가 목표를 달성하는 데 도움이 되는 연산(operation)을 노출하라. 연산은 계산을 수행하거나 부수효과(side effect)를 초래하거나 둘 다 하는 메서드다.
  • 클라이언트가 목표를 달성하는 데 도움이 되는 상태(state)를 노출하라. 상태는 시스템의 현재 상태다.

구현 세부 사항은 이 두 가지 중 아무것도 하지 않는다.

단위 테스트 (블라디미르 코리코프, 2021) 5장

여기서 클라이언트는 코드를 사용하는 다른 코드나 프로그램이다. 코드를 누가 사용하는지를 확인해야 식별할 수 있는 동작을 정의할 수 있다. 예제를 하나 살펴보자.

  1. 사용자의 이메일이 회사 도메인에 속한 경우 해당 사용자는 직원으로 표시된다. 그렇지 않으면 고객으로 간주한다.
  2. 시스템은 회사의 직원 수를 추적해야 한다. 사용자 유형이 직원에서 고객으로, 또는 그 반대로 변경되면 이 숫자도 변경해야 한다.
  3. 이메일이 변경되면 시스템은 메시지 버스로 메시지를 보내 외부 시스템에 알려야 한다.

단위 테스트 (블라디미르 코리코프, 2021) 7장

위와 같은 요구사항을 충족하는 서비스를 다이어그램에서 ’이메일 변경 서비스’로 표시한다. ’이메일 변경 서비스’ 코드를 사용하는 외부 클라이언트가 ’식별할 수 있는 동작’은 어떤 게 있을까?

flowchart LR C["외부 클라이언트"] -->|외부 클라이언트의 \n식별할 수 있는 동작| S["이메일 변경 서비스"] S -->|외부 클라이언트의 \n식별할 수 있는 동작| M["메시지 버스"]

’이메일 변경 서비스’를 호출할 수 있는 Public API와 이메일이 변경되면 외부 시스템에 알리는 ’메시지 버스’를 식별할 수 있다. 예를 들어 이메일 변경 API가 있고 그 API에서 이메일 변경 서비스를 호출한다. API를 테스트하는 코드에서는 메시지 버스로 이메일 변경 알림이 오는지를 테스트하면 된다. 거기에서 멈춰야 한다. 더 ’이메일 변경 서비스’를 파고들어서 테스트하면 안 된다.

이제 레이어를 한꺼풀 벗겨서 한 뎁스 밑으로 내려와서 테스트해보자. 테스트 대상은 ’이메일 변경 서비스’가 된다. 테스트 파일들도 ’이메일 변경 서비스’를 구현한 파일 이름 뒤에 _test 를 붙인 파일에 구현할 것이다. 식별할 수 있는 동작은 어떻게 달라질까?

flowchart LR S["이메일 변경 서비스"] -->|이메일 변경 서비스의\n식별할 수 있는 동작| U["사용자"] S -->|이메일 변경 서비스의\n식별할 수 있는 동작| D["회사"] S -->|이메일 변경 서비스의\n식별할 수 있는 동작| E["이벤트"]

’사용자’, ’회사’가 식별할 수 있는 대상이 된다. 입력받은 이메일 주소를 검사해서 직원 혹은 고객으로 구분해야 하며 직원 수도 추적해야 한다. ’메시지 버스’는 더이상 식별할 수 있는 동작이 아니다. 이메일 변경 서비스를 테스트할 때는 ’메시지 버스’의 존재를 잊어야 한다. 대신 이메일로도 보낼 수 있고 버스로도 보낼 수 있고 혹은 로그로 남길 수 있는 ’이벤트’를 만드는 것을 테스트한다. 이렇게 ’식별할 수 있는 동작’은 어떤 대상을 테스트하느냐에 따라 달라진다.

전체 그림을 살펴보자.

flowchart LR C["외부 클라이언트"] ==>|외부 클라이언트의 \n식별할 수 있는 동작| S["이메일 변경 서비스"] S ==>|외부 클라이언트의 \n식별할 수 있는 동작| M["메시지 버스"] S -->|이메일 변경 서비스의\n식별할 수 있는 동작| U["사용자"] S -->|이메일 변경 서비스의\n식별할 수 있는 동작| D["회사"] S -->|이메일 변경 서비스의\n식별할 수 있는 동작| E["이벤트"]

만약 ’이메일 변경 서비스’를 호출하는 코드를 테스트한다면 ’메시지 버스’까지만 테스트해야 한다. ’사용자’, ’회사’, ’이벤트’는 ’구현 세부 사항’이 된다. ’이메일 변경 서비스’를 테스트한다면 ’사용자’, ’회사’, ’이벤트’를 테스트하면 된다. 만약 ’이벤트’ 코드를 리팩터링한다면 ’이메일 변경 서비스’ 테스트 코드까지만 깨져야 한다. 그 위의 레이어에 해당하는 ’외부 클라이언트’가 ’이메일 변경 서비스’를 사용하는 테스트 코드는 리팩터링 내성을 가져야 한다.

마치며

인터넷에서 본 좋은 지침이 있다. ’How’를 테스트하지 말고 ’What’을 테스트하라. 어떻기에 해당하는 구현 세부 사항을 테스트하지 말고 무엇을 하는지에 대한 테스트를 하라는 지침이다. 한 문장으로 표현할 수 있는 좋은 기준이지만 무엇이 ’What’에 해당하는지에 대한 기준이 모호하다.

’구현 세부 사항(Implementation Details)’이 여기서 ’How’를 가리키고 ’식별할 수 있는 동작(Observable Behavior)’은 ’What’을 가리킨다. ’식별할 수 있는 동작’에 대한 가이드가 좋아서 테스트를 짤 때, 도움이 된다. ’단위 테스트 (블라디미르 코리코프, 2021)’ 책을 좀 더 빨리 알았더라면 테스트를 짜는 시행착오를 많이 줄일 수 있었을 텐데 아쉽다.

’식별할 수 있는 동작(Observable Behavior)’은 뭐랄까 좀 더 나은 용어가 어디엔가 있을 것 같은 용어다. 하지만 더 나은 용어가 떠오르지는 않으니 이걸 앞으로 사용할 생각이다.