SPRING

스프링 DB 2편 - 데이터 접근 활용 기술 (3)

illho 2023. 6. 18. 15:31

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

 

스프링 DB 2편 - 데이터 접근 활용 기술 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의 소개 | 인

www.inflearn.com

 

데이터 접근 기술 - 테스트

 

데이터 베이스에 연동하는 테스트에 대해서 알아보았다.

데이터 접근 기술은 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요하다.

 

 

src/main/resources/application.properties

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

 

src/test/resources/application.properties 

spring.profiles.active=test

 

테스트 케이스는 src/test에 있기 때문에 실행하면 src/test에 있는 application.properties 파일이 우선순위를 가지고 실행된다. 

 

test - application.properties 수정

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

 

테스트 실행 코드는 @SpringBootTest를 사용하기 때문에 @SpringBootApplication를 찾아서 설정으로 사용한다.

 

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

@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데이터베이스로 testcase 라는 db를 만들었다.

 

src/test/resources/application.properties

spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
spring.datasource.username=sa

 

다시 테스트를 실행했지만 처음에만 실행에 성공하고 그 뒤에 계속 오류가 발생한다.

테스트를 2번째 실행할 때는 처음 테스트를 실행할 때 데이터가 남아 있기 때문에 두번째 테스트에 영향을 주기 때문이다.

 

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

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

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

 

테스트 - 데이터 롤백

트랜잭션을 활용하면 위와 같은 문제를 해결할 수 있다.

테스트에 @BeforeEach , @AfterEach 라는 기능을 사용한다.

 

@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

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

 

 

@Transactional 를 사용해서 위와 같은 소스들을 전부 쓰지 않아도 된다.

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

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

 

테스트 - 임베디드 모드 DB

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

 

임베디드 모드

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

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

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

 

ItemServiceApplication

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

프로필이 test 인 경우에만 데이터 소스를 스프링 빈으로 등록한다.

dataSource() 에서 jdbc:h2:mem:db => 임베디드 모드로 동작하는 H2 데이터베이스를 사용한다.

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

 

메모리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)
);

 

H2 데이터베이스를 끄고 ItemRepositoryTest 를 실행해보면 정상적으로 수행한다.

 

SQL스크립트가 로그에 잘 찍히는 것을 볼 수 있다.

 

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

스프링 부트는 임베디드 데이터베이스에 대한 설정도 기본으로 제공한다.

앞에서 했던 메모리 DB용 dataSource()를 주석하고 , test - application.properties 에서 db 설정 정보도 주석처리 한다.

이러면 데이터베이스에 접근하는 모든 설정 정보가 사라지게 된다.

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

 

ItemRepositoryTest를 실행하면 테스트가 정상 수행한다.

 

 

jdbc:h2:mem 뒤에 임의의 데이터베이스 이름은 여러 데이터소스가 사용될 때 발생하는 충돌을 방지하기위해 스프링 부트가 임의로 부여한 것이다.