• 애플리케이션 데이터베이스: 다른 애플리케이션이 접근할 수 없는 데이터베이스

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

  • 테스트를 작성하기 전에 통합 테스트가 가능하게끔 준비 단계를 수행해야 된다.
    • 형상 관리 시스템에 데이터베이스 유지
    • 모든 개발자를 위한 별도의 데이터베이스 인스턴스 사용
    • 데이트베이스 배포에 마이그레이션 기반 방식 적용

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

  • 데이터베이스를 테스트하는 방법의 첫 번째 단계는 데이터베이스 스키마를 일반 코드로 취급하는 것이다.
    • 일반 코드와 마찬가지로 데이터베이스 스키마는 Git과 같은 형상 관리 시스템에 저장하는 것이 최선이다.
  • 모델 데이터베이스 방식
  • 모델 데이터베이스를 사용하는 것은 데이터베이스 스키마를 유지하는 데 상당히 좋지 못한 방법이다.
    • 변경 내역 부재: 데이터베이스 스키마를 과거 특정 시점으로 되돌릴 수 없다. 이는 운영 환경에서 버그를 재현할 때 중요할 수 있다.
    • 복수의 원천 정보: 모델 데이터베이스는 개발 상태에 대한 원천 정보를 둘러싸고 경합하게 된다. 이렇게 기준을 두 가지(GIt과 모델 데이터베이스)로 두면 부담이 가중된다.
  • 모든 데이터베이스 스키마 업데이트를 형상 관리 시스템에 두면 원천 정보를 하나로 할 수 있고, 일반 코드 변경과 함께 데이터베이스 변경을 추적할 수 있다.

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

  • 참조 데이터: 애플리케이션이 제대로 작동하도록 미리 채워야 하는 데이터
  • 참조 데이터와 일반 데이터 구별법: 애플리케이션이 데이터를 수정할 수 있으면 일반 데이터고, 그렇지 않으면 참조 데이터다.
  • 테이블, 뷰, 인덱스 등의 데이터베이스 스키마는 SQL 스크립트 형태로 표현된다.
    • 개발 중에 언제든지 이러한 스크립트로 기능을 완전히 갖춘 최신 데이터베이스 인스턴스를 만들 수 있어야 한다
  • 참조 데이터는 애플리케이션의 필수 사항이므로, 다른 데이터베이스 스키마와 함께 SQL INSERT 문 형태로 형상 관리 시스템에 저장해야 한다.

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

  • 실제 데이터베이스로 테스트하는 것은 충분히 어렵다. 다른 개발자들과 데이터베이스를 공유해야 한다면 훨씬 더 어려워진다.
    • 서로 다른 개발자가 실행한 테스트는 서로 간섭된다.
    • 하위 호환성이 없는 변경으로 다른 개발자의 작업을 막을 수 있기 때문이다.
  • 테스트 실행 속도를 극대화하려면 개발자마다 별도의 데이터베이스 인스턴스를 사용하라.

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

  • 마이그레이션 기반 방식은 초기에는 구현하고 유지 보수하기가 어렵지만 장기적으로 상태 기반 방식보다 훨씬 효과적이다.
  • 상태 기반 방식
    • 개발 내내 유지 보수하는 모델 데이터베이스가 있다.
    • 배포 중에 비교 도구가 스크립트를 생성해서 운영 데이터베이스를 모델 데이터베이스와 비교해 최신 상태로 유지한다.
    • 차이점: 물리적인 모델 데이터베이스는 원천 데이터가 아니다.
    • 대신 해당 데이터베이스를 작성하는데 사용할 수 있는 SQL 스크립트가 있다.
  • 마이그레이션 기반 방식
    • 운영 데이터베이스와 개발 데이터베이스를 자동으로 동기화하기 위한 도구를 쓸 수 없고, 업그레이드 스크립트를 직접 작성해아 한다.
    • 형상 관리에 저장하는 산출물은 데이터베이스 상태가 아닌 마이그레이션이다.
    • 마이그레이션은 일반적으로 평이한 SQL 스크립트로 표시(Flyway, Liquibase 등)하지만, SQL로 변환할 수 있는 DSL 같은 언어를 사용해 작성할 수도 있다.
  • 상태 기반 방식보다 마이그레이션 기반 방식을 선호하라
    • 마이그레이션 기반 방식은 데이터 모션 문제를 해결하는 도움이 된다.
    • 데이터 모션: 새로운 데이터베이스 스키마를 준수하도록 기존 데이터의 형태를 변경하는 과정
      • 예: Name 컬럼을 FirstName과 LastName으로 나눈다.
    • 상태 기반 방식은 병합 충돌을 해결하기가 수월하지만, 데이터 모션이 병합 충돌보다 훨씬 더 중요하다.

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

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

  • 비즈니스 연산에 데이터 변경이 포함된다면, 데이터 모순을 피하고자 이 연산에 포함된 모든 업데이트는 원자적이어야 한다.
  • 잠재적 모순을 피하려면 결정 유형을 두 가지로 나눠야 한다.
    • 엄데이트할 데이터
    • 업데이트 유지 또는 롤백 여부
  • 이는 리포지터리와 트랜잭션으로 나눠서 책임을 구분할 수 있다.
    • 리포지터리: 데이터베이스의 데이터에 대한 접근과 수정을 가능하게 하는 클래스다.
    • 트랜잭션: 데이터 업데이트를 완전히 커밋하거나 롤백하는 클래스다.
  • 트랜잭션은 전체 비즈니스 연산 동안 있으며 연산이 끝나면 폐기된다. 반면에 리포지터리는 데이터베이스 호출이 완료되면 즉시 리포지터리를 폐기할 수 있다.
  • 리포지터리와 트랜잭션을 도입하면 잠재적인 데이터 모순을 피할 수 있지만 작업 단위(unit of work)로 업그레이드하는 더 좋은 방법이 있다.
    • 작업 단위에는 비즈니스 연산의 영향을 받는 객체 목록이 있다. 작업이 완료되면,작업 단위네느 데이터베이스를 변경하기 위해 해야 하는 업데이트를 모두 파악하고 이러한 업데이트를 하나의 단위로 실행한다.
  • 작업 단위는 업데이트 지연이 가능하다는 장점이 있다.
    • 비즈니스 연산 종료 시점에 모든 업데이트를 실행하므로 데이터베이스 트랜잭션의 기간을 단축하고 데이터 혼잡을 줄인다.
    • 이 패턴은 종종 데이터베이스 호출 수를 줄이는데도 도움이 된다.
  • 비관계형 데이터베이스에서의 데이터 모순
    • 대부분의 비관계형 데이터베이스가 갖는 문제점은 고전적인 의미에서 트랜잭션이 없다는 것이다.
    • 원자적 업데이트는 단일 도큐먼트 내에서만 보장된다. 따라서, 한 번에 둘 이상의 도큐먼트를 수정하는 비즈니스 연산이 없도록 도큐먼트를 설계해야 한다.
    • 관계형 데이터베이스의 행보다 도큐먼트가 더 유연하기 때문에 가능하다.
    • 도메인 주도 설계에서는 비즈니스 연산당 둘 이상의 집계를 수정하면 안 된다는 지침이 있다. 이 지침은 데이터 모순으로부터 보호하는 것과 같은 목표가 있으며, 각 도큐먼트가 하나의 집계에 해당하는 도큐먼트 데이터베이스를 사용하는 시스템에만 적용된다.

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

  • 테스트 구절 간에 데이터베이스 트랜잭션이나 작업 단위를 재사용하지 말라.
  • 테스트 준비, 실행, 검증이라는 세 구절에서 같은 작업 단위를 사용하는 것은 컨트롤러가 운영 환경에서 하는 것과 다른 황경을 만들기 때문에 문제가 된다.
  • 통합 테스트에서 적어도 세 개의 트랜잭션 또는 작업 단위를 사용하라(준비, 실행, 검증 구절 당 하나씩).

