박종훈 기술블로그

데이터베이스 테스트 - 10장

10장 데이터베이스 테스트 단위테스트 (블라디미르 코리코프)


10장에서 다루는 내용

  • 데이터베이스 테스트를 위한 전제 조건
  • 데이터베이스 테스트 모범 사례
  • 테스트 데이터 생명 주기
  • 테스트 내 데이터베이스 트랜잭션 관리

통합 테스트 라는 퍼즐의 마지막 조각은 프로세스 외부 관리 의존성이다. 가장 일반적인 예시는 애플리케이션 데이터베이스다. 애플리케이션 데이터베이스는 다른 애플리케이션이 접근할 수 없는 데이터베이스다.

실제 데이터베이스를 테스트하면 회귀 방지가 아주 뛰어나지만 설정하기가 쉽지 않다. 이 장에서는 데이터베이스 테스트를 시작하기 전에 거쳐야 할 준비 단계를 살펴본다. 데이터베이스 스키마(schema)를 추적하고, 상태 기반 데이터베이스 배포 방식과 마이그레이션 기반 데이터베이스 배포 방식의 차이점을 설명하며, 상태 기반보다 마이그레이션 기반을 선택해야 하는 이유를 알아본다.

기본 지식을 학습한 후 테스트 중에 트랜잭션을 관리하는 방법과 남은 데이터를 정리하는 방법, 그리고 중요하지 않은 부분을 제거하고 필수 요소를 강조해 테스트를 작게 할 수 있는 방법을 살펴본다.

10.1 데이터베이스 테스트를 위한 전제 조건

통합 테스트에서는 관리 의존성이 그대로 있어야 한다. 관리 의존성에는 목을 사용하는 것은 불가능하기 때문에 비관리 의존성보다 작업하기가 더 힘들 수 있다. 그러나 테스트를 작성하기 전에 통합 테스트가 가능하게끔 준비 단계를 수행해야 한다.

이 절에서는 다음 과 같은 전제 조건을 살펴본다.

  • 형상 관리 시스템에 데이터베이스를 유지
  • 모든 개발자를 위한 별도의 데이터베이스 인스턴스 사용
  • 데이터베이스 배포에 마이그레이션 기반 방식 적용

다른 것들과 마찬가지로 테스트를 용이하게 하면 보통 데이터베이스 상태도 개선된다.

10.1.1 데이터베이스를 형상 관리 시스템에 유지

데이터베이스를 테스트하는 방법의 첫 번째 단계는 데이터베이스 스키마를 일반 코드로 취급하는 것이다. 일반 코드와 마찬가지로 데이터베이스 스키마를 Git과 같은 형상 관리 시스템에 저장하는 것이 최선이다.

모델 데이터베이스를 사용하는 것은 데이터베이스 스키마를 유지하는데 상당히 좋지 못한 방법이다. 이유는 다음과 같다.

  • 변경 내역 부재: 데이터베이스 스키마를 과거의 특정 시점으로 되돌릴 수 없다. 이는 운영 환경에서 버그를 재현할 때 중요할 수 있다.
  • 복수의 원천 정보: 모델 데이터베이스는 개발 상태에 대한 원천 정보를 둘러싸고 경합하게 된다. 이렇게 기준을 두 가지(Git과 모델 데이터베이스)로 두면 부담이 가중된다.

반면 모든 데이터베이스 스키마 업데이트를 형상 관리 시스템에 두면 원천 정보를 하나로 할 수 있고, 일반 코드 변경과 함께 데이터베이스 변경을 추적할 수 있다. 형상 관리 외부에서는 데이터베이스 구조를 수정하면 안 된다.

10.1.2 참조 데이터도 데이터베이스 스키마다

데이터베이스 스키마 와 관련해서는, 일반적으로 테이블, 뷰, 인덱스, 저장 프로시저 및 데이터베이스의 구조를 형성하는 기타 다른 요소들이 고려됩니다. 번역수정

스키마는 SQL 스크립트 형태로 표현된다. 개발 중에 언제든지 이러한 스크립트로 기능을 완전히 갖춘 최신 데이터베이스 인스턴스를 만들 수 있어야 한다.

