[강의메모] 스프링 DB 2편 -데이터 접근 활용 기술 - ch3. 테스트 (feat. 임베디드 모드 쓰고 싶다면? build.gradle에 우선 h2 설정부터!!)

@SpringBootTest

@SpringBootTest는 @SpringBootApplicatoin를 찾아서 설정으로 사용한다. 

 

테스트 - 데이터베이스 분리

컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하고 있으니 테스트에서 문제가 발생한다.

이런 문제를 해결하려면 테스트를 다른 환경과 철저하게 분리해야 한다.

 

가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다.

 

테스트에서 매우 중요한 원칙은 다음과 같다.

  • 테스트는 다른 테스트와 격리해야 한다. 
  • 테스트는 반복해서 실행할 수 있어야 한다.

DELETE SQL을 사용하면 되는 거 아냐?

 테스트가 끝날 때 마다 추가한 데이터에 DELETE SQL 을 사용해도 되겠지만, 이 방법도 궁극적인 해결책은 아니다. 

만약 테스트 과정에서 데이터를 이미 추가했는데, 테스트가 실행되는 도중에 예외가 발생하거나 애플리케이션이 종료되어 버려서 테스트 종료 시점에 DELETE SQL 을 호출하지 못할 수 도 있다! 

그러면 결국 데이터가 남아있게 된다.

 

우리에겐 데이터 롤백이 가능한 트랜잭션이 있다.

 

테스트 - 데이터 롤백

트랜잭션과 롤백 전략

테스트가 끝나고 나서 트랜잭션을 강제로 롤백해버리면 데이터가 깔끔하게 제거된다.

테스트를 하면서 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도 괜찮다. 

트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다.


이렇게 트랜잭션을 활용하면 테스트가 끝나고 나서 데이터를 깔끔하게 원래 상태로 되돌릴 수 있다.

 

테스트에 직접 트랜잭션 추가 

@SpringBootTest
class ItemRepositoryTest {
    @Autowired
    ItemRepository itemRepository;
    
    //트랜잭션 관련 코드
    @Autowired
    PlatformTransactionManager transactionManager;
    TransactionStatus status;

    @BeforeEach
    void beforeEach() {
    //트랜잭션 시작
        status = transactionManager.getTransaction(new
            DefaultTransactionDefinition());
    }

    @AfterEach
    void afterEach() {
    //MemoryItemRepository 의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
            ((MemoryItemRepository) itemRepository).clearStore();
        }
        //트랜잭션 롤백
        transactionManager.rollback(status);
    }
//...
}

 

트랜잭션 관리자는 PlatformTransactionManager 를 주입 받아서 사용하면 된다.

참고로 스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다. 

 

 

테스트 - @Transactional (이것만 쓰면 됨)

스프링은 테스트 데이터 초기화를 위해 트랜잭션을 적용하고 롤백하는 방식을 @Transactional 애노테이션 하나로 깔끔하게 해결해준다.

@Transactional
@SpringBootTest
class ItemRepositoryTest {}

 

@Transactional 원리

스프링이 제공하는 @Transactional 애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다.

그런데 @Transactional 애노테이션을 테스트에서 사용하면 아주 특별하게 동작한다.

 

@Transactional이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 
자동으로 롤백시켜 버린다!

 

@Transactional이 적용된 테스트 동작 방식

트랜잭션을 테스트에서 시작하기 때문에 서비스, 리포지토리에 있는 @Transactional 도 테스트에서 시작한 트랜잭션에 참여한다. (트랜잭션 전파) 

 

강제로 커밋하기 @Commit

@Transactional 을 테스트에서 사용하면 테스트가 끝나면 바로 롤백되기 때문에 테스트 과정에서 저장한 모든 데이터가 사라진다. 

당연히 이렇게 되어야 하지만, 정말 가끔은 데이터베이스에 데이터가 잘 보관되었는지 최종 결과를 눈으로 확인하고 싶을 때도 있다. 

 

이럴 때는 다음과 같이 @Commit 을 클래스 또는 메서드에 붙이면 테스트 종료후 롤백 
대신 커밋이 호출된다. 참고로 @Rollback(value = false) 를 사용해도 된다.

 

예시

