개요
데이터베이스 테스트는 버그로부터 훌륭히 보호할 수 있는 효과적인 도구다. 관리 의존성에 직접 작동하는 통합 테스트는 대규모 리팩터링에서 발생하는 버그로부터 보호하기에 가장 효율적인 방법이다.
데이터베이스 테스트를 위한 전제 조건
데이터베이스를 형상 관리 시스템에 유지
데이터베이스 스키마를 소스 코드와 같이 형상 관리 시스템에 저장해야 한다. 테이블, 뷰, 인덱스, 저장 프로시저와 데이터베이스 구성 방식에 대한 청사진이 되는 기타 모든 항목 등이 데이터베이스 스키마에 해당한다.
이렇게 해야 하는 이유는 명확하다. 데이터베이스 구조 변경을 추적할 수 있고, 팀원 간 일관된 데이터베이스 환경을 유지할 수 있다. 또한 코드와 데이터베이스 스키마의 버전을 동기화하여 배포 시 일관성을 보장할 수 있다.
데이터베이스 스키마가 형상 관리되지 않으면 개발자마다 다른 데이터베이스 구조를 가지게 되어 테스트 결과가 일관되지 않다. 특히 새로운 컬럼 추가, 인덱스 생성, 제약 조건 변경 등이 팀원 간에 동기화되지 않으면 테스트가 실패하거나 예상과 다른 결과를 낳을 수 있다.
예를 들어, 개발자 A가 users 테이블에 email 컬럼을 추가했는데, 개발자 B의 데이터베이스에는 해당 컬럼이 없다면, 개발자 B의 테스트는 실패할 것이다. 이런 문제를 방지하려면 데이터베이스 스키마도 코드와 함께 버전 관리되어야 한다.
참조 데이터도 데이터베이스 스키마다
참조 데이터는 애플리케이션이 제대로 작동하도록 미리 채워야 하는 데이터다. 참조 데이터와 일반 데이터를 구별하려면 애플리케이션에서 해당 데이터를 수정할 수 있는지 확인하면 된다. 수정할 수 있으면 일반 데이터이고, 그렇지 않으면 참조 데이터다.
참조 데이터의 예로는 국가 코드, 통화 코드, 사용자 역할(Role), 상품 카테고리 등이 있다. 이런 데이터는 애플리케이션 로직에서 참조되지만, 일반 사용자가 직접 수정할 수 없는 데이터다.
참조 데이터가 형상 관리되지 않으면 개발 환경마다 다른 참조 데이터를 가지게 되어 테스트 결과가 달라질 수 있다. 예를 들어, 특정 국가 코드가 참조 데이터에 없으면 해당 국가 사용자의 주문 처리가 실패할 수 있다.
또한 참조 데이터는 애플리케이션 로직과 밀접한 관련이 있다. 새로운 국가가 추가되거나 기존 국가 코드가 변경되면 애플리케이션 코드도 함께 수정되어야 한다. 따라서 참조 데이터도 코드와 함께 버전 관리되어야 한다.
모든 개발자를 위한 별도의 데이터베이스 인스턴스
개발자마다 데이터베이스 인스턴스를 별도로 두게 한다. 더 좋은 방법은 개발자 장비에 인스턴스를 호스팅하는 것인데, 이렇게 하면 테스트 실행 속도를 극대화할 수 있다.
공유 데이터베이스를 사용하면 여러 문제가 발생한다. 첫째, 테스트 간 간섭이 발생한다. 한 개발자가 실행한 테스트의 데이터가 다른 개발자의 테스트에 영향을 줄 수 있다. 둘째, 병렬 테스트 실행이 어려워진다. 여러 테스트가 동시에 같은 테이블을 수정하려 하면 데드락이나 데이터 불일치가 발생할 수 있다. 셋째, 테스트 실행 속도가 느려진다. 네트워크를 통한 데이터베이스 접근은 로컬 접근보다 훨씬 느리다.
개발자 장비에 데이터베이스를 설치하면 테스트 실행 속도가 크게 향상된다. 로컬 접근이므로 네트워크 지연이 없고, 다른 개발자의 테스트와 완전히 격리된다. 또한 개발자가 자신만의 데이터베이스 설정을 가질 수 있어서 실험적인 변경을 안전하게 수행할 수 있다.
상태 기반 vs 마이그레이션 기반 데이터베이스 배포
상태 기반 데이터베이스 배포는 데이터베이스의 최종 상태를 명시적으로 정의하고, 비교 도구가 현재 상태와 목표 상태를 비교하여 필요한 마이그레이션을 자동으로 생성하는 방식이다. 마이그레이션 기반 방식은 데이터베이스를 특정 상태에서 다른 상태로 전환하기 위해 명시적인 마이그레이션 스크립트를 작성하는 방식이다.
상태 기반 방식의 장점은 병합 충돌을 쉽게 처리할 수 있다는 것이다. 두 개발자가 동시에 같은 테이블을 수정해도 최종 상태만 맞으면 자동으로 병합된다. 하지만 데이터 모션(Data Motion) 문제를 해결하기 어렵다. 예를 들어, 컬럼명을 변경하면서 기존 데이터를 새 컬럼으로 이전하는 작업은 상태 기반으로는 표현하기 어렵다.
마이그레이션 기반 방식은 데이터 모션 문제를 해결하는 데 도움이 된다. 컬럼명 변경, 데이터 타입 변환, 복잡한 데이터 변환 로직 등을 명시적으로 작성할 수 있다. 하지만 병합 충돌이 발생하면 수동으로 해결해야 한다.
상태 기반 방식보다는 마이그레이션 기반 방식을 선호한다. 왜냐하면 데이터 모션 처리가 병합 충돌보다 훨씬 중요하기 때문이다. 데이터 손실이나 잘못된 변환은 애플리케이션 오류보다 훨씬 심각한 문제를 야기할 수 있다.
데이터베이스 트랜잭션 관리
제품 코드에서 데이터베이스 트랜잭션 관리하기
비즈니스 연산은 데이터를 원자적으로 업데이트해야 한다. 원자성을 얻으려면 데이터베이스 트랜잭션 매커니즘에 의존해야 한다. 가능하면 작업 단위 패턴을 사용한다. 작업 단위는 데이터베이스 트랜잭션에 의존하며, 비즈니스 연산 종료 시점까지 업데이트를 모두 지연시켜서 성능을 향상시킨다.
작업 단위 패턴은 여러 리포지토리 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장한다. 각 리포지토리에서 개별적으로 트랜잭션을 관리하는 것보다 효율적이며, 비즈니스 로직의 일관성을 유지할 수 있다.
예를 들어, 주문 생성 시나리오를 생각해보자. 주문을 생성하고, 재고를 차감하고, 결제를 처리하는 과정이 모두 성공하거나 모두 실패해야 한다. 만약 재고 차감은 성공했지만 결제 처리가 실패했다면, 재고 차감도 롤백되어야 한다. 이런 경우 작업 단위 패턴을 사용하면 모든 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장할 수 있다.
작업 단위 패턴의 또 다른 장점은 성능 향상이다. 여러 개의 작은 트랜잭션을 하나의 큰 트랜잭션으로 묶으면 데이터베이스 연결 횟수를 줄일 수 있고, 네트워크 오버헤드를 감소시킬 수 있다.
통합 테스트에서 데이터베이스 트랜잭션 관리하기
테스트 구절마다 데이터베이스 트랜잭션이나 작업 단위를 재사용하지 말아야 한다. 준비, 실행, 검증 구절에 각각 고유의 트랜잭션이나 작업 단위가 있어야 한다. 이렇게 하면 각 구절이 독립적으로 실행되어 테스트의 격리성을 보장할 수 있다.
이 규칙을 지키지 않으면 여러 문제가 발생할 수 있다. 첫째, 테스트 간 간섭이 발생한다. 한 구절에서 변경한 데이터가 다른 구절에 영향을 줄 수 있다. 둘째, 테스트의 예측 가능성이 떨어진다. 같은 테스트를 여러 번 실행해도 다른 결과가 나올 수 있다. 셋째, 디버깅이 어려워진다. 문제가 발생했을 때 어느 구절에서 발생했는지 파악하기 어렵다.
각 구절이 독립적인 트랜잭션을 가지면 테스트의 격리성이 보장된다. 준비 구절에서 생성한 데이터가 실행 구절에 영향을 주지 않고, 실행 구절에서 변경한 데이터가 검증 구절에 영향을 주지 않는다. 이렇게 하면 테스트가 더 안정적이고 예측 가능해진다.
테스트 데이터 생명 주기
병렬 테스트 실행과 순차적 테스트 실행
통합 테스트는 순차적으로 실행한다. 병렬 실행에는 상당한 노력이 필요하며, 보통 그럴 가치가 없다. 병렬 테스트 실행은 데이터베이스 리소스 경합, 테스트 간 간섭, 복잡한 동기화 문제를 야기할 수 있다.
병렬 테스트 실행의 문제점은 명확하다. 첫째, 데이터베이스 리소스 경합이 발생한다. 여러 테스트가 동시에 같은 테이블을 수정하려 하면 데드락이나 락 대기가 발생할 수 있다. 둘째, 테스트 간 간섭이 발생한다. 한 테스트가 생성한 데이터가 다른 테스트에 영향을 줄 수 있다. 셋째, 복잡한 동기화 문제가 발생한다. 테스트 실행 순서에 따라 결과가 달라질 수 있다.
순차 실행이 더 안정적이고 예측 가능한 결과를 제공한다. 테스트가 하나씩 실행되므로 간섭이 없고, 디버깅도 쉬워진다. 또한 병렬 실행을 위한 복잡한 설정이나 동기화 로직이 필요 없다.
물론 병렬 실행이 테스트 실행 시간을 단축할 수 있지만, 그 이익보다는 문제가 더 많다. 통합 테스트는 단위 테스트보다 느리지만, 그럼에도 불구하고 순차 실행이 더 안전하고 효율적이다.
테스트 실행 간 데이터 정리
테스트 시작 시점에 남은 데이터를 정리한다. 이 방식은 빠르고 일관성 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다. 이렇게 하면 별도의 종료 단계도 둘 필요가 없다.
테스트 후 정리보다는 테스트 전 정리가 더 안전하고 효율적이다. 그 이유는 다음과 같다. 첫째, 테스트가 실패해도 정리 작업이 실행된다. 테스트 후 정리 방식은 테스트가 실패하면 정리 코드가 실행되지 않아 데이터가 남을 수 있다. 둘째, 정리 작업을 실수로 건너뛸 위험이 없다. 테스트 전 정리는 모든 테스트에서 자동으로 실행되므로 누락될 가능성이 없다. 셋째, 더 빠르다. 테스트 전 정리는 필요한 데이터만 삭제하면 되지만, 테스트 후 정리는 모든 관련 데이터를 찾아서 삭제해야 한다.
테스트 전 정리 방식의 또 다른 장점은 일관성이다. 모든 테스트가 동일한 초기 상태에서 시작되므로 테스트 결과가 일관되고 예측 가능하다. 또한 정리 작업이 실패해도 테스트 실행에 영향을 주지 않는다.
인메모리 데이터베이스 피하기
SQLite와 같은 인메모리 데이터베이스는 사용하지 말아야 한다. 다른 업체의 데이터베이스로 테스트를 실행하면 보호 수준이 떨어진다. 테스트에서도 운영 환경과 같이 동일한 DBMS를 사용해야 한다.
인메모리 데이터베이스를 사용하면 여러 문제가 발생할 수 있다. 첫째, 실제 프로덕션 환경과 동작이 다를 수 있다. SQLite는 PostgreSQL이나 MySQL과 다른 SQL 문법이나 데이터 타입을 지원한다. 둘째, 성능 특성이 다르다. 인메모리 데이터베이스는 디스크 I/O가 없어서 실제 환경과 성능 특성이 크게 다르다. 셋째, 제약 조건이나 트랜잭션 동작이 다를 수 있다.
예를 들어, PostgreSQL의 JSON 타입이나 배열 타입을 사용하는 경우 SQLite에서는 제대로 테스트할 수 없다. 또한 PostgreSQL의 특정 함수나 연산자를 사용하는 경우 SQLite에서는 다른 결과가 나올 수 있다.
테스트의 목적은 실제 프로덕션 환경에서 발생할 수 있는 문제를 미리 발견하는 것이다. 인메모리 데이터베이스를 사용하면 실제 환경과 다른 동작으로 인해 숨겨진 버그를 놓칠 수 있다. 따라서 테스트에서도 운영 환경과 동일한 DBMS를 사용해야 한다.
테스트 구절에서 코드 재사용하기
준비 구절에서 코드 재사용하기
필수가 아닌 부분을 비공개 메서드 또는 헬퍼 클래스로 추출해 테스트를 단축한다. 준비 구절에서는 오브젝트 마더를 선택한다. 오브젝트 마더는 테스트에서 자주 사용하는 객체를 빠르게 생성하기 위한 유틸리티 클래스다.
오브젝트 마더의 장점은 간단하고 빠르게 객체를 생성할 수 있다는 것이다. 미리 정의된 객체를 제공하므로 복잡한 설정 없이 바로 사용할 수 있다. 또한 테스트 코드의 중복을 줄이고 가독성을 높일 수 있다.
하지만 오브젝트 마더는 유연성이 떨어진다는 단점이 있다. 특정 속성만 변경하기 어렵고, 다양한 변형을 만들기 위해서는 많은 메서드를 정의해야 한다.
예를 들어, 사용자 객체를 생성하는 오브젝트 마더가 있다면, 일반 사용자, 관리자, 비활성 사용자 등 다양한 변형을 미리 정의해야 한다. 하지만 이런 변형이 많아지면 오브젝트 마더 자체가 복잡해지고 유지보수가 어려워진다.
따라서 오브젝트 마더는 자주 사용되는 기본적인 객체에만 사용하고, 복잡한 변형이 필요한 경우에는 다른 방법을 고려해야 한다.
실행 구절에서 코드 재사용하기
실행 구절에서는 데코레이터 메서드를 작성한다. 데코레이터 메서드는 기존 메서드를 감싸서 추가 기능을 제공하는 메서드다. 공통적인 실행 로직을 추출하여 테스트 코드의 중복을 줄이고 가독성을 높일 수 있다.
데코레이터 메서드의 장점은 여러 테스트에서 공통으로 사용되는 실행 로직을 한 곳에 모을 수 있다는 것이다. 예를 들어, 주문 생성과 관련된 여러 테스트에서 공통으로 사용되는 로직이 있다면 이를 데코레이터 메서드로 추출할 수 있다.
데코레이터 메서드는 기존 메서드를 감싸서 추가 기능을 제공한다. 예를 들어, 주문 생성 메서드를 감싸서 자동으로 결제 처리까지 수행하는 데코레이터 메서드를 만들 수 있다. 이렇게 하면 테스트에서 복잡한 실행 로직을 간단하게 표현할 수 있다.
하지만 데코레이터 메서드를 너무 많이 사용하면 테스트의 의도가 불분명해질 수 있다. 데코레이터 메서드가 너무 복잡하거나 많은 기능을 포함하면 테스트가 무엇을 하는지 파악하기 어려워진다. 따라서 데코레이터 메서드는 단순하고 명확한 기능만 수행하도록 해야 한다.
검증 구절에서 코드 재사용하기
검증 구절에서는 플루언트 인터페이스를 도입한다. 플루언트 인터페이스는 메서드 체이닝을 통해 자연스러운 언어로 읽히는 API를 제공한다. 복잡한 검증 로직을 추상화하여 테스트의 의도를 명확하게 표현할 수 있다.
플루언트 인터페이스의 장점은 테스트 코드가 자연스러운 언어로 읽힌다는 것이다. 예를 들어,
assertThat(order)
.hasStatus(OrderStatus.CONFIRMED)
.hasTotalAmount(100.0)
.wasCreatedAt(expectedDate)
와 같이 읽을 수 있다. 이는
assertEquals(OrderStatus.CONFIRMED, order.getStatus());
assertEquals(100.0,order.getTotalAmount());
assertEquals(expectedDate, order.getCreatedAt());
보다 훨씬 읽기 쉽다.
플루언트 인터페이스는 복잡한 검증 로직을 추상화할 수 있다. 예를 들어, 주문이 올바르게 생성되었는지 검증하는 복잡한 로직을 assertThat(order).isValid()와 같이 간단하게 표현할 수 있다. 이렇게 하면 테스트의 의도가 명확해지고 가독성이 향상된다.
하지만 플루언트 인터페이스를 너무 복잡하게 만들면 오히려 가독성이 떨어질 수 있다. 메서드 체이닝이 너무 길어지거나 복잡한 조건이 많아지면 테스트의 의도를 파악하기 어려워진다. 따라서 플루언트 인터페이스는 단순하고 명확한 검증만 수행하도록 해야 한다.
테스트가 데이터베이스 트랜잭션을 너무 많이 생성하는가?
테스트 구절마다 고유한 트랜잭션을 사용하는 것이 올바르다. 각 구절이 독립적으로 실행되어야 테스트의 격리성이 보장된다. 트랜잭션 수가 많아지는 것을 걱정할 필요는 없다.
테스트의 안정성과 격리성이 성능보다 중요하다. 트랜잭션을 재사용하면 테스트 간 간섭이 발생할 수 있고, 테스트의 예측 가능성이 떨어질 수 있다. 또한 테스트가 실패했을 때 어느 구절에서 문제가 발생했는지 파악하기 어려워진다.
트랜잭션 수가 많아지는 것에 대한 우려는 이해할 수 있지만, 실제로는 큰 문제가 되지 않는다. 현대의 데이터베이스는 트랜잭션 처리가 매우 효율적이며, 테스트에서 사용하는 트랜잭션은 대부분 짧고 단순하다.
또한 테스트의 목적은 버그를 찾는 것이지, 성능을 최적화하는 것이 아니다. 테스트가 느려지더라도 안정성과 격리성을 보장하는 것이 더 중요하다. 만약 테스트 실행 시간이 문제가 된다면, 테스트를 병렬로 실행하거나 불필요한 테스트를 제거하는 등의 다른 방법을 고려해야 한다.
데이터베이스 테스트에 대한 일반적인 질문
읽기 테스트를 해야 하는가?
읽기 테스트 임계치는 쓰기 테스트 임계치보다 높아야 한다. 가장 복잡하거나 중요한 읽기 작업만 테스트한다. 나머지는 무시한다.
읽기 작업은 일반적으로 단순하고, 비즈니스 로직이 적어서 테스트의 가치가 상대적으로 낮다. 단순한 CRUD 작업은 테스트하지 않아도 된다. 하지만 복잡한 조회나 집계 작업은 반드시 테스트해야 한다.
읽기 테스트의 임계치를 높게 설정하는 이유는 다음과 같다. 첫째, 읽기 작업은 데이터를 변경하지 않으므로 버그가 발생해도 데이터 손실이 없다. 둘째, 읽기 작업은 일반적으로 단순하고 예측 가능하다. 셋째, 읽기 테스트는 쓰기 테스트보다 구현하기 어렵고 유지보수 비용이 높다.
하지만 복잡한 읽기 작업은 반드시 테스트해야 한다. 예를 들어, 복잡한 조인 쿼리, 집계 함수, 윈도우 함수 등을 사용하는 경우에는 테스트가 필요하다. 이런 작업은 버그가 발생하기 쉽고, 디버깅이 어려우며, 성능에 큰 영향을 줄 수 있다.
또한 비즈니스적으로 중요한 읽기 작업도 테스트해야 한다. 예를 들어, 매출 집계, 사용자 통계, 재고 조회 등은 비즈니스 의사결정에 직접적인 영향을 주므로 테스트가 필요하다.
리포지토리 테스트를 해야 하는가?
리포지토리는 직접 테스트하지 말고 포괄적인 통합 테스트 스위트로 취급한다. 리포지토리 테스트는 회귀 방지에 대한 이득이 너무 적은 데 반해 유지비가 너무 높다.
리포지토리는 단순한 CRUD 작업만 수행하며, 복잡한 비즈니스 로직이 없기 때문에 테스트의 가치가 낮다. 리포지토리 테스트는 다음과 같은 문제가 있다. 첫째, 구현 세부사항에 결합된다. 리포지토리의 내부 구현이 변경되면 테스트도 함께 변경되어야 한다. 둘째, 유지보수 비용이 높다. 리포지토리 테스트는 데이터베이스 스키마 변경에 민감하고, 테스트 데이터 관리가 복잡하다. 셋째, 회귀 방지 효과가 낮다. 리포지토리는 단순한 CRUD 작업만 수행하므로 버그가 발생할 가능성이 낮다.
대신 리포지토리는 포괄적인 통합 테스트에서 간접적으로 테스트하는 것이 좋다. 비즈니스 로직을 테스트하면서 리포지토리도 함께 테스트할 수 있다. 이렇게 하면 리포지토리의 실제 사용 시나리오를 테스트할 수 있고, 비즈니스 로직과의 통합도 함께 검증할 수 있다.
예를 들어, 주문 생성 테스트에서 주문 리포지토리를 사용한다면, 주문 생성 로직과 함께 리포지토리도 테스트할 수 있다. 이렇게 하면 리포지토리가 실제로 올바르게 작동하는지 확인할 수 있고, 비즈니스 로직과의 통합도 함께 검증할 수 있다.
정리
데이터베이스 테스트를 잘 만들면 버그로부터 훌륭히 보호할 수 있다. 경험상 아주 효과적인 도구인데, 이러한 도구 없이는 해당 소프트웨어를 완전히 신뢰할 수 없다. 이러한 테스트는 데이터베이스를 리팩터링하거나 ORM을 전환하거나 데이터베이스 공급업체를 변경할 때 큰 도움이 된다.
관리 의존성에 직접 작동하는 통합 테스트는 대규모 리팩터링에서 발생하는 버그로부터 보호하기에 가장 효율적인 방법이다. 데이터베이스 테스트는 느리고 복잡하지만, 올바른 전략을 사용하면 빠르고 안정적인 테스트 스위트를 구축할 수 있다.
데이터베이스 테스트의 핵심은 올바른 전제 조건을 갖추는 것이다. 데이터베이스 스키마를 형상 관리하고, 참조 데이터를 함께 관리하며, 개발자마다 별도의 데이터베이스 인스턴스를 사용해야 한다. 또한 마이그레이션 기반 배포 방식을 사용하여 데이터 모션 문제를 해결해야 한다.
트랜잭션 관리도 중요하다. 제품 코드에서는 작업 단위 패턴을 사용하고, 테스트에서는 각 구절마다 독립적인 트랜잭션을 사용해야 한다. 테스트 데이터 생명 주기도 신중하게 관리해야 한다. 순차적 테스트 실행, 테스트 전 데이터 정리, 실제 DBMS 사용 등의 원칙을 지켜야 한다.
테스트 코드의 재사용도 중요하다. 준비 구절에서는 오브젝트 마더를, 실행 구절에서는 데코레이터 메서드를, 검증 구절에서는 플루언트 인터페이스를 사용하여 테스트 코드의 중복을 줄이고 가독성을 높일 수 있다.
마지막으로, 읽기 테스트는 신중하게 선택하고, 리포지토리는 직접 테스트하지 말고 통합 테스트에서 간접적으로 테스트해야 한다. 이렇게 하면 효율적이고 안정적인 데이터베이스 테스트 스위트를 구축할 수 있다.
용어 정의
데이터베이스 스키마 (Database Schema)
데이터베이스의 구조를 정의하는 청사진으로, 테이블, 뷰, 인덱스, 저장 프로시저와 데이터베이스 구성 방식에 대한 기타 모든 항목을 포함한다. 소스 코드와 같이 형상 관리 시스템에 저장되어야 하며, 데이터베이스 구조 변경을 추적할 수 있다.
데이터베이스 스키마는 애플리케이션의 데이터 모델을 정의하는 핵심 요소다. 테이블 구조, 컬럼 타입, 제약 조건, 인덱스, 관계 등을 포함한다. 스키마가 변경되면 애플리케이션 코드도 함께 변경되어야 하므로, 스키마와 코드의 버전을 동기화하는 것이 중요하다.
참조 데이터 (Reference Data)
애플리케이션이 제대로 작동하도록 미리 채워야 하는 데이터다. 애플리케이션에서 수정할 수 없는 데이터로, 일반 데이터와 구별된다. 참조 데이터는 애플리케이션 코드와 함께 버전 관리되어야 한다.
참조 데이터는 애플리케이션의 비즈니스 로직과 밀접한 관련이 있다. 국가 코드, 통화 코드, 사용자 역할, 상품 카테고리 등이 대표적인 예다. 이런 데이터는 애플리케이션 로직에서 참조되지만, 일반 사용자가 직접 수정할 수 없다. 참조 데이터가 변경되면 애플리케이션 코드도 함께 변경되어야 하므로, 참조 데이터와 코드의 버전을 동기화하는 것이 중요하다.
상태 기반 데이터베이스 배포 (State-Based Database Deployment)
데이터베이스의 최종 상태를 명시적으로 정의하고, 비교 도구가 현재 상태와 목표 상태를 비교하여 필요한 마이그레이션을 자동으로 생성하는 방식이다. 병합 충돌을 쉽게 처리할 수 있지만, 데이터 모션 문제를 해결하기 어렵다.
상태 기반 방식은 데이터베이스의 최종 상태만 정의하면 되므로 간단하다. 두 개발자가 동시에 같은 테이블을 수정해도 최종 상태만 맞으면 자동으로 병합된다. 하지만 컬럼명 변경, 데이터 타입 변환, 복잡한 데이터 변환 로직 등은 표현하기 어렵다.
마이그레이션 기반 데이터베이스 배포 (Migration-Based Database Deployment)
데이터베이스를 특정 상태에서 다른 상태로 전환하기 위해 명시적인 마이그레이션 스크립트를 작성하는 방식이다. 데이터 모션 문제를 해결하는 데 도움이 되며, 상태 기반 방식보다 선호된다.
마이그레이션 기반 방식은 데이터 모션 문제를 해결하는 데 도움이 된다. 컬럼명 변경, 데이터 타입 변환, 복잡한 데이터 변환 로직 등을 명시적으로 작성할 수 있다. 하지만 병합 충돌이 발생하면 수동으로 해결해야 한다. 데이터 손실이나 잘못된 변환은 애플리케이션 오류보다 훨씬 심각한 문제를 야기할 수 있으므로, 마이그레이션 기반 방식을 선호한다.
작업 단위 패턴 (Unit of Work Pattern)
여러 리포지토리 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장하는 패턴이다. 비즈니스 연산 종료 시점까지 업데이트를 모두 지연시켜서 성능을 향상시키며, 데이터베이스 트랜잭션에 의존한다.
작업 단위 패턴은 여러 리포지토리 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장한다. 예를 들어, 주문 생성 시 주문을 생성하고, 재고를 차감하고, 결제를 처리하는 과정이 모두 성공하거나 모두 실패해야 한다. 작업 단위 패턴을 사용하면 모든 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장할 수 있다.
오브젝트 마더 (Object Mother)
테스트에서 자주 사용하는 객체를 빠르게 생성하기 위한 유틸리티 클래스다. 미리 정의된 객체를 제공하지만 유연성이 떨어진다는 단점이 있다. 테스트 데이터 생성의 간단한 방법으로 활용된다.
오브젝트 마더는 테스트에서 자주 사용하는 객체를 빠르게 생성하기 위한 유틸리티 클래스다. 예를 들어, 사용자 객체를 생성하는 오브젝트 마더가 있다면, 일반 사용자, 관리자, 비활성 사용자 등 다양한 변형을 미리 정의할 수 있다. 하지만 이런 변형이 많아지면 오브젝트 마더 자체가 복잡해지고 유지보수가 어려워진다.
데코레이터 메서드 (Decorator Method)
기존 메서드를 감싸서 추가 기능을 제공하는 메서드다. 공통적인 실행 로직을 추출하여 테스트 코드의 중복을 줄이고 가독성을 높일 수 있다. 테스트의 실행 구절에서 코드 재사용을 위해 사용된다.
데코레이터 메서드는 기존 메서드를 감싸서 추가 기능을 제공하는 메서드다. 예를 들어, 주문 생성 메서드를 감싸서 자동으로 결제 처리까지 수행하는 데코레이터 메서드를 만들 수 있다. 이렇게 하면 테스트에서 복잡한 실행 로직을 간단하게 표현할 수 있다. 하지만 데코레이터 메서드를 너무 많이 사용하면 테스트의 의도가 불분명해질 수 있다.
플루언트 인터페이스 (Fluent Interface)
메서드 체이닝을 통해 자연스러운 언어로 읽히는 API를 제공하는 인터페이스다. 복잡한 검증 로직을 추상화하여 테스트의 의도를 명확하게 표현할 수 있다. 테스트의 검증 구절에서 코드 재사용을 위해 사용된다.
플루언트 인터페이스는 메서드 체이닝을 통해 자연스러운 언어로 읽히는 API를 제공한다. 하지만 플루언트 인터페이스를 너무 복잡하게 만들면 오히려 가독성이 떨어질 수 있다.