데이터베이스 스키마에 속하지만, 데이터베이스 스키마로 거의 여기지 않는 부분이 있는데, 바로 참조 데이터(reference data)다.

[팁] 참조 데이터와 일반 데이터를 구별할 수 있는 간단한 방법은 다음과 같다. 애플리케이션이 데이터를 수정할 수 있으면 일반 데이터이고, 그렇지 않으면 참조 데이터이다.

참조 데이터는 애플리케이션의 필수 사항이므로, 테이블, 뷰 그리고 다른 데이터베이스 스키마와 함께 SQL INSERT 문 형태로 형상 관리 시스템에 저장해야 한다.

참조 데이터는 보통 일반 데이터와 별도로 저장되지만, 두 데이터가 동일한 테이블에 공존하는 경우도 있다. 이러헥 하려면 수정할 수 있는 데이터(일반 데이터)와 수정할 수 없는 데이터(참조 데이터)를 구분하는 플래그를 두고, 애플리케이션이 참조 데이터를 변경하지 못하게 해야 한다.

10.1.3 모든 개발자를 위한 별도의 데이터베이스 인스턴스

실제 데이터베이스로 테스트하는 것은 충분히 어렵다. 다른 개발자들과 데이터베이스를 공유해야 한다면 훨씬 더 어려워진다. 공유 데이터베이스를 사용하면 개발 프로세스를 방해하게 된다. 그 이유는 다음과 같다.

  • 서로 다른 개발자가 실행한 테스트는 서로 간섭되기 때문이다.
  • 하위 호환성이 없는 변경으로 다른 개발자의 작업을 막을 수 있기 때문이다.

테스트 실행 속도를 극대화 하려면 개발자마다 별도의 데이터베이스 인스턴스를 사용하라 (가능하면 개발자 머신에서)

10.1.4 상태 기반 데이터베이스 배포와 마이그레이션 기반 데이터베이스 배포

상태 기반 방식

개발 내내 유지 보수하는 모델 데이터베이스가 있다. 배포 중에 비교 도구가 스크립트를 생성해서 운영 데이터베이스를 모델 데이터베이스와 비교해 최신 상태로 유지한다.

상태 기반 방식을 사용하면 물리적인 모델 데이터베이스는 원천 데이터가 아니다. 대신 해당 데이터베이스를 작성하는 데 사용할 수 있는 SQL 스크립트가 있다. 스크립트는 형상 관리에 저장된다. 운영 데이터베이스의 상태와 관계없이, 비교 도구는 불필요한 테이블을 삭제하고 새 테이블을 생성하고 컬럼명을 바꾸는 등 모델 데이터베이스와 동기화하는 데 필요한 모든 작업을 수행한다.

마이그레이션 기반 방식

데이터베이스를 어떤 버전에서 다른 버전으로 명시적으로 마이그레이션을 한다. 이 방식은 운영 데이터베이스와 개발 데이터베이스를 자동으로 동기화하기 위한 도구를 쓸 수 없고, 업데이트 스크립트를 직접 작성해야 한다.

migration-example

마이그레이션은 일반적으로 SQL 스크립트로 표시하지만, DSL 같은 언어를 사용해 작성할 수도 있다. 대표적인 예시는 다음과 같다.

  • Flywaydb
  • Liquibase
  • FluentMigrator

상태 기반 방식보다 마이그레이션 기반 방식을 선호하라

  • 상태 기반 방식은 상태를 형상 관리에 저장함으로써 상태를 명시하고 비교 도구가 마이그레이션을 암묵적으로 제어할 수 있게 한다.
  • 마이그레이션 기반 방식은 마이그레이션을 명시적으로 하지만 상태를 암묵적으로둔다. 데이터베이스 상태를 직접 볼 수 없으며 마이그레이션으로 조합해야 한다.

데이터베이스 상태가 명확하면 병합 충돌을 처리하기가 수월한 반면, 명시적 마이그레이션은 데이터 모션 문제를 해결하는데 도움이 된다.

[정의] 데이터 모션은 새로운 데이터베이스 스키마를 준수하도록 기존 데이터의 형태를 변경하는 과정이다.

두 가지 다 중요해보이지만, 대부분의 프로젝트에서는 데이터 모션이 병합 충돌보다 훨씬 더 중요하다.

상태 기반 방식은 데이터 변경을 구현하기가 쉽지 않다. 데이터는 상황에 다라 달라지기 때문이다. 결과적으로 상태 기반 방식은 대다수의 프로젝트에서 실용적이지 않다.

[팁] 마이그레이션을 통해 데이터베이스 스키마에 (참조 데이터를 포함해) 모든 수정 사항을 적용하라. 형상 관리에 마이그레이션이 커밋(commit)된 후에는 수정하지 말라. 마이그레이션이 잘못된 경우 이전 마이그레이션을 수정하는 대신 새 마이그레이션을 생성하라. 잘못된 마이그레이션으로 인해 데이터가 손실될 수 있는 경우에만 이 규칙을 예외로 하라.

10.2 데이터베이스 트랜잭션 관리

데이터베이스 트랙잭션 관리는 제품 코드와 테스트 코드 모두에 중요한 주제다. 제품 코드에서 트랜잭션 관리를 적절히 하면 데이터 모순을 피할 수 있다. 테스트에서는 운영 환경에 근접한 설정으로 데이터베이스 통합을 검증하는 데 도움이 된다.

이 절에서는 먼저 제품 코드(컨트롤러)에서 트랜잭션을 처리하는 방법과 통합 테스트에서 트랜잭션을 사용하는 방법을 알아본다.

10.2.1 제품 코드에서 데이터베이스 트랜잭션 관리하기

읽기 전용 연산(예: 사용자 정보를 외부 클라이언트에 반환할 때) 중에는 여러 트랜잭션을 열어도 괜찮다. 그러나 비즈니스 연산에 데이터 변경이 포함된다면, 모순을 피하고자 이 연산에 포함된 모든 업데이트는 원자적이어야 한다.

예를 들어 A와 B를 이어서 저장하는 로직이 있다고 가정할 때, A에 대한 것은 잘 저장해도 이후 연결에 문제가 생겨 B에 대한 저장을 실패할 수 있다. 이럴 경우 데이터 불일치가 발생할 수 있다.

[정의] 원자적 업데이트(atomic update)는 모두 수행하거나 전혀 수행하지 않는 것이다. 원자적 업데이트 세트의 각 업데이트는 전체적으로 완료되거나 아무런 영향도 미치지 않아야 한다.

데이터베이스 트랜잭션에서 데이터베이스 연결 분리하기

잠재적인 모순을 피하려면 결정 유형을 두 가지로 나눠야 한다.

  • 업데이트 할 데이터
  • 업데이트 유지 또는 롤백 여부

이러한 분리는 컨트롤러가 결정을 동시에 내릴 수 없기 때문에 중요하다. 컨트롤러가 모든 비즈니스 로직의 모든 단계가 성공했을 때 업데이트를 유지할 수 있는지 여부를 알 수 있기 때문입니다. 그리고 비즈니스 로직을 수행하기 위해서는 데이터베이스에 액세스하고 업데이트를 시도해야만 합니다.

Database 클래스를 레포지토리(repository)와 트랜잭션으로 나눠서 이러한 책임을 구분할 수 있다.

  • 레포지토리는 데이터베이스의 데이터에 대한 접근과 수정을 가능하게 하는 클래스다.
  • 트랜잭션은 데이터 업데이트를 완전히 커밋하거나 롤백하는 클래스다. 데이터 수정의 원자성 확보를 위해 기본 데이터베이스 트랜잭션에 의존하는 사용자 정의 클래스다.

레포지토리와 트랜잭션은 책임과 수명이 다르다. 트랜잭션은 전체 비즈니스 연산 동안 있으며 연산이 끝나면 폐기된다. 반면에 레포지토리는 수명이 짧다. 데이터베이스 호출이 완료되는 즉시 레포지토리를 폐기할 수 있다. 레포지토리는 항상 현재 트랜잭션 위에서 작동한다. 데이터베이스에 연결할 때는 레포지토리가 트랜잭션에 등록해서 연결 중에 이뤄진 모든 데이터 수정 사항이 나중에 트랜잭션에 의해 롤백될 수 있도록 한다.

Commit 은 트랜잭션을 성공으로 표시한다. Dispose 는 트랜잭션을 종료한다. 비즈니스 연산이 끝날 때 항상 호출한다. 이전에 Commit 이 호출된 경우, Dispose는 모든 데이터 업데이트를 저장하고, 그렇지 않으면 롤백한다.

Commit 은 의사 결정이 필요하기 때문에 컨트롤러가 한다. Dispose 호출은 의사결정이 필요하지 않으므로 인프라 계층의 클래스에 메서드 호출을 위임할 수 있다.

JDBC의 경우 commit 과 rollback을 가지는 것으로 보인다. setAutoCommit(false); 을 통해 트랜잭션을 시작하고 이후 commit 을 통해 커밋을 하고 롤백이 필요하면 rollback 을 호출한다. setAutoCommit(true); 를 통해 트랜잭션을 종료할 수 있다.

작업 단위로 트랜잭션 업그레이드하기

레포지토리와 트랙잭션을 도입하여 잠재적인 데이터 모순을 피할 수 있었다. 여기서 더 개선해보자면 Trasaction 클래스를 작업 단위(unit of work)로 업그레이드 할 수 있다.

[정의] 작업 단위에는 비즈니스 연산의 영향을 받는 객체 목록이 있다. 작업이 완료되면, 작업 단위는 데이터베이스를 변경하기 위해 해야 하는 업데이트를 모두 파악하고 이러한 업데이트를 하나의 단위로 실행한다. (이러한 이유로 패턴 이름이 됨)

일반 트랜잭션과 비교해서 작업 단위가 갖는 가장 큰 장점은 업데이트 지연이다. 트랜잭션과 달리 작업 단위는 비즈니스 연산 종료 시점에 모든 업데이트를 실행하므로 데이터베이스 트랜잭션의 기간을 단축하고 데이터 혼잡을 줄인다. 이 패턴은 종종 데이터베이스 호출 수를 줄이는 데도 도움이 된다.

[참고] 데이터베이스 트랜잭션은 작업 단위 패턴도 구현한다.

실제로는 이러한 작업을 직접 할 필요가 없다. 대부분의 ORM(Objecct-Relational Mapping) 라이브러리가 작업 단위 패턴을 구현한다.

** 비관계형 데이터베이스에서의 데이터 모순 ** 관계형 데이터베이스를 사용할 때는 데이터 모순을 피하기가 쉽다. 모든 주요 관계형 데이터베이스는 필요한 만큼 여러 행에 걸쳐 원자적 업데이트를 제공한다. 그러나 MongoDB와 같은 비관계형 데이터베이스에서는 동일한 수준으로 어떻게 보호할 수 있을까? 대부분의 비관계형 데이터베이스가 갖는 문제점은 고전적인 의미에서 트랜잭션이 없다는 것이다. 원자적 업데이트는 단일 도큐먼트 내에서만 보장된다. 비즈니스 연산이 여러 문서에 영향을 주는 경우 모순이 생기기 쉽다.(비 관계형 데이터베이스에서 도큐먼트는 행과 동일함) 비관계형 데이터베이스는 모순을 다른 각도에서 접근한다. 한 번에 둘 이상의 도큐먼트를 수정하는 연산이 없도록 도큐먼트를 설계해야 한다. 관계형 데이터베이스의 행보다 도큐먼트가 더 유연하기 때문에 가능하다. 단일 도큐먼트는 어떠한 형태로든 그리고 아무리 복잡한 데이터라도 저장할 수 있으므로 아주 복잡한 비즈니스 연산 사이드 이펙트도 포착할 수 있다. 도메인 주도 설계에서는 비즈니스 연산단 둘 이상의 집계를 수정하면 안 된다는 지침이 있다. 이 지침은 데이터 모순으로부터 보호하는 것과 같은 목표가 있으며, 각 도큐먼트가 하나의 집계에 해당하는 도큐먼트 데이터베이스를 사용하는 시스템에만 적용된다.

10.2.2 통합 테스트에서 데이터베이스 트랜잭션 관리하기

통합 테스트에서 데이터베이스 트랜잭션을 관리하는 경우 다음 지침을 준수하라. 테스트 구절 간(책에서는 AAA, 준비-실행-검증)에 데이터베이스 트랜잭션이나 작업 단위를 재사용하지 말라.

[팁] 통합 테스트에서 적어도 세 개의 트랜잭션 또는 작업 단위를 사용하라(준비, 실행, 검증 구절당 하나씩)

10.3 테스트 데이터 생명 주기

공유 데이터베이스를 사용하면 통합 테스트를 서로 분리할 수 없는 문제가 생긴다. 이 문제를 해결하려면,

  • 통합 테스트를 순차적으로 실행하라.
  • 테스트 실행 간에 남은 데이터를 제거하라.

테스트는 데이터베이스 상태에 따라 달라지면 안 된다. 테스트는 데이터베이스 상태를 원하는 조건으로 만들어야 한다.

10.3.1 병렬 테스트 실행과 순차적 테스트 실행

통합 테스트를 병렬로 실행하려면 상단한 노력이 필요하다. 모든 테스트 데이터가 고유한지 확인해야 데이터베이스 제약 조건을 위반하지 않고 테스트가 다른 테스트 후에 입력 데이터를 잘못 수집하는 일이 없다. 남은 데이터를 정리하는 것도 까다로워진다. 성능 향상을 위해 시간을 허비하지 말고 순차적으로 통합 테스트를 실행하는 것이 더 실용적이다. 대안으로 컨테이너를 사용해 테스트를 병렬 처리할 수도 있다. 그러나 이러한 방식은 실제로 유지 보수 부담이 너무 커지게 된다.

도커를 사용하면 아래의 문제가 있다.

  • 데이터베이스를 추적하는데 어렵다.
  • 도커 이미지를 유지 보수 해야한다.
  • 각 테스트마다 컨테이너 인스턴스가 있는지 확인해야 한다.
  • 통합 테스트를 일괄 처리(Batch Processing)해야한다. (모든 컨테이너 인스턴스를 한 번에 만들 수 없기 때문에)
  • 다 사용한 컨테이너는 폐기해야 한다.

따라서 통합 테스트의 실행 시간을 최소화해야하는 경우가 아니라면 컨테이너를 사용하지 않는 것이 좋다.

일괄 처리(Batch processing) 한다는게 한번에 묶어 처리한다는 느낌이 들어 동시 처리와 무슨 차이가 있을까 생각이 들었는데 Batch processing의 뜻이 컴퓨터 프로그램 흐름에 따라 순차적으로 자료를 처리하는 방식 이라고 한다. 한국어와 영어의 어감의 차이인가 싶다.

10.3.2 테스트 실행 간 데이터 정리

테스트 실행 간에 남은 데이터를 정리하는 방법은 네 가지가 있다.

  • 각 테스트 전에 데이터베이스 백업 복원하기: 이 방법은 데이터 정리 문제를 해결할 수 있지만 다른 세 가지 방법보다 훨씬 느리다. 컨테이너를 사용하더라도 컨테이너 인스턴스를 제거하고 새 컨테이너를 생성하는 데 보통 몇 초 정도 걸리기 때문에 전체 테스트 스위트 실행 시간이 빠르게 늘어난다.
  • 테스트 종료 시점에 데이터 정리하기: 이 방법은 빠르지만 정리 단계를 건너뛰기 쉽다. 테스트 도중에 빌드 서버가 중단하거나 디버거에서 테스트를 종료하면 입력 데이터는 데이터베이스에 남아있고 이후 테스트 실행에 영향을 주게 된다.
  • 데이터베이스 트랜잭션에 각 테스트를 래핑하고 커밋하지 않기: 이 경우 테스트와 SUT에서 변경한 모든 내용이 자동으로 롤백된다. 이 접근 방식은 정리 단계를 건너뛰는 문제를 해결하지만 또 다른 문제를 제기한다. 이는 작업 단위를 재사용할 때와 같은 문제인데, 추가 트랜잭션으로 인해 운영 환경과 다른 설정이 생성되는 것이다.
  • 테스트 시작 시점에 데이터 정리하기: 이 방법이 가장 좋다. 빠르게 작동하고 일관성이 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다.

[팁] 삭제 스크립트는 일반 데이터를 모두 제거해야 하지만 참조 데이터는 제거하지 말아야 한다. 나머지 데이터베이스 스키마와 참조 데이터는 마이그레이션으로만 제어돼야 한다.

