테스트를 개발의 일부로 만들기

이재상·2026년 1월 25일

요약
1. 테스트는 개발의 일부이며, 별개로 생각하지 말고 개발과 함께 생각해야한다.
2. 불필요한 테스트가 아닌 실제 사용자 흐름을 고려한 테스트 설계와 테스트 구현에 있어서 스트레스 받지 않고 가벼운 테스트 환경을 구축한다.
3. Unit Test는 MemoryDB를 활용해서 Mocking을 최소화하고, E2E Test는 K6를 활용해서 다양한 테스트를 수행한다.

서론

개발자가 되고 초기에는 스타트업에서 빠르게 개발을 해야한다고 생각을 해서 테스트를 구현하지 않았다.
테스트 코드 작성 자체가 시간 낭비라고 생각했었다. 하지만 돌이켜보면, 테스트를 어떻게 효율적으로 구현하고 활용해야 하는지 몰랐기 때문에 가졌던 막연한 거부감이었다.

테스트가 없는 상태로 1~2년 제품을 개발하다보니 제품의 안정성이 심각해져서 요구 사항 개발보다 버그 픽스가 많아지고 픽스를 해도 사이드 이펙트로 다른곳에서 깨지는 경우가 잦았다.
그래서 그 당시에 아래와 같은 내용을 깨달았다.

  • 테스트 코드 구현 시간보다 추후에 버그 픽스에 대한 시간이 더 많이 소모된다.
  • 매번 불안해하며 릴리즈를 하게 됐는데, 테스트 코드가 있으면 최소한의 안전성이 보장되기 때문에 덜 불안해한다.
  • 개발을 하다보면 리펙토링이나 요구사항에 따라 코드를 변경하는 일이 잦은데, 테스트 코드가 있다면 사이드 이펙트에 대해서 덜걱정해도 된다.
  • 다만 테스트 커버리지가 100%라고 해서 버그가 없는 것은 아니다. 하지만 테스트 코드는 제품 안정성의 '저점(Low Point)'을 높여주는 확실한 안전장치 역할을 한다.

이러한 내용을 깨달으면서 어떻게 테스트를 설계하고 활용했는지 작성하려고 한다.

본론

이 글에서는 Unit Test와 E2E Test를 통한 제품의 안정성을 올리는 과정을 설명한다.

Unit Test

Unit Test의 역할

Unit Test는 개발자가 코드를 작성하면서 가장 자주 실행하는 테스트다.
그래서 무엇보다도 빠르고 가벼워야 하며, 테스트를 실행하는 행위 자체가 부담이 되어서는 안 된다.

내가 생각하는 Unit Test의 역할은 다음과 같다.

  • 비즈니스 로직이 의도한 대로 동작하는지 빠르게 검증한다.
  • 코드 변경이나 리팩토링 시 사이드 이펙트를 조기에 발견한다.
  • 개발 중 언제든 실행할 수 있을 정도로 실행 시간이 짧아야 한다.

따라서 Unit Test에서는

  • 함수 단위 또는 Service / Class 단위의 로직을 중심으로 테스트하고
  • 외부 시스템에 대한 의존성은 최소화하며
  • 과한 Mocking으로 실제 동작과 괴리된 테스트가 되지 않도록 주의했다.

Unit Test를 구현하기 위해 Javascript에서 가장 많이 사용되는 Jest를 사용했다.
참고로 VSCode Extension에 Jest Runner라는 Extension이 있는데, 해당 Extension을 사용하면 간편하게 Unit Test를 실행할 수 있어 편리하다.

MemoryDB

Unit Test를 구현하면서 제일 고민이였던 부분이 Database에 대한 의존성이였다.
실제 Database를 띄워서 진행하면 가벼운 테스트가 아니게 되고, 관리가 필요해지는 문제가 발생했다.
이러한 고민을 하던 와중 MemoryDB를 사용하기로 했다.

PostgreSQL에는 pg-mem이라는 라이브러리가 있어서 사용했고, MongoDB에는 mongodb-memory-server라는 라이브러리를 사용했다.
pg-mem은 특정 윈도우 함수나 문법을 지원하지 않아 우회하거나 Mocking이 필요한 경우가 있었다. 반면 mongodb-memory-server는 실제 MongoDB 바이너리를 구동하므로 문법 호환성 문제없이 모든 기능을 활용할 수 있었다.

또한 pg-mem은 backup 기능이 존재해서, 초기 테이블을 생성하고 backup을 만든 다음 테스트마다 restore해서 테스트 환경을 초기화해 테스트 성능을 좋게 만들 수 있다.
MongoDB는 아쉽게도 backup 기능이 존재하지 않아서, global beforeEach와 global afterEach를 사용해서 collection을 초기화하는 방향으로 구현했다.