@Commit
@Transactional 
@SpringBootTest
class ItemRepositoryTest {}

 

정리

  • 테스트가 끝난 후 개발자가 직접 데이터를 삭제하지 않아도 되는 편리함을 제공한다.
  • 테스트 실행 중에 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없다. 이 경우 트랜잭션을 커밋 
    하지 않기 때문에, 데이터는 자동으로 롤백된다. (보통 데이터베이스 커넥션이 끊어지면 자동으로 롤백되어 버린 
    다.)
  • 트랜잭션 범위 안에서 테스트를 진행하기 때문에 동시에 다른 테스트가 진행되어도 서로 영향을 주지 않는 장점이 
    있다.
  • @Transactional 덕분에 아주 편리하게 다음 원칙을 지킬수 있게 되었다.
    • 테스트는 다른 테스트와 격리해야 한다. 
    • 테스트는 반복해서 실행할 수 있어야 한다.

 

 

테스트 - 임베디드 모드 DB (build.gradle - h2 설정부터!!)

build.gradle에 일단 h2를 사용한다고부터 명시

testRuntimeOnly 'com.h2database:h2:2.1.214'

임베디드 모드

H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다.

그래서 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다. 

 

DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드(Embedded mode)라 한다. 

종료 시 함께 종료되고, 데이터도 모두 사라진다. (자바 메모리를 함께 사용하는 라이브러리처럼 동작)

 

조심해야할 부분, 애플리케이션과 함게 실행하기에 아무 테이블도 없는 상태이다.

 

스프링 부트 - 기본 SQL 스크립트를 사용해서 데이터베이스를 초기화하는 기능 (feat. src/test/resources/schema.sql)

메모리 DB는 애플리케이션이 종료될 때 함께 사라지기 때문에, 

애플리케이션 실행 시점에 데이터베이스 테이블도 새로 만들어주어야 한다.

 

스프링 부트는 SQL 스크립트를 실행해서 애플리케이션 로딩 시점에 데이터베이스를 초기화하는 기능을 제공한다.

 

src/test/resources/schema.sql

이름은 schema.sql 로 해야하고, 파일은 src/test/resources 하위에 생성한다.

 

예시

src/test/resources/schema.sql

drop table if exists item CASCADE; 
create table item
(
    id        bigint generated by default as identity, 
    item_name varchar(10),
    price integer, 
    quantity integer,
primary key (id) 
);

 

로그 확인

기본 SQL 스크립트가 잘 실행되는지 로그로 확인하려면 다음이 추가되어 있는지 확인하자.

src/test/resources/application.properteis

#schema.sql
logging.level.org.springframework.jdbc=debug

 

..init.ScriptUtils     : 0 returned as update count for SQL: create table item 
( id bigint generated by default as identity, item_name varchar(10), price 
integer, quantity integer, primary key (id) )

 

테스트 - 스프링 부트와 임베디드 모드

스프링 부트는 개발자에게 정말 많은 편리함을 제공하는데, 임베디드 데이터베이스에 대한 설정도 기본으로 제공한다.

 

test - application.properties에  별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터소스( DataSource )를 만들어서 제공한다.

 

conn0: url=jdbc:h2:mem:d8fb3a29-caf7-4b37-9b6c-b0eed9985454

로그를 보면 jdbc:h2:mem 뒤에 임의의 데이터베이스 이름이 들어가 있다. 이것은 혹시라도 여러 데이터소스가 사용될 때 같은 데이터베이스를 사용하면서 발생하는 충돌을 방지하기 위해 스프링 부트가 임의의 이름을 부여한 것이다.

 

임베디드 데이터베이스 이름을 스프링 부트가 기본으로 제공하는 jdbc:h2:mem:testdb 로 고정하고 싶으면

spring.datasource.generate-unique-name=false 추가

 

 

결론 

@Transactional을 사용하면 데스트의 중요한 원칙 2가지를 지킬 수 있다. 

- 테스트는 다른 테스트와 격리해야한다. 

- 테스트는 반복해서 실행할 수 있어야 한다. 

스프링부트는 test - application.properties에 아무 설정이 없다면? 임베디드 DB를 자동으로 실행시켜준다.


참고자료 및 출처

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard