정글에서 온 개발자

1/14 TIL 데이터베이스 테스트 본문

TIL

1/14 TIL 데이터베이스 테스트

dev-diver 2025. 1. 15. 00:47

전제조건

  • 데이터베이스 스키마를 형상관리로 관리
    • 단순 인스턴스로 관리하는 것에 비해 과거 시점으로 돌릴 수 있는 등 장점이 있다.
    • 형상관리 외에서는 스키마를 수정하면 안된다.
  • 개발자마다 별도의 데이터베이스 인스턴스 사용
    • 테스트 중 운영과 간섭이 없도록 함
  • 데이터베이스 배포에 마이그레이션 기반 적용
    • 상태 기반 방식은 병합 충돌 처리가 수월
    • 마이그레이션은 데이터 모션 문제 해결이 수월
    • 모션 문제 해결이 병합충돌 해결보다 중요하다.

* 병합 충돌 : 마이그레이션 도중 사용자가 데이터를 수정할 수 있음..?

스키마

  • 테이블, 부, 인덱스, 저장프로시저 등 SQL 스크립트로 표현된다.
  • 데이터베이스에 저장된 데이터라도 변하지 않는 데이터 (참조 데이터) 도 스키마다! (SQL INSERT 형태로 저장)

마이그레이션

  • 마이그레이션이 커밋 된 후에는 수정하지 말고, 잘못되면 되돌리지 말고 새 마이그레이션을 생성

트랜잭션 관리

  • 읽기 연산 중에는 여러 트랜잭션을 열어도 된다.
  • 중간에 데이터 변경이 포함된다면 원자성을 위해 트랜잭션을 쓴다.
  • 업데이트할 데이터 , 업데이트 유지 또는 롤백 여부에 대한 책임을 나눈다.

리포지토리 - 데이터에 대한 접근과 수정을 가능하게 하는 클래스
트랜잭션 - 데이터 업데이트를 완전히 커밋하거나 롤백하는 클래스

  • 리포지토리는 수명이 짧고, 트랜잭션은 연산 동안 존재한다.
  • 리포지토리에 트랜잭션을 주입한다!
public UserConroller(  //UserController 생성자
    Transaction transaction,
    ..)
{
    _transaction = transaction;
    _userRepository = new UserRepository(transaction);
    
}

public class Transaction : IDisposable
{
    public void Commit() {...}
    public void Dispose() {...}
}
  • ORM에서 지원하는 작업단위 패턴을 사용하면 '작업 지연'을 통해 SQL 호출을 줄일 수도 있다.

 

  • gorm에서 쓰는 tx.begin()이 따로 없는 것, Controller가 시작하자마자 transaction을 넣는 것이 생소한데, 어떻게 적용해야 할까?
  • gorm에서는 작업단위 패턴을 어떻게 지원할지도 의문이다.

* 비관계형 데이터베이스에는 트랜잭션이 없다. 그래서 한번에 여러 도큐먼트를 수정하지 않도록 도큐먼트 구조 자체를 연산 단위로 설계한다.

통합테스트에서 트랜잭션

  • 운영과 동일하게 트랜잭션을 쪼개야한다.
  • 따라서 준비, 실행, 검증 각각 트랜잭션을 사용해야 한다.

데이터 생명 주기

데이터 정리

  • 테스트 전 데이터베이스 백업 복원 - 느리다.
  • 테스트 종료 시점에 데이터 정리하기 - 테스트 도중 서버 중단되면 정리 안됨
  • 데이터베이스 트랜잭션에 각 테스트를 래핑하고 커밋 안하기 - 운영과 다르게 트랜잭션이 운용된다.
  • 테스트 시작 시점에 데이터 정리하기 - 가장 좋음!
public abstract class IntegerationTests
{
	protected IntegrationTests()
    {
    	ClearDatabase();
    }
    
