본문 바로가기
인프런/스프링 DB 2편 - 데이터 접근 활용 기술

[인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 3. 데이터 접근 기술 - 테스트

by hxxyeoniii 2024. 7. 22.

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

테스트 코드

@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);
    
    //여기서 3개 이상이 조회되는 문제가 발생
    test(null, null, item1, item2, item3);
}

 
로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 DB를 사용해 테스트에서 문제가 발생
-> 테스트를 다른 환경과 분리해야 함
 
 
간단한 방법은 DB를 용도에 따라 구분하는 것
-> 첫번째 테스트는 성공 but 두번째부터 실패
-> 첫번째 테스트에 저장한 데이터가 계속 남아있기 때문
 
 
=> 테스트는 다른 테스트와 격리해야 함
=> 테스트는 반복해 실행할 수 있어야 함


테스트 - 데이터 롤백

트랜잭션과 롤백 전략

@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를 주입 받아 사용 : 스프링 부트가 적절한 트랜잭션 매니저를 스프링 빈으로 등록해줌
-> @BeforeEach : 테스트 실행 직전 호출되므로 여기서 트랜잭션 시작 : transactionManager.getTransaction으로 트랜잭션 시작
-> @AfterEach : 테스트 완료 직후 호출되므로 여기서 트랜잭션 롤백 : transactionManager.rollback()으로 트랜잭션 롤백


테스트 - @Transactional

스프링은 @Transactional 애노테이션 하나로 트랜잭션을 적용 및 롤백해준다.

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

 

 

@Transactional 원리

1. @Transactional 애노테이션이 테스트 메서드나 클래스에 있으면 먼저 트랜잭션을 시작한다.
2. 테스트 로직을 실행한다. 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행된다.
   -> 트랜잭션은 기본적으로 전파되기 때문에, 리포지토리에서 사용하는 JdbcTemplate도 같은 트랜잭션을 사용한다.
3. 테스트 실행 중 item1, item2, item3을 DB에 저장한다.
   -> 테스트가 리포지토리를 호출하고 리포지토리는 JdbcTemplate을 사용해 데이터를 저장한다.
4. 검증을 위해 item1, item2, item3을 조회한다.
5. @Transactional이 테스트에 있으면 테스트가 끝날때 트랜잭션을 강제로 롤백한다.
7. 롤백에 의해 DB의 item1, item2, item3 데이터가 제거된다.
 
 
* 테스트 케이스의 메서드나 클래스에 @Transactional을 직접 붙여 사용할 때만 이렇게 동작한다.
트랜잭션을 테스트에서 시작하기 때문에 서비스나 리포지토리에 있는 @Transactional도 테스트에 시작한 트랜잭션에 참여한다.
= 같은 트랜잭션을 사용한다.
 
 
=> 테스트가 끝난 후 데이터를 직접 삭제하지 않아도 됨
=> 테스트 실행 중 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없음(보통 DB 커넥션이 끊어지면 자동으로 롤백됨)
=> 트랜잭션 범위 안에서 테스트를 진행하기 때문에 다른 테스트가 진행되어도 서로 영향을 주지 않음
 
 
@Commit
가끔 DB에 데이터가 잘 보관되었는지 확인을 위해 커밋을 해야하는 경우도 존재
이럴 경우, @Commit 또는 @Rollback(value = false)를 사용하면 된다.


테스트 - 임베디드 모드 DB

테스트를 위해 별도 DB를 설치, 운영하는 것은 상당히 번잡한 작업
 
 
임베디드 모드
H2 데이터베이스는 자바로 개발되어 있고, JVM 안에서 메모리 모드로 동작하는 특별한 기능을 제공
애플리케이션 실행 시 H2 DB도 해당 JVM 메모리에 포함해 함께 실행할 수 있음
DB를 애플리케이션에 내장해 함께 실행한다고 하여 임베디드 모드라 함
 
-> 애플리케이션이 종료되면 임베디드 모드의 H2 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;
    }

-> Profile("test") : 프로필이 test인 경우에만 데이터소스를 스프링 빈으로 등록
-> dataSource()
        : jdbc:h2:mem:db 처럼 적을 시 임베디드 모드로 동작하는 H2 DB를 사용할 수 있음(중요)
        : DB_CLOSE_DELAY=-1 은 임베디드 모드에서 DB 커넥션이 끊어지면 데이터베이스가 종료되는데 이것을 방지하는 설정
 
 
스프링 부트 : 기본 SQL 스크립트를 사용해 DB를 초기화하는 기능 제공
* 메모리 DB는 애플리케이션이 종료될 때 함께 사라지기 때문에, 실행 시점에 테이블도 새로 만들어주어야 함!
* 스프링 부트는 SQL 스크립트를 실행해 애플리케이션 로딩 시점에 DB를 초기화하는 기능을 제공함
 
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)
 );

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

사실 스프링 부트는 임베디드 DB에 대한 설정도 기본으로 제공한다.
 
데이터베이스에 접근하는 설정 정보 주석처리
-> 별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터소스를 만들어 제공
 
 
application.properties

spring.profiles.active=test

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

logging.level.org.springframework.jdbc=debug

 
 
임베디드 DB 이름을 스프링 부트가 기본으로 제공하는 jdbc:h2:mem:testdb로 고정하고 싶으면 application.properties에 다음 설정을 추가하면 됨!

spring.datasource.generate-unique-name=false