10.3.3 인메모리 데이터베이스 피하기

통합 테스트를 서로 분리하는 또 다른 방법으로 데이터베이스를 SQLite와 같은 인메모리 데이터베이스로 교체할 수도 있다. 인메모리 데이터베이스는 다음과 같은 장점이 있다.

  • 테스트 데이터를 제거할 필요가 없음
  • 작업 속도 향상
  • 테스트가 실행될 때마다 인스턴스화 가능

이러한 모든 장점에도 불구하고, 인메모리 데이터베이스는 일반 데이터베이스와 기능적으로 일관성이 없기 때문에 사용하지 않는 것이 좋다. 일반 데이터베이스와의 차이로 인해 테스트에서 거짓 양성 또는 거짓 음성(더 나쁨)이 발생하기 쉽다.

[팁] 테스트에서도 운영 환경과 같은 데이터베이스 관리 시스템을 사용하라.

10.4 테스트 구절에서 코드 재사용하기

통합 테스트가 너무 빨리 커지면 유지 보수 지표가 나빠질 수 있다. 통합 테스트는 가능한 짧게 하되 서로 결합하거나 가독성에 영향을 주지 않는 것이 중요하다. 아무리 짧은 테스트일지라도 서로 의존해서는 안 된다. 또한 테스트 시나리오의 전체 컨텍스트를 유지해야 하며, 진행 상황을 이해하려고 테스트 클래스의 다른 부분을 검사해서는 안 된다. 통합 테스트를 짤베 하기에 가장 좋은 방법은 비즈니스와 관련 없는 기술적인 부분을 비공개 메서드나 헬퍼 클래스로 추출하는 것이다. 추출한 부분은 재사용 할 수 있다.

이 절에서는 테스트의 세 가지 구절(준비, 동작, 검증)을 어떻게 줄여야 하는지 알아본다.

10.4.1 준비 구절에서 코드 재사용하기

테스트 준비 구절 간에 코드를 재사용하기에 가장 좋은 방법은 비공개 팩토리 메서드를 도입하는 것이다. 오브젝트 마더 패턴을 사용하는 것이 좋다.

오브젝트 마더 패턴에 대해서는 마틴 파울러의 글 ObjectMother를 참고하자.

요약하면 오브젝트 마더 패턴은 테스트 시에 필요한 객체를 미리 생성하고 초기화하는 역할을 하는 “Object Mother”라는 특별한 클래스나 메서드를 만드는 것 이다.

팩토리 메서드를 배치할 위치

테스트에서 중요한 부분만 남기고자 기술적인 부분을 팩토리 메서드로 옮길 때는 이 메서드를 어디에 둬야 하는지를 묻는 질문에 직면하게 된다. 테스트와 같은 클래스에 있어야 할까? 기초 클래스에 둘까? 아니면 별도의 헬퍼 클래스에 둘까? 단순하게 시작하라. 기본적으로 팬토리 메서드를 동일한 클래스에 배치하라. 코드 복제가 중요한 문제가 될 경우에만 별도의 헬퍼 클래스로 이동하라. 기초 클래스에 팩토리 메서드를 넣지 말라. 기초 클래스는 데이터 정리와 같이 모든 테스트에서 실행해야 하는 코드를 위한 클래스로 남겨둬야 한다.

10.4.2 실행 구절에서 코드 재사용하기

어떤 컨트롤러 기능을 호출해야 하는지에 대한 정보가 있는 대리자(delegate)를 받는 메서드를 도입하면 코드를 줄일 수 있다. (c#은 메서드를 매개변수로 전달할 수 있다., 데코레이터 패턴을 사용해 구현한다.)

데코레이터 패턴 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴

private string Execute(
    Func<UserController, string> func, // 컨트롤러 기능을 정의한 대리자
    MessageBus messageBus,
    IDomainLogger logger)
{
	using (var context = new CrmContext(ConnectionString))
    {
		var controller = new UserController(context, messageBus, logger);
		return func(controller);
	}
}

10.4.3 검증 구절에서 코드 재사용하기

검증 구절은 헬퍼 메서드를 통해 줄일 수 있다. 검증문에 플루언트 인터페이스를 사용할수도 있다. 검증문을 읽기가 훨씬 쉬워진다.

User userFromDb = QueryUser(user.UserId);
userFromDb
   .ShouldExist()
	WithEmail("new@gmail.com")
   .WithType(UserType.Customer);

Company companyFromDb = QueryCompany();
companyFromDb
   .ShouldExist()
   .WithNumberOfEmployees(0);

10.4.4 테스트가 데이터베이스 트랜잭션을 너무 많이 생성하는가?

트랜잭션이 많아지면 테스트가 느려지기 때문에 어느정도 문제가 되기는 하지만 할 수 있는 것은 많지 않다. 가치 있는 테스트에서는 여러 측면을 절충해야한다. 이 상황에서는 빠른 피드백과 유지 보수성 간의 절충이 발생된다. 이러한 경우에는 유지 보수성을 위해 성능을 양보하는 것이 좋다.

10.5 데이터베이스 테스트에 대한 일반적인 질문

10.5.1 읽기 테스트를 해야하는가?

쓰기를 철저히 테스트하는 것이 매우 중요하다. 왜냐하면 위험성이 높기 때문이다. 쓰기 작업이 잘못되면 데이터가 손생돼 데이터베이스뿐만 아니라 외부 애플리케이션에도 영향을 미칠 수 있다. 쓰기를 다루는 테스트는 이러한 실수에 대비한 보호책이 되므로 매우 가치가 있다. 그러나 읽기는 이에 해당하지 않는다. 따라서 복잡하거나 중요한 읽기 작업만 테스트하고 나머지는 무시하라. 캡슐화는 변경 사항에 비춰 데이터 일관성을 유지하는 것이다. 데이터 변경이 없으면 읽기 캡슐화는 의미가 없다. 읽기에는 추상화 계층이 거의 없기 때문에 단위 테스트가 아무 소용이 없다. 읽기를 테스트하기로 결정한 경우에는 실제 데이터베이스에서 통합 테스트를 하라.

10.5.2 레포지토리 테스트를 해야 하는가?

레포지토리가 도메인 객체를 어떻게 데이터베이스에 매핑하는지를 테스트하는 것은 유익할지 모른다. 하지만 레포지토리를 테스트하는 것은 유지비가 높고 회귀 방지가 떨어져서 테스트 스위트에 손실이 된다.

높은 유지비

레포지토리는 코드 유형 다이어그램에서 컨트롤러 사분면에 포함된다.

migration-example

복잡도는 거의 없고 프로세스 외부 의존성인 데이터베이스와 통신한다. 프로세스 외부 의존성이 있으면 테스트의 유지비가 증가한다. 하지만 유지비 대비 유익하지는 못하다.

낮은 회귀 방지

레포지토리는 그렇게 복잡하지 않으며 회귀 방지에서 일반적인 통합테스트가 주는 이점과 겹친다. 따라서 레포지토리에 대한 테스트는 가치를 충분히 더 주지 못한다.

레포지토리가 같고 있는 약간의 복잡도를 별도의 알고리즘으로 추출하여 해당 알고리즘을 테스트하는 것이 좋다. 하지만 안타깝게도 ORM을 사용하면 데이터 매핑과 데이터베이스 상호작용 간의 분리는 불가능 하다. 적어도 리팩터링 낸성이 저하되지 않고서는 데이터베이스 호출 없이 ORM 매핑을 테스트 할 수 없다.

결론

따라서 레포지토리는 직접 테스트하지 말고, 포괄적인 통합 테스트 스위트의 일부로 테스트하라. EventDispatcher도 별도로 테스트하지 말라. 목 체계가 복잡해서 유지비가 너무 많이 들지만, 회귀 방지의 이점은 너무 적다.

10.6 결론

데이터베이스 테스트를 잘 만들면 버그로부터 훌륭히 보호할 수 있다. 아주 효과적인 도구이며, 이러한 도구 없이는 소프트웨어를 완전히 신뢰할 수 없다. 데이터베이스 테스트는 데이터베이스를 리팩터링하거나 ORM을 전환하거나 데이터베이스 공급업체를 변경할 때 큰 도움이 된다.

요약

  • 데이터베이스 스키마를 소스 코드와 같이 형상 관리 시스템에 저장하라. 테이블, 뷰, 인덱스, 저장 프로시저와 데이터베이스 구성 방식에 대한 청사진이 되는 기타 모든 항목 등이 데이터베이스 스키마에 해당한다.
  • 참조 데이터도 데이터베이스 스키마에 해당한다. 이는 애플리케이션이 제대로 작동하도록 미리 채워져야 하는 데이터다. 참조 데이터와 일반 데이터를 구별하려면 애플리케이션에서 해당 데이터를 수정할 수 있는지 확인하면 된다. 수정할 수 있으면 일반 데이터이고, 그렇지 않으면 참조 데이터다.
  • 개발자마다 데이터베이스 인스턴스를 별도롤 두게 하라. 더 좋은 방법은 개발자 장비에 인스턴스를 호스팅하는 것인데, 이렇게 하면 테스트 실행 속도를 극대화할 수 있다.
  • 상태 기반 데이터베이스 배포 방식은 상태를 명시적으로 만들고 비교 도구가 마이그레이션을 암묵적으로 제어할 수 있도록 한다. 마이그레이션 기반 방식은 데이터베이스를 특정 상태에서 다른 상태로 전환하게끔 명시적 마이그레이션을 사용하도록 한다. 데이터베이스 상태가 명확하면 병합 충돌을 좀 더 쉽게 처리할 수 있는 데 반해, 명시적 마이그레이션은 데이터 모션 문제를 해결하는 데 도움이 된다.
  • 상태 기반 방식보다는 마이그레이션 기반 방식을 선호한다. 왜냐하면 데이터 모션 처리가 병합 충돌보다 훨씬 중요하기 때문이다. 마이그레이션을 통해 모든 수정 사항을 데이터베이스 스키마(참조 데이터 포함)에 적용하라.
  • 비즈니스 연산은 데이터를 원자적으로 업데이트해야 한다. 원자성을 얻으려면 데이터베이스 트랜잭션 매커니즘에 의존하라.
  • 가능하면 작업 단위 패턴을 사용하라. 작업 단위는 데이터베이스 트랜잭션에 의존하며, 비즈니스 연산 종료 시점까지 업데이트를 모두 지연시켜서 성능을 향상시킨다.
  • 데이터베이스 트랜잭션이나 작업 단위를 재사용ㅎ아지 말라. 각 구절에 각각 고유의 트랜잭션이나 작업 단위가 있어야 한다.
  • 통합 테스트는 순차적으로 실행하라. 병렬 실행에는 상당한 노력이 필요하며 보통 그럴 가치가 없다.
  • 테스트 시작 시점에 남은 데이터를 정리하라. 이 방식은 빠르고 일관성 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다. 이렇게 하면 별도의 종료 단계도 둘 필요가 없다.
  • SQLite와 같은 인메모리 데이터베이스는 사용하지 말라. 보호 수준이 떨어진다. 테스트에서도 운영 환경과 동일한 DBMS를 사용하라.
  • 필수가 아닌 부분을 비공개 메서드 또는 헬퍼 클래스로 추출해 테스트를 단축하라.
    • 준비 구절에서는 오브젝터 마더를 사용하라.
    • 실행 구절에서는 데코레이터 메서드를 작성하라.
    • 검증 구절에서는 플루언트 인터페이스를 도입하라.
  • 가장 복잡하거나 중요한 읽기 작업만 테스트하라. 나머지는 무시하라.
  • 레포지토리는 직접 테스트하지 말고 포괄적인 통합 테스트 스위트로 취급하라.

주석

번역수정

번역본에서는 아래와 같이 번역되어 있음 데이터베이스 스키마에 관해 유력한 용의자는 테이블, 뷰, 인덱스, 저장 프로시저 그리고 데이터베이스가 어떻게 구성되는지에 대한 청사진을 형성하는 나머지 모든 것이다.

원문은 다음과 같음 When it comes to the database schema, the usual suspects are tables, views, indexes, stored procedures, and anything else that forms a blueprint of how the database is constructed.

무슨 의미인지 와닿지 않아서 수정함.