    private void ClearDatabase()
    {
    	string query = 
        	"DELETE FROM dbo.User; .."
            
        ...
    }
}

인메모리 데이터베이스 사용하지 않기

  • 운영 DB와 환경이 다르게 될 수 있기 때문에

테스크 구절 코드 재사용

  • 준비문 -> 오브젝트 마더
  • 실행문 -> 대리자
  • 검증 -> 헬퍼메서드와 플루언트 api (클래스 확장을 통해)

위와 같이 정리하면서 트랜잭션이 늘어나 성능 저하가 있을 수 있지만, 유지보수를 위한 감수 가능한 절충이다.

* 오브젝트 마더 :  주로 테스트 코드에서 사용(하고 여기서 쓰는 용어) - 복잡한 객체 반복 생성을 피하기 위해 미리 객체를 생성해두고 테스트에 필요한 객체를 편리하게 생성.
* 오브젝트 마더가 빌더 패턴으로 되면 빌더.  (선택적 인수가 있으면 오브젝트 빌더가 더 좋을수도)

기타

읽기 테스트

읽기는 도메인 모델이 필요가 없다. -> 단위 테스트가 필요 없다. -> 테스트 하고 싶으면 통합테스트를 하자
변경사항이 없으므로 캡슐화도 필요 없다. ->  ORM도 필요 없고 일반 SQL을 쓰는 것이 좋다. (기능 얘기인지, 테스트 얘기인지?)

레포지토리 테스트

리포지토리가 도메인 객체를 데이터베이스에 어떻게 맵핑하는지 정도는 유닛 테스트를 할 수 있다. 그러나 가치가 크지 않다.
ORM을 사용하면 데이터베이스 상호작용과 맵핑을 분리하는 것도 불가능하다.
통합테스트의 일부로써 다루는 게 좋다.


내 생각

대망의 데이터베이스 테스트다. 이거 보려고 여기까지 읽었는데!
트랜잭션 부분은 이해하기 어려웠다. 기존에 서비스(책에서는 컨트롤러)에서 트랜잭션을 사용하도록 구현하는게 어렴풋이 맞는 것 같다. 이렇게 생각해보니 사실상 도메인 로직을 쓰는 모델 계층이 없는 것 같다는 생각이 들었다. 책을 보고 모델이 어떤 역할을 수행했는지 다시 봐야할 것 같다.

레포지토리 부분도 어려웠다. 좀 더 예제가 필요하다. Go로 책에 나온 패턴을 구현해봐야 할 것 같다. ORM과 레포지토리를 같이 쓰는 경우도 생각이 더 필요하다.

Go는 진정한 생성자가 없고 오브젝트 마더만 있는 것 같다는 생각이 들었다. 그러나 오브젝트 마더는 테스트에서 주로 쓰는 용어라고 한다.

읽기 테스트를 하지 말라고 하는데, 나는 사실 간단한 읽기가 아닌 리스트형 읽기를 테스트하고 싶다. 쿼리를 최적화하려고 수정하는 중 LEFT JOIN 조건 등이 잘못되어 회귀되는 경우가 종종 있었기 때문이다. 이 방법은 책에 안나오는 것 같으니 따로 공부해야 할 것 같다.

이쯤 되면 내가 만드는 시스템이 통합테스트정도만 필요한 복잡하지 않은 시스템인가 싶고 (기능만 많은) 도대체 유닛 테스트가 필요한 복잡한 로직의 시스템은 어떤게 있다는 건지 궁금하다.

다른 웹 프로젝트들을 봐야할 때인가보다.

'TIL' 카테고리의 다른 글

Go가 탄생한 이유  (0) 2025.01.19
1/15 TIL 단위 테스트 안티패턴  (0) 2025.01.16
1/13 TIL 목 처리  (0) 2025.01.13
1/12 TIL 통합테스트  (1) 2025.01.12
1/11 TIL 가치 있는 단위 테스트를 위한 리팩토링  (0) 2025.01.12