포스트

(JDBC - 4) 스프링에서의 데이터베이스 테스트

JDBC, JdbcTemplate, JPA 등의 데이터 접근 기술을 사용할 때 데이터베이스와 관련된 테스트에 대하여


JDBC, JdbcTemplate, MyBatis, JPA 등의 데이터 접근 기술을 사용할 때 데이터베이스와 관련된 테스트에 대해 알아보자.


1. 데이터베이스 연동

JDBC, JdbcTemplate, MyBatis, JPA 등의 데이터 접근 기술을 사용할 때, 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회하는지 테스트를 할 수 있어야한다.


테스트를 진행하기 전 test/resources/application.properties를 수정해야한다.

1
2
3
4
5
6
7
8
9
10
spring.profiles.active=test

# spring.sql.init.mode=always
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/real_test_database?serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=admin

logging.level.org.springframework.jdbc=debug


다음의 테스트 코드로 테스트를 실행해보자.


ItemRepositoryTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@SpringBootTest
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;

    @AfterEach
    void afterEach() {
        // MemoryItemRepository의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
            ((MemoryItemRepository) itemRepository).clearStore();
        }
    }

    @Test
    void save() {
        // given
        Item item = new Item("itemA", 10000, 10);

        // when
        Item savedItem = itemRepository.save(item);

        // then
        Item findItem = itemRepository.findById(item.getId()).get();
        assertThat(findItem).isEqualTo(savedItem);
    }

    @Test
    void updateItem() {
        // given
        Item item = new Item("item1", 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();

        // when
        ItemUpdateDto updateParam = new ItemUpdateDto("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);

        // then
        Item findItem = itemRepository.findById(itemId).get();
        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
    
    /**
     * 테스트 실패!
     * 원인은 이전 데이터베이스를 사용하면서 남았던 데이터 때문
     * 테스트를 할 때 기본적으로 격리된 데이터베이스에서 수행해야함
     * 지금의 상황은 마치 프로덕션이나 개발환경 DB를 테스트용으로 사용한것과 마찬가지
     */
    @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);
    }

    void test(String itemName, Integer maxPrice, Item... items) {
        List<Item> result = itemRepository.findAll(new ItemSearchCond(itemName, maxPrice));
        assertThat(result).containsExactly(items);
    }
}
  • 테스트를 실행해보면, updateItem()save()는 정상적으로 테스트를 통과하지만 findItems()는 실패한다
  • findItems()의 실패 원인은 과거에 서버를 실행하면서 저장했던 데이터가 데이터베이스에 보관되어 있기 때문이다
  • 외부에 영향 받지 않는 격리된 환경에서 테스트하는 것이 중요!
    • 지금의 테스트는 기존 DB를 사용하는 것이기 때문에 문제 발생


데이터베이스의 분리를 통해 문제를 해결해보자.



2. 데이터베이스 분리

이전 테스트에서의 문제를 해결하기 위해서 테스트 전용으로 데이터베이스를 분리해보자.


test/resources/application.properties에서의 DB 엔드포인트 설정을 수정하자.

1
spring.datasource.url=jdbc:mysql://localhost:3306/testcode_database?serverTimezone=Asia/Seoul
  • 가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것
  • 기존 MySQL 컨테이너에서 create database testcode_database;로 테스트코드용 DB를 생성하자
  • 기존의 jdbc:mysql://localhost:3306/test_database에서 test_databasetestcode_database로 수정


이제 다시 한번 findItems() 테스트를 실행해보자. 이번에는 통과하는 것을 확인할 수 있다. 그러나 다시 한번 findItems()를 실행하면 다시 실패하는 것을 볼 수 있다.

실패하는 이유는 테스트를 실행할 때 입력한 데이터가 데이터베이스에 누적되고 있기 때문이다. 우리가 제일 처음 겪었던 문제가 이전 개발 환경에서 사용한 데이터베이스의 기존에 존재했던 데이터 때문이라면, 이번에는 테스트를 실행하면서 생긴 데이터 때문에 반복 테스트가 실패한 것이다. 이를 해결하기 위해서는 테스트 후에 데이터베이스를 초기화 하거나 지우는 작업이 필요하다.


롤백(rollback)을 통해 문제를 해결해보자.



3. Rollback

롤백을 이용해서 테스트 후 데이터가 누적되는 문제를 해결해보자.

테스트가 끝나고 트랜잭션을 강제로 롤백하면 데이터를 깔끔하게 제거할 수 있다. 데스트 중에 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도, 트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다.


코드를 통해 알아보자.


ItemRepositoryTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@SpringBootTest
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;

    /**
     * DataSource와 마찬가지로 스프링에서 자동으로 빈 등록해줌
     */
    @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를 주입받아서 사용
    • 스프링이 자동으로 빈 등록 해줌
  • @BeforeEach
    • 각각의 테스트 케이스 실행전에 호출된다
    • 여기에서 트랜잭션을 시작하도록 한다
    • 각 테스트를 트랜잭션 범위 안에서 실행할 수 있게 된다
  • @AfterEach
    • 각 테스트 케이스 실행 완료 후에 호출된다
    • 여기에서 트랜잭션을 롤백한다
    • 롤백 후 트랜잭션 실행 전 상태로 복구된다


지금까지 구현한 트랜잭션과 롤백에 대한 기능을 편리하게 사용할 수 있도록 해주는 @Transactional이 존재한다.



4. 테스트에서의 @Transactional

트랜잭션의 롤백을 편리하게할 수 있는 @Transactional에 대해 알아보자.

트랜잭션에 사용하던 @Transactional을 테스트에서 사용하면 조금 다르게 동작한다.

코드를 통해 알아보자.


기존 테스트를 복사해서 ItemRepositoryTestV2로 만들고. 위에 @Transactional을 붙이자.

1
2
3
4
5
6
7
8
@Transactional
@SpringBootTest
class ItemRepositoryTestV2 {
  /**
   * 기존 PlatformTransactionManager, @BeforeEach, @AfterEach 삭제
   * 이전의 트랜잭션 관련 코드 전부 제거해도 됨
   */
}
  • 이전의 트랜잭션을 적용한 코드와 마찬가지로 정상적으로 동작한다


기존 @Transactional 애노테이션은 로직이 성공적으로 수행되면 커밋하도록 동작한다. 그러나 @Transactional을 테스트에서 사용하면 다음과 같이 동작한다.

테스트에서 @Transactional을 사용하는 경우, 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시킨다.


참고로 데이터베이스에 정말로 데이터가 잘 보관되는지 눈으로 확인해보고 싶은 경우라면, 다음과 같이 @Commit을 붙이면 테스트 종류후 롤백 대신 커밋이 된다.

1
2
3
4
5
@Commit
// @Rollback(value = false)
@Transactional
@SpringBootTest
class ItemRepositoryTest {}
  • @Rollback(value = false)@Commit과 똑같은 기능을 한다



5. 임베디드 모드(Embedded Mode)

임베디드 모드를 사용하기 위해서는 먼저 H2 데이터베이스를 라이브러리에 추가하자.


build.gradle

1
2
3
4
5
dependencies {
    //...
    // 추가
    runtimeOnly 'com.h2database:h2'
}


test/resources/application.propertiesdatasource관련 설정을 주석 처리 하자.

1
2
3
4
5
6
7
8
9
10
spring.profiles.active=test

# spring.sql.init.mode=always
# spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# spring.datasource.url=jdbc:mysql://localhost:3306/real_test_database?serverTimezone=Asia/Seoul
# spring.datasource.username=root
# spring.datasource.password=admin

logging.level.org.springframework.jdbc=debug


별다른 정보를 제공하지 않으면 스프링 부트는 임베디드 모드로 접근하는 DataSource를 만들어서 제공한다.



Reference

  1. 인프런 - 김영한 : 스프링 완전 정복
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Comments powered by Disqus.