Unit Test 설계

일반적으로 Service Layer Test를 가장 신경써서 구현을 한다.
이에 Service별 Test 코드가 존재하고, Method별로 Test Suite가 있으며 그 아래 사용자 행위별 Test Case가 있는 방식으로 구현했다.

describe('UserService', () => {
  describe('createUser', () => {
    it('정상: 유저가 생성된다.', async () => { 
      // Given
      // When
      // Then
    })

    it ('실패: 이미 존재하는 유저명으로 생성하면 실패한다.', async () => { 
      // Given
      // When
      // Then
    })

    it ('실패: 유저명이 비어있으면 실패한다.', async () => { 
      // Given
      // When
      // Then
    })
  })
})

특히 이전부터 사용되던 Given/When/Then 패턴을 사용하면서 테스트 코드를 구현하면 가독성도 좋아지고 명확하게 테스트 코드를 구현할 수 있다.

과한 Test

간혹 Test Coverage를 100%로 만들기 위해서나, 거의 발생하지 않을 케이스에 대한 테스트를 구현하기 위해 과하게 테스트 코드를 구현하는 경우가 종종있다.
이는 추후 Test CI나 로컬 환경에서 실행할 때 시간이 오래 걸리게 되고, 스트레스를 유발하게 된다.
따라서 무작정 테스트 코드를 구현하기 보다는 정말 이 Test가 필요한지, 추후 사이드 이펙트에 대한 방어가 잘 대처가 되는 코드인지에 대해서 고민해야 한다.
혹은 버그가 발생해도 영향 범위가 매우 적은 경우나 변경이 거의 이뤄지지 않는 코드에 대해서는 테스트 코드를 구현하지 않는 것도 좋은 선택이 될 수 있다.

그래서 이전에는 모든 코드에 대한 Test Code를 구현을 하다가, 최근에는 Service/Repository 위주의 테스트 코드를 작성하게 됐다.

E2E Test

E2E Test의 역할

E2E Test는 Unit Test로 커버하기 어려운 영역을 검증하기 위한 테스트다.
코드의 내부 구현보다는 실제 사용자의 흐름이 문제없이 동작하는지를 확인하는 데 목적이 있다.

E2E Test에서는 다음과 같은 부분을 중점적으로 확인했다.

  • 인증 / 인가, Middleware, Guard, Interceptor가 정상적으로 동작하는지
  • 여러 API가 조합된 실제 사용자 시나리오가 문제없이 수행되는지
  • 실제 Database를 사용했을 때 발생할 수 있는 설정 또는 연동 문제

이 때문에 E2E Test는 실제 Database를 사용하고 Production 환경과 최대한 유사한 구성으로 테스트를 진행했다.
실행 속도는 Unit Test보다 느리지만, 대신 시스템 전반에 대한 신뢰도를 확보하는 용도로 사용했다.

E2E Test 구현을 위해 어떤 Framework을 사용할지 고민을 하다가 K6를 사용하기로 했는데, 그 이유는 아래와 같다.

  1. Go 기반이라 가볍고, 스트레스 테스트를 수행해야할 때 설정이 아주 쉽다.
  2. Go 기반이지만 Javascript 문법을 지원하고, 특히 Typescript 문법도 지원하기 때문에 기존에 사용하던 Type들을 활용해서 구현할 수 있다.
  3. Rest/gRPC 테스트가 모두 필요했는데 간단하게 모두 지원했다.

테스트 구현 방법

K6를 사용한 E2E Test는 테스트 코드 자체의 재사용성과 가독성을 가장 중요하게 두고 구현했다.
단순히 요청을 던지고 응답 코드만 확인하는 형태가 아니라, 실제 사용자 시나리오를 코드로 표현하는 데 집중했다.

  • 하나의 테스트 파일이 하나의 사용자 시나리오를 담당하도록 구성
  • 인증이 필요한 경우, 로그인 → 토큰 발급 → 이후 요청 흐름을 하나의 시나리오로 묶어서 테스트
  • 단일 API 테스트보다는 여러 API가 연속으로 호출되는 흐름 위주로 작성

특히 E2E Test에서는 내부 구현에 대한 검증보다는
“이 요청이 성공했을 때 다음 요청이 정상적으로 이어질 수 있는가”에 초점을 맞췄다.
그래서 response body의 세부 필드보다는, 다음 요청에 필요한 핵심 값들만 검증하고 전달하는 방식으로 테스트를 구성했다.


테스트 활용 방법

E2E Test는 Unit Test처럼 항상 실행하는 테스트는 아니었지만, 실행 시점과 목적을 명확히 구분해서 활용했다.

  • 로컬 환경
    • 서버를 직접 띄운 상태에서 특정 시나리오만 선택적으로 실행
    • 신규 기능 개발 후 전체 흐름이 깨지지 않았는지 빠르게 확인하는 용도
  • CI 환경 (GitHub Actions)
    • Infra(Database, Cache 등)를 Container로 구성해서 테스트 환경을 구성
    • 테스트 실행 전후로 Database를 초기화해서 테스트 결과가 서로 영향을 주지 않도록 처리
    • 매 테스트에서 실행하는 것이 아닌, 릴리즈 전에 실행해서 최소한의 안전장치로 활용

CI에서의 실행 시간은 분명히 Unit Test보다 오래 걸렸지만, 대신 배포 전에 최소한의 시스템 안정성을 확보하는 안전장치 역할을 해줬다.
특히 인증/인가나 설정 누락과 같은 문제는 이 단계에서 대부분 잡을 수 있었다.

K6를 E2E Test로 사용한 방식

K6는 흔히 Stress Test 도구로 많이 사용되지만, 이 글에서는 E2E Test 프레임워크로 활용했다.

  • 기본적으로는 단일 VU(virtual user)로 실행
  • 필요할 경우 동일한 시나리오를 여러 VU로 실행해서 간단한 동시성 문제를 확인
  • 테스트 목적에 따라 옵션만 변경해서 E2E Test와 간단한 스트레스 테스트를 겸용으로 사용

이 방식의 장점은, E2E Test를 작성해두면 이후에 별도의 테스트 코드를 새로 작성하지 않고도 Stress 테스트로 확장할 수 있다는 점이었다.

K6의 단점

K6는 충분히 강력한 도구였지만, 운영하면서 아쉬운 점도 분명히 있었다.

첫번째로 Javascript 문법을 모두 지원하지 않는다.
K6는 Go 기반으로 JS 문법을 실행하는 구조라 Node.js 런타임이 아니다. 따라서 npm 패키지나 Node Native Module(fs, crypto 등)을 직접 사용할 수 없다는 제약이 있었다.

두번째로 테스트 편의 기능이 부족했다.
초기에는 expect와 같은 assertion 도구가 기본 제공되지 않아 간단한 검증 로직도 직접 구현해야 했다. 최근 버전에서는 일부 기능이 개선되었지만, 여전히 Jest 수준의 편의성은 아니다.

그럼에도 K6는 Stress Test에서 사용하기 편리하고 성능이 좋았으며, gRPC 구현이나 다른 방면 테스트 구현에 있어 편리해서 사용하게 됐다.

결론

현재 회사로 이직하고서 맨 처음에 테스트 코드가 하나도 없고, 매일 버그가 발생해서 모두가 야근을 하는 모습을 보면서 테스트 코드를 제안했다.
하지만 대부분의 사람들이 테스트 코드 작성에 대한 거부감이 있었고, 과거의 나와 동일하게 생각을 했다.
그중 몇분은 제안을 들어줘서 테스트 코드를 구현을 시작했고, 테스트 코드를 작성해 본 동료들은 점차 그 효용을 체감했고, 자연스럽게 팀 내에 테스트 문화가 정착되었다.
테스트 문화가 정착된 이후 간단한 버그나 요구사항에 맞지 않는 버그가 거의 발생하지 않는 수준으로 줄어들었고, 코드 리펙토링에 대한 두려움이 많이 줄어들었다.
이를 통해 PostgreSQL에서 MongoDB로 Migration을 하는 과정에서도 문제 없이 잘진행할 수 있었다.

테스트 코드를 구현하지 않는 조직에 있는 개발자분들을 만날때마다 이야기하는 내용이 있다.
모든 회사의 개발자들은 모두 바쁘고, 당장 릴리즈를 쫒아가느라 고생하지만 테스트 코드를 짜는 조직이 있고 아닌 조직이 있다. (당연하지만 QA팀이 있다고 테스트 코드가 없어도 괜찮은건 아니다.)
테스트를 구현한다고 여유로운 조직이 아니라, 미래의 시간을 아끼기 위해 투자라고 생각하고 구현을 한다고 생각한다.
한번도 테스트 코드를 구현하지 않았다면, 시간 낭비라고 단정짓지 말고 테스트 문화를 도입한 다음에 판단해도 늦지 않는다고 생각한다.
특히 요즘은 AI Assistant의 도움으로 테스트 코드 작성 비용이 획기적으로 줄어들었다. 설계만 잘해둔다면 더 이상 '시간이 없어서' 못 한다는 핑계는 통하지 않는 시대가 되었다.

profile
문제를 코드로만 보지 않고 구조와 흐름으로 해결하는 백엔드 개발자

0개의 댓글