Spring DB - DB 테스트

Kwon Yongho·2023년 5월 18일
0

Spring-DB

목록 보기
9/16
post-thumbnail

DB 테스트

  1. 데이터베이스 연동
  2. 데이터베이스 분리
  3. 데이터 롤백
  4. @Transactional
  5. 임베디드 모드 DB
  6. 스프링 부트와 임베디드 모드

1. 데이터베이스 연동

데이터 접근 기술에 대해서 더 알아보기 전에 데이터베이스에 연동하는 테스트에 대해서 알아보자.

test - application.properties 수정
src/test/resources/application.properties

spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
logging.level.org.springframework.jdbc=debug

ItemServiceApplication

@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
  • ItemRepositoryTest@SpringBootTest를 사용한다. @SpringBootTestSpringBootApplication를 찾아서 설정으로 사용한다.
  • @SpringBootApplication설정이 과거에는 MemoryConfig.class를 사용하다가 이제는 JdbcTemplateV3Config.class를 사용하도록 변경되었다. 따라서 테스트도 JdbcTemplate 을 통해 실제 데이터베이스를 호출하게 된다.

테스트 실행
ItemRepositoryTest 실행

  • updateitem(): 성공
  • save(): 성공
  • findItems(): 실패
    @Test
    void findItems() {
        //given
        Item item1 = new Item("itemA-1", 10000, 10);
        Item item2 = new Item("itemA-2", 20000, 20);
        Item item3 = new Item("itemB-1", 30000, 30);

        itemRepository.save(item1);
        itemRepository.save(item2);
        itemRepository.save(item3);

        //둘 다 없음 검증
        test(null, null, item1, item2, item3);
        test("", null, item1, item2, item3);

        //itemName 검증
        test("itemA", null, item1, item2);
        test("temA", null, item1, item2);
        test("itemB", null, item3);

        //maxPrice 검증
        test(null, 10000, item1);

        //둘 다 있음 검증
        test("itemA", 10000, item1);
    }

실패 원인
문제는 H2 데이터베이스에 이미 과거에 서버를 실행하면서 저장했던 데이터가 보관되어 있기 때문이다.

2. 데이터베이스 분리

  • 로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하고 있으니 테스트에서 문제가 발생한다.
  • 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다.

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

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

데이터베이스 파일을 생성 후 테이블을 생성했다.

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

test - application.properties 변경

spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase

이제 테스트를 실행

  • 맨 처음에는 실행에 성공한다.
  • 그런데 같은 findItems() 테스트를 다시 실행하면 테스트에 실패한다.
  • 처음 테스트를 실행할 때 저장한 데이터가 계속 남아있기 때문에 두번째 테스트에 영향을 준 것이다. 이 문제는 save()같은 다른 테스트가 먼저 실행되고 나서 findItems()를 실행할 때도 나타난다. 다른 테스트에서 이미 데이터를 추가했기 때문이다. 결과적으로 테스트 데이터가 오염된 것이다.
  • 이 문제를 해결하려면 각각의 테스트가 끝날 때 마다 해당 테스트에서 추가한 데이터를 삭제해야 한다. 그래야 다른 테스트에 영향을 주지 않는다.

테스트에서 매우 중요한 원칙

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

3. 데이터 롤백

트랜잭션과 롤백 전략

  • 이때 도움이 되는 것이 바로 트랜잭션이다.
  • 테스트가 끝나고 나서 트랜잭션을 강제로 롤백해버리면 데이터가 깔끔하게 제거된다.
  • 테스트를 하면서 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도 괜찮다. 트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다.
  • 테스트는 각각의 테스트 실행 전 후로 동작하는 @BeforeEach, @AfterEach라는 편리한 기능을
    제공한다.

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를 주입 받아서 사용하면 된다. 참고로 스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다.
  • transactionManager.getTransaction(new DefaultTransactionDefinition()) 로 트랜잭션을 시작한다.
  • @AfterEach: 각각의 테스트 케이스가 완료된 직후에 호출된다. 따라서 여기서 트랜잭션을 롤백하면 된다. 그러면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다.
    • transactionManager.rollback(status)로 트랜잭션을 롤백한다.

TestCase 데이터베이스에 데이터를 모두 삭제하고 다시 테스트를 돌려보면 성공하는 것을 확인 할 수 있다.

4. @Transactional

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

ItemRepositoryTest @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 애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다.
  • 그런데 @Transactional 애노테이션을 테스트에서 사용하면 아주 특별하게 동작한다.
  • @Transactional이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시켜 버린다.

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

  • 간단하게 설명하면 @Transactional이 있으면 테스트 로직 실행 시 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행되고 테스트가 끝나면 트랜잭션을 강제로 롤백한다. 롤백에 의해 앞서 데이터베이스에 저장된 데이터는 제거된다.

강제로 커밋하기 - @Commit
가끔은 데이터베이스에 데이터가 잘 보관 되었는지 최종 결과를 눈으로 확인하고 싶을 때가 있다. 이럴 때는 @Commit을 클래스 또는 메서드에 붙이면 테스트 종료 후 롤백 대신 커밋이 호출된다. @Rollback(value = false)를 사용해도 동일한 결과가 나온다.

5. 임베디드 모드 DB

임베디드 모드

  • H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다.
  • 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다.
  • DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드(Embedded mode)라 한다.
  • 쉽게 이야기해서 애플리케이션에서 자바 메모리를 함께 사용하는 라이브러리처럼 동작하는 것이다.

ItemServiceApplication 변경

package hello.itemservice;

import hello.itemservice.config.JdbcTemplateV3Config;
import hello.itemservice.repository.ItemRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Slf4j
//@Import(MemoryConfig.class)
//@Import(JdbcTemplateV1Config.class)
//@Import(JdbcTemplateV2Config.class)
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.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);
	}
	@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;
	}

}
  • @Profile("test")
    • 프로필이 test 인 경우에만 데이터소스를 스프링 빈으로 등록한다.
    • 테스트 케이스에서만 이 데이터소스를 스프링 빈으로 등록해서 사용하겠다는 뜻이다.
  • dataSource()
    • jdbc:h2:mem:db: 이 부분이 중요하다. 데이터소스를 만들때 이렇게만 적으면 임베디드 모드(메모리모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.
    • DB_CLOSE_DELAY=-1: 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.
    • 이 데이터소스를 사용하면 메모리 DB를 사용할 수 있다.

H2 데이터베이스 서버를 끄고 ItemRepositoryTest

  • Table "ITEM" not found이 부분이 핵심이다. 데이터베이스 테이블이 없는 것이다.
  • 생각해보면 메모리 DB에는 아직 테이블을 만들지 않았다.

스프링은.. 다양한 기능을 제공한다
스프링 부트 - 기본 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)
);

ItemRepositoryTest 테스트

  • 테스트가 정상 수행되는 것을 확인할 수 있다.

6. 스프링 부트와 임베디드 모드

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

임베디드 모드 DB 관련 설정을 모두 주석처리했다.

설정을 하지 않아도 정상 수행 된다.

참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB

0개의 댓글