외부 관리 의존성의 가장 일반적인 예는 애플리케이션 데이터베이스다.
애플리케이션 데이터베이스는 다른 애플리케이션이 접근할 수 없는 데이터베이스다.
실제 데이터베이스를 테스트하면 회귀 방지가 아주 뛰어나지만 설정하기 쉽지 않다.
통합 테스트에서는 관리 의존성이 그대로 있어야 한다.
데이터베이스는 관리 의존성으로 목을 사용하지 못하므로 비관리 의존성보다 테스트가 더 힘들 수 있다.
데이터베이스 통합 테스트가 가능하게끔 준비 단계를 수행해야 한다.
테스트를 용이하게 하면 보통 데이터베이스 상태도 개선된다.
통합 테스트를 작성하지 않아도 데이터베이스 상태 개선이라는 가치를 얻을 수 있다.
데이터베이스 테스트의 첫 번째 단계는 데이터베이스 스키마를 일반 코드로 취급하는 것이다.
일반 코드와 마찬가지로 스키마는 Git과 같은 형상 관리 시스템에 저장하는 것이 최선이다.
모델 데이터베이스를 형상관리로 사용하는 것은 데이터베이스 스키마를 유지하는데 좋지 않다.
반면 모든 데이터베이스 스키마 업데이트를 형상 관리 시스템에 두면 원천 정보를 하나로 할 수 있고, 일반 코드 변경과 함께 데이터베이스 변경을 추적할 수 있다.
형상 관리 외부에서는 데이터베이스 구조를 수정하면 안 된다.
데이터베이스 스키마의 대상은 테이블, 뷰, 인덱스, 저장 프로시저 그리고 데이터베이스가 어떻게 구성되는지에 대한 청사진을 형성하는 나머지 모든 것이다.
스키마는 SQL 스크립트 형태로 표현된다.
개발 중에 언제든지 이러한 스크립트로 기능을 완전히 갖춘 최신 데이터베이스 인스턴스를 만들 수 있어야 한다.
참조 데이터를 스키마로 여기지 않는 경우가 많은데 참조 데이터(애플리케이션이 제대로 작동하도록 미리 채워야 하는 데이터) 또한 스키마다.
참조 데이터는 애플리케이션의 필수 사항이므로 테이블, 뷰 그리고 다른 데이터베이스 스키마와 함께 SQL INSERT 문 형태로 형상 관리 시스템에 저장해야 한다.
참조 데이터는 일반적인 데이터와 별도로 저장되지만 두 데이터가 동일한 테이블에 공존하는 경우도 있는데 이렇게 하려면 수정할 수 있는 데이터와 수정할 수 없는 데이터를 구분하는 플래그를 둬 참조 데이터를 수정하지 못하도록 해야 한다.
다른 개발자와 데이터베이스를 공유해야 한다면 테스트가 훨씬 더 어려워진다.
개발자마다 별도로 데이터베이스 인스턴스를 사용해야 한다.
데이터베이스 배포에는 상태 기반과 마이그레이션 기반이라는 두 가지 방식이 있다.
마이그레이션 기반 방식은 초기에는 구현하고 유지 보수하기가 어렵지만 장기적으로 상태 기반 방식보다 훨씬 효과적이다.
개발 내내 유지보수하는 모델 데이터베이스가 있다.
배포 중에 비교 도구가 스크립트를 생성해서 운영 데이터베이스를 모델 데이터베이스와 비교해 최신 상태로 유지한다.
차이점은 상태 기반 방식을 사용하면 물리적인 모델 데이터베이스는 원천 데이터가 아니라는 것이다.
대신 해당 데이터베이스를 작성하는 데 사용할 수 있는 SQL 스크립트가 있다.
스크립트는 형상 관리에 저장된다.
상태 기반 접근 방식에서는 비교 도구는 테이블 삭제 생성 동기화 등 모든 어려운 작업을 수행한다.
반면 마이그레이션 기반 방식은 데이터베이스를 어떤 버전에서 다른 버전으로 전환하는 명시적인 마이그레이션을 의미한다.
운영 데이터베이스와 개발 데이터베이스를 자동으로 동기화하기 위한 도구를 쓸 수 없고 업그레이드 스크립트를 직접 작성해야 한다.
하지만 운영 데이터베이스 스키마에서 문서화되지 않은 변경 사항을 발견할 때 데이터베이스 비교 도구가 아직 유용할 수 있다.
마이그레이션 기반 방식에서 형상 관리에 저장하는 산출물은 데이터베이스 상태가 아닌 마이그레이션이다.
상태 기반 vs 마이그레이션 기반
- | 데이터베이스 상태 | 마이그레이션 메커니즘 |
---|---|---|
상태 기반 방식 | 명시적 | 암묵적 |
마이그레이션 기반 방식 | 암묵적 | 명시적 |
데이터베이스 상태가 명확하면 병합 충돌을 처리하기 수월한 반면 명시적 마이그레이션은 데이터 모션(새로운 데이터베이스 스키마를 준수하도록 기존 데이터 형태를 변경하는 과정) 문제를 해결하는 데 도움이 된다.
병합 충돌 완화와 데이터베이스 모션 용이성 중 데이터 모션이 훨씬 더 중요하다.
아직 운영으로 릴리스되지 않은 상태라면 데이터베이스를 변경할 때마다 데이터를 다시 생성할 수 있다.
그러나 릴리스 되면 마이그레이션 기반 방식으로 전환해서 데이터 모션을 올바르게 처리한다.
트랜잭션 관리를 적절히 하면 데이터 모순을 피할 수 있다.
읽기 전용 연산 중에는 여러 트랜잭션을 열어도 괜찮다.
데이터 변경이 포함된다면 데이터 모순을 피하고자 연산에 포함된 모든 업데이트는 원자적이어야 한다.
원자적 업데이트 : 모두 수행하거나 전혀 수행하지 않는 것
잠재적 모순을 피하려면 결정 유형을 두 가지로 나눠야 한다.
컨트롤러가 이러한 결정을 동시에 내릴 수 없으므로 이렇게 분리하는 것이 중요하며 비지니스 연산의 모든 단계가 성공했을 때 업데이트를 수행할 수 있는지 여부만 안다.
또한 데이터베이스에 접근하고 업데이트를 시도해야만 이러한 단계를 밟을 수 있다.
데이터베이스 리포지토리와 트랜잭션으로 나눠서 이러한 책임을 구분할 수 있다.
리포지터리와 트랜잭션은 책임이 서로 다르고 수명도 다르다.
트랜잭션은 전체 비지니스 연산 동안 있으며 연산이 끝나면 폐기된다.
반면에 리포지터리는 수명이 짧다.
데이터베이스 호출이 완료되는 즉시 리포지터리를 폐기할 수 있다.
리포지터리는 항상 현재 트랜잭션 위에서 작동한다.
데이터베이스에 연결할 때는 리포지터리가 트랜잭션에 등록해서 연결 중에 이뤄진 모든 데이터 수정 사항이 나중에 트랜잭션에 의해 롤백될 수 있도록 한다.
리포지터리와 트랜잭션을 도입하면 잠재적은 데이터 모순을 피할 수 있지만 더 좋은 방법이 있다.
트랜잭션 클래스를 작업 단위로 업그레이드 할 수 있다.
작업 단위에는 비지니스 연산의 영향을 받는 객체 목록이 있다.
작업이 완료되면 작업 단위는 데이터베이스를 변경하기 위해 해야 하는 업데이트를 모두 파악하고 이러한 업데이트를 하나의 단위로 실행한다.
작업 단위가 갖는 가장 큰 장점은 업데이트 지연이다.
트랜잭션과 달리 작업 단위는 비지니스 연산 종료 시점에 모든 업데이트를 실행하므로 데이터베이스 트랜잭션의 기간을 단축하고 데이터 혼잡을 줄인다.
JPA에서는 영속성 컨텍스트에 반영하고 모든 로직이 끝난 후 한번에 flush 한다.
통합 테스트에서 데이터베이스 트랜잭션을 관리하는 경우 다음 지침을 준수하라.
테스트 구절 간에 데이터베이스 트랜잭션이나 작업 단위를 재사용하지 말라.
AAA 세 구절에서 모두 동일한 DatabaseContext 인스턴스를 사용한다.
이렇게 작업 단위를 재사용하는 것은 컨트롤러가 운영 환경에서 하는 것과 다른 환경을 만들기 때문에 문제가 된다.
운영 환경에서는 각 비지니스 연산 전용 인스턴스가 있다.
전용 인스턴스는 컨트롤러 메서드 호출 직전에 생성되고 직후에 폐기된다.
동작 모순에 빠지지 않으려면 통합 테스트를 가능한 운영 환경에서와 비슷하게 해야 한다.
공유 데이터베이스를 사용하면 통합 테스트를 서로 분리할 수 없는 문제가 생긴다.
이 문제를 해결하려면
테스트는 데이터베이스 상태에 따라 달라지면 안 된다.
테스트는 데이터베이스 상태를 원하는 조건으로 만들어야 한다.
통합 테스트를 병렬로 실행하려면 상당히 많은 노력이 필요하다 성능 향상을 위해 시간을 허비하지 말고 순차적으로 통합 테스트를 실행하는 것이 더 실용적이다.
각 테스트 전에 데이터베이스 백업 복원하기
데이터 정리 문제를 해결할 수 있지만 다른 세 가지 방법보다 훨씬 느리다.
컨테이너를 사용하더라도 컨테이너 인스턴스를 제거하고 새 컨테이너를 생성하는 데 보총 몇 초 정도 걸리기 때문에 전체 테스트 실행 시간이 빠르게 늘어난다.
테스트 종료 시점에 데이터 정리하기
이 방법은 빠르지만 정리 단계를 건너뛰기 쉽다.
테스트 도중에 빌드 서버가 중단하거나 디버거에서 테스트를 종료하면 입력 데이터는 데이터베이스에 남아있고 이후 테스트 실행에 영향을 주게 된다.
데이터베이스 트랜잭션에 각 테스트를 래핑하고 커밋하지 않기
이 경우 테스트와 SUT에서 변경된 모든 내용이 자동으로 롤백된다. 이 접근 방식은 정리 단계를 건너뛰는 문제를 해결하지만 또 다른 문제를 제기한다. 이는 작업 단위를 재사용할 때와 같은 문제인데 추가 트랜잭션으로 인해 운영 환경과 다른 설정이 생성되는 것이다.
테스트 시작 시점에 데이터 정리하기
이 방법이 가장 좋다. 빠르게 작동하고 일관성이 없는 동작을 일으키지 않으며 정리 단계를 실수로 건너뛰지 않는다.
모든 통합 테스트의 기초 클래스를 두고 기초 클래스에 삭제 스크립트를 작성하라.
이렇게 사용하면 테스트가 시작될 때마다 스크립트가 자동으로 실행되도록 할 수 있다.
인메모리 데이터베이스는 다음과 같은 장점이 있다.
인메모리 데이터베이스는 공유 의존성이 아니기 때문에 통합 테스트는 실제로 컨테이너 접근 방식과 유사한 단위 테스트가 된다.
인메모리 데이터베이스는 일반 데이터베이스와 기능적으로 일관성이 없기 때문에 사용하지 않는 것이 좋다.
운영 환경과 테스트 환경이 일치하지 않는 문제이며일반 데이터베이스와 인메모리 데이터베이스 차이로 거짓 양성, 거짓 음성이 발생하기 쉽다.
통합 테스트는 가능한 짧게 하되 서로 결합하거나 가독성에 영향을 주지 않는 것이 중요하다.
아무리 짧은 테스트일지라도 서로 의존해서는 안 된다.
또한 테스트 시나리오의 전체 컨텍스트를 유지해야 하며 진행 상황을 이해하려고 테스트 클래스의 다른 부분을 검사해서는 안 된다.
통합 테스트를 짧게 하기에 가장 좋은 방법은 비지니스와 관련이 없는 기술적인 부분을 비공개 메서드나 핼퍼 클래스로 추출하는 것이다.
팩토리 메서드 패턴 만들기
팩토리 메서드에 기본값 추가
테스트에서 중요한 부분만 남기고자 기술적인 부분을 팩토리 메서드로 옮길 때는 이 메서드를 어디에 둬야 하는지를 묻는 질문에 직면하게 된다.
기본적으로 팩토리 메서드를 동일한 클래스에 배치하라.
코드 복제가 중요한 문제가 될 경우에만 별도의 핼퍼 클래스로 이동하라.
기초 클래스에 팩토리 메서드를 넣치 말라.
기초 클래스는 데이터 정리와 같이 모든 테스트에서 실행해야 하는 코드를 위한 클ㄹ래스로 남겨둬야 한다.
쓰기는 위험성이 높기 때문에 철저히 테스트하는 것이 매우 중요하다.
쓰기 작업이 잘못되면 데이터가 손상돼 데이터베이스뿐만 아니라 외부 애플리메이션에도 영향을 미칠 수 있다.
쓰기를 다루는 테스트는 이러한 실수에 대비한 보호책이 되므로 매우 가치가 있다.
그러니 읽기는 이에 해당하지 않는다.
가장 복잡하거나 중요한 읽기 작업만 테스트하고 나머지는 무시하라
또한 읽기에는 도메인 모델도 필요하지 않다.
도메인 모델링의 주요 목표 중 하나는 캡슐화다.
캡슐화는 변경 사항에 비춰 데이터 일관성을 유지하는 것이다.
데이터 변경이 없으면 읽기 캡슐화는 의미가 없다.
읽기에는 추상화 계층이 거의 없기 떄문에 단위 테스트가 아무 소용이 없다.
읽기를 테스트하기로 결정한 경우에는 실제 데이터베이스에서 통합 테스트를 하라.
리포지터리는 데이터베이스 위에 유용한 추상화를 제공한다.
리포지터리를 다른 통합 테스트와 독립적으로 테스트해야 하는가?
리포지터리 테스트는 가치를 충분히 더 주지 못한다.
리포지터리를 테스트하기 가장 좋은 방법은 리포지터리가 갖고 있는 약간의 복잡도를 별도의 알고리즘으로 추출하고 해당 알고리즘 전용 테스트를 작성하는 것이다.
리포지터리는 직접 테스트하지 말고 포괄적인 통합 테스트의 일부로 취급하라.