Spring/Spring boot

[JDBC] 데이터 접근 기술 - 테스트

챛채 2023. 9. 10. 18:08

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

실제 데이터베이스를 연동해서 테스트를 하면 데이터베이스에 이미 과거에 서버를 실행하며 저장했던 데이터가 보관 되어있어서 이 데이터가 현재 테스트에 영향을 끼치게 된다.

이를 해결하기 위해서는 테스트를 다른 환경과 철저하게 분리해야하는데 가장 간단한 방법으로는 테스트 전용 데이터베이스를 별도로 운영하는 것이다.

 

H2 데이터베이스를 용도에 따라서 2가지로 구분한다.

  •  jdbc:h2:tcp://localhost/~/test : local에서 접근하는 서버 전용 데이터베이스
  •  jdbc:h2:tcp://localhost/~/testcase : test 케이스에서 사용하는 전용 데이터 베이스

하지만 테스트를 2번 이상 실행하게 되면 이전 테스트에서 저장된 데이터가 계속 남아있을 수 있기 때문에 두번째 테스트에 영향을 미치므로 각각의 테스트가 끝날 때 마다 해당 테스트에서 추가한 데이터를 삭제해야 한다.

 

테스트에서 매우 중요한 원칙에는 두가지가 있다.

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

물론 테스트가 끝날 때 마다 DELETE SQL을 사용해도 되지만, 비정상적으로 종료됐을 때 호출하지 못할 수도 있기 때문에 가장 도움이 되는 방법은 바로 트랜잭션이다. 

 

2. 테스트 - 데이터 롤백

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

중간에 테스트가 실패해서 롤백을 호출하지 못해도 트랜잭션을 커밋하지 않았기 때문에 해당 데이터가 반영되지 않는다.

 

@BeforeEach와 @AfterEach를 이용해 트랜잭션과 롤백을 적용하면 편리하다.

@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);
    }

 

3. 테스트 - @Transactional

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

 

@Transactional
@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);
    }

 

@Transactional 원리

@Transactional은 로직이 성공적으로 수행되면 커밋하도록 동작한다. 그런데 이를 테스트에서 사용하면 특별하게 동작을 하는데

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

 

강제로 커밋하기 - @Commit

@Transactional을 사용 했을 때 최종 결과를 굳이 눈으로 확인하고 싶다면 @Commit를 클래스 또는 메서드에 붙이면 테스트 종료 후 롤백 대신 커밋이 호출된다. @Commit 또는 @Rollback(value=flase)를 붙여도 된다.

 

@Commit
@Transactional
@SpringBootTest
class ItemRepositoryTest {}

 

4. 테스트 - 임베디드 모드 DB

테스트 케이스를 실행하기 위해서 별도의 데이터베이스를 설치하고, 운영하는 것은 상당히 번잡한 작업이다.

테스트가 끝나면 데이터베이스 자체를 제거해도 된다.

 

임베디드 모드

H2 데이터베이스는 자바로 개발되어 있고, JVM 안에서 메모리 모드로 동작하는 기능을 제공한다. 그래서 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다. 

즉, 애플리케이션에서 자바 메모리를 함께 사용하는 라이브러리처럼 동작한다.

 

임베디드 모드 직접 사용

[ItemServiceApplication.java]

@Slf4j
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web") //web범위 하위로만 컴포넌트 스캔(컨트롤러만)
public class ItemServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ItemServiceApplication.class, args);
	}

	@Bean
	@Profile("local") //특정 프로필이 활성화 되어있는 경우에만 빈으로 등록
	public TestDataInit testDataInit(ItemRepository itemRepository) {
		return new TestDataInit(itemRepository);
	}

	/*
    메모리 DB용 데이터소스
    */
	@Bean
	@Profile("test")
	public DataSource dataSource() {
		log.info("메모리 데이터베이스 초기화");
		DriverManagerDataSource dataSource = new DriverManagerDataSource();
		dataSource.setDriverClassName("org.h2.Driver");
		dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
		dataSource.setUsername("sa");
		dataSource.setPassword("");
		return dataSource;
	}
}

jdbc:h2:mem:db : 데이터소스 만들 때 이렇게만 적으면 임베디드 모드(메모리 모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.

DB_CLOSE_DELAY=-1 : 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.

 

하지만 막상 실행해보면 메모리 DB에는 아직 테이블을 만들지 않아 오류가 발생한다.

 

스프링 부트 - 기본 SQL 스크립트를 사용해서 데이터베이스를 초기화 하는 기능

JDBC나 JdbcTemplate를 직접 사용해서 테이블을 생성하는 DDL을 호출해도 되지만 너무 불편하기 때문에

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

 

다음 파일을 //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)
);

 

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

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

메모리 DB용 데이터소스 설정과 아래의 application.properties의 데이터베이스 설정정보도 지워도 된다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa
jdbcTemplate sql log

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

 

참고

임베디드 데이터베이스에 대한 스프링 부트의 더 자세한 설정은 여기서 찾아보면 된다.

https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.datasource.embedded

 

Data

The Spring Framework provides extensive support for working with SQL databases, from direct JDBC access using JdbcTemplate to complete “object relational mapping” technologies such as Hibernate. Spring Data provides an additional level of functionality

docs.spring.io