테스트 데이터 생명 주기

  • 공유 데이터베이스를 사용하면 통합 테스트를 서로 분리할 수 없는 문제가 생긴다. 이 문제를 해결하려면,
    • 통합 테스트를 순차적으로 실행하라.
    • 테스트 실행 간에 남은 데이터를 제거하라.

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

  • 통합 테스트를 병렬로 실행하려면 상당한 노력이 필요하다.
    • 모든 테스트 데이터가 고유한지 확인해야 데이터베이스 제약 조건을 위반하지 않고 테스트가 다른 테스트 후에 입력 데이터를 잘못 수집하는 일이 없다.
    • 남은 데이터를 정리하는 것도 까다로워진다.
  • 성능 향상을 위해 시간을 허비하지 말고 순차적으로 통합 테스트를 실행하는 것이 더 실용적이다.
  • 컨테이너를 사용해 테스트를 병렬 처리할 수도 있는데, 이러한 방식은 실제로 유지 보수 부담이 너무 커지게 된다.
    • 도커 이미지로 만들 모델 데이터베이스를 추적해야 할 뿐만 아니라,
    • 도커 이미지를 유지 보수해야 하고
    • 각 테스트마다 컨테이너 인스턴스가 있는지 확인해야 하며
    • 통합 테스트를 일괄 처리하고
    • 다 사용한 컨테이너는 폐기해야 한다.
  • 데이터베이스는 개발자당 하나의 인스턴스만 갖는 것이 더 실용적이다.

테스트 실행 간 데이터 정리

  • 방법 네 가지
    • 각 테스트 전에 데이터베이스 백업 복원하기: 다른 방법들 보다 훨씬 느리다. 컨테이너를 사용하더라도 컨테이너 인스턴스를 제거하고 새 컨테이너를 생성하는 데 보통 몇 초 정도 걸린다.
    • 테스트 종료 시점에 데이터 정리하기: 테스트 도중에 빌드 서버가 중단하거나 디버거에서 테스트를 종료하면 입력 데이터는 데이터베이스에 남아있고 이후 테스트 실행에 영향을 주게 된다.
    • 데이터베이스 트랜잭션에 각 테스트를 래핑하고 커밋하지 않기: 작업 단위를 재사용할 떄와 같은 문제가 생겨서, 추가 트랜잭션으로 인해 운영 환경과 다른 설정이 생성된다.
    • 테스트 시작 시점에 데이터 정리하기: 이 방법이 가장 좋다. 빠르게 작동하고 일관성이 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다.
  • 모든 통합 테스트의 기초 클래스를 두고, 기초 클래스에 삭제 스크립트를 작성하라.
  • 삭제 스크립트는 일반 데이터를 모두 제거해야 하지만 참조 데이터는 제거하지 말아야 한다.

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

  • 인메모리 데이터베이스 장점
    • 테스트 데이터를 제거할 필요가 없음
    • 작업 속도 향상
    • 테스트가 실행될 떄마다 인스턴스화 가능
  • 인메모리 데이터베이스 단점
    • 일반 데이터베이스와 기능적으로 일관성이 없기 떄문에 사용하지 않는 것이 좋다.(운영 환경과 테스트 환경이 일치하지 않는 문제)
  • 테스트에서도 운영 환경과 같은 데이터베이스 관리 시스템을 사용하라. 보통 버전이 달라도 괜찮지만, 공급업체는 같아야 한다.

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

  • 통합 테스트가 너무 빨리 커지면 유지 보수 지표가 나빠질 수 있다.
  • 통합 테스트는 가능한 짧게 하되 서로 결합하거나 가독성에 영향을 주지 않는 것이 중요하다.
  • 통합 테스트를 짧게 하기 가장 좋은 바업은 비즈니스와 관련이 없는 기술적인 부분을 비공개 메서드나 헬퍼 클래스로 추출하는 것이다.

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

  • 준비 구절 간에 코드를 재사용하기에 가장 좋은 방법은 비공개 팩토리 메서드를 도입하는 것이다.
    • 오브젝트 마더: 테스트 픽스처를 만드는 데 도움이 되는 클래스 또는 메서드
    • 테스트 데이터 빌더: 오브젝트 마더와 유사하게 작동하지만 일반 메서드 대신 플루언트 인터페이스를 제공한다.
  • 팩토리 메서드를 배치할 위치
    • 시작은 단순하게 팩토리 메서드를 테스트 코드와 동일한 클래스에 배치하라.
    • 코드 복제가 중요한 문제가 될 경우에만 별도의 헬퍼 클래스로 이동하라.
    • 기초 클래스에는 팩토리 메서드를 넣지말라. 기초 클래스는 데이터 정리와 같이 모든 테스트에서 실행해야 하는 코드를 위한 클래스로 남겨둬야 한다.

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

  • 어던 컨트롤러 기능을 호출해야 하는지에 대한 정보가 있는 대리자를 받는 메서드를 도입할 수 있다.

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

  • 데이터 검증을 위한 헬퍼 메서드나 플루언트 인터페이스를 만들 수 있다.

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

  • 데이터베이스 컨텍스트를 추가하면 테스트가 느려지기 때문에 어느 정도 문제가 되기는 한다.
  • 하지만 이는 가치 있는 테스트에서 빠른 피드백과 유지 보수성 간의 절충을 의미한다.
    • 유지 보수성을 위해 성능을 양보함으로써 절충하는 것이 좋다.

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

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

  • 쓰기는 위험성이 높기 때문에 철저히 테스트하는 것이 중요하다.
    • 쓰기 작업이 잘못되면 데이터가 손상돼 데이터베이스뿐만 아니라 외부 애플리케이션에도 영향을 미칠 수 있다.
  • 읽기는 도메인 모델도 필요하지 않다. 데이터의 변경이 없으면 읽기 캡슐화는 의미가 없다.
    • 실제로 읽기는 추상화 계층을 피해서 성능 면에서 ORM보다 일반 SQL을 사용하는 것이 좋다.
  • 읽기는 추상화 계층이 거의 없기 때문에 단위 테스트가 아무 소용이 없다.
    • 읽기를 테스트하기로 결정한 경우에는 실제 데이터베이스에서 통합 테스트를 하라.

리포지터리 테스트를 해야 하는가?

  • 리포지터리는 데이터베이스 위에 유용한 추상화를 제공한다.
  • 리포지터리 테스트는 유지비가 높고 회귀 방지가 떨어져서 테스트 스위트에 손실이 된다.
    • 리포지터리는 코드유형 다이어그램에서 컨트롤러 사분면에 포함된다.
    • 리포지터리는 복잡하지 안흥며 회귀 방지에서 일반적인 통합 테스트가 주는 이점과 겹친다.
  • 리포지터리를 테스트하기에 가장 좋은 방법은 리포지터리 갖고 있는 약간의 복잡도를 별도의 알고리즘으로 추출하고 해당 알고리즘 전용 테스트를 작성하는 것이다.
  • ORM을 사용할 때는 데이터 매핑과 데이터베이스 상호작용 간의분리가 불가능하다.
    • 따라서 리포지터리를 직접 테스트하지 말고, 포괄적인 통합 테스트 스위트의 일부로 취급하라.