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

Ch03. 테스트 - @Transactional, 임베디드 모드 DB, 스프링 부트와 임베디드 모드

webmaster 2022. 6. 26. 13:28
728x90

@Transactional

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

@SpringBootTest
@Transactional
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 이 테스트에 있으면 스프링은 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시켜 버린다!

그림으로 알아보는 Test에서 트랜잭션 동작 과정

  1. 테스트에 @Transactional 애노테이션이 테스트 메서드나 클래스에 있으면 먼저 트랜잭션을 시작한다.
  2. 테스트를 로직을 실행한다. 테스트가 끝날 때까지 모든 로직은 트랜잭션 안에서 수행된다. 트랜잭션은 기본적으로 전파되기 때문에, 리포지토리에서 사용하는 JdbcTemplate도 같은 트랜잭션을 사용한다. 
  3. 테스트 실행 중에 INSERT SQL을 사용해서 item1 , item 2 , item3을 데이터베이스에 저장한다. 물론 테스트가 리포지토리를 호출하고, 리포지토리는 JdbcTemplate을 사용해서 데이터를 저장한다.
  4. 검증을 위해서 SELECT SQL로 데이터를 조회한다. 여기서는 앞서 저장한 item1 , item2 , item3 이 조회되었다. SELECT SQL도 같은 트랜잭션을 사용하기 때문에 저장한 데이터를 조회할 수 있다. 다른 트랜잭션에서는 해당 데이터를 확인할 수 없다. 여기서 assertThat()으로 검증이 모두 끝난다.
  5. @Transactional 이 테스트에 있으면 테스트가 끝날 때 트랜잭션을 강제로 롤백한다. 
  6. 롤백에 의해 앞서 데이터베이스에 저장한 item1 , item2 , item3의 데이터가 제거된다.

참고 

테스트 케이스의 메서드나 클래스에 @Transactional을 직접 붙여서 사용할 때 만 이렇게 동작한다. 

그리고 트랜잭션을 테스트에서 시작하기 때문에 서비스, 리포지토리에 있는 @Transactional 도 테스트에서 시작한 트랜잭션에 참여한다. (이 부분은 뒤에 트랜잭션 전파에서 더 자세히 설명하겠다. 지금은 테스트에서 트랜잭션을 실행하면 테스트 실행이 종료될 때까지 테스트가 실행하는 모든 코드가 같은 트랜잭션 범위에 들어간다고 이해하면 된다. 같은 범위라는 뜻은 쉽게 이야기해서 같은 트랜잭션을 사용한다는 뜻이다. 그리고 같은 트랜잭션을 사용한다는 것은 같은 커넥션을 사용한다는 뜻이기도 하다.)

강제 Commit(@Rollback(false) / @Commit)

@Transactional을 테스트에서 사용하면 테스트가 끝나면 바로 롤백되기 때문에 테스트 과정에서 저장한 모든 데이터가 사라진다. 당연히 이렇게 되어야 하지만, 정말 가끔은 데이터베이스에 데이터가 잘 보관되었는지 최종 결과를 눈으로 확인하고 싶을 때도 있다. 이럴 때는 다음과 같이 @Commit을 클래스 또는 메서드에 붙이면 테스트 종료 후 롤백 대신 커밋이 호출된다. 참고로 @Rollback(value = false)를 사용해도 된다.

임베디드 모드 DB

테스트 케이스를 실행하기 위해서 별도의 데이터베이스를 설치하고, 운영하는 것은 상당히 번잡한 작업이다. 단순히 테스트를 검증할 용도로만 사용하기 때문에 테스트가 끝나면 데이터베이스의 데이터를 모두 삭제해도 된다. 더 나아가서 테스트가 끝나면 데이터베이스 자체를 제거해도 된다.

임베디드 모드

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

ItemServiceApplication

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

}
  • @Profile("test")
    • Profile이 test 인 경우에만 데이터 소스를 스프링 빈으로 등록한다.
    • 테스트 케이스에서만 이 데이터 소스를 스프링 빈으로 등록해서 사용하겠다는 뜻이다.
  • dataSource()
    • jdbc:h2:mem:db : 이 부분이 중요하다. 데이터소스를 만들 때 이렇게만 적으면 임베디드 모드(메모리 모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.
    • DB_CLOSE_DELAY=-1 : 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.
    • 이 데이터 소스를 사용하면 메모리 DB를 사용할 수 있다.
  • 실행 시, 오류가 발생한다
    • WHY? 메모리 DB는 애플리케이션이 종료될 때 함께 사라지기 때문에, 애플리케이션 실행 시점에 데이터베이스 테이블도 새로 만들어주어야 한다.

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)
);
  • JDBC나 JdbcTemplate를 직접 사용해서 테이블을 생성하는 DDL을 호출해도 되지만, 너무 불편하다. 스프링 부트는 SQL 스크립트를 실행해서 애플리케이션 로딩 시점에 데이터베이스를 초기화하는 기능을 제공한다.
  • 파일 이름, 위치 모두 맞아야 스프링 부트가 해당 스키마를 읽어 테이블 생성을 해준다.

SQL 스크립트를 사용해서 데이터베이스를 초기화하는 자세한 방법은 다음 스프링 부트 공식 매뉴얼을 참고하자

https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.datainitialization.using-basic-sql-scripts

 

“How-to” Guides

Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework’s spring-jcl module. To use Logback, you need to include it and spring-jcl on the classpath. The recommended way to do th

docs.spring.io

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

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

ItemServiceApplication

@Slf4j
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {

 	...

   /* //주석 처리(dataSource 없다)
   @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/resource/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
  • 이렇게 하면 데이터베이스에 접근하는 모든 설정 정보가 사라지게 된다.
  • 이렇게 별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터 소스( DataSource )를 만들어서 제공한다. 바로 앞서 우리가 직접 만든 데이터 소스와 비슷하다 생각하면 된다.
  • 참고로 로그를 보면 다음 부분을 확인할 수 있는데 jdbc:h2:mem 뒤에 임의의 데이터베이스 이름이 들어가 있다. 이것은 혹시라도 여러 데이터 소스가 사용될 때 같은 데이터베이스를 사용하면서 발생하는 충돌을 방지하기 위해 스프링 부트가 임의의 이름을 부여한 것이다

자동으로 SpringBoot가 임베디드 DB로 연결한다.

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

spring.datasource.generate-unique-name=false

임베디드 데이터베이스에 대한 스프링 부트의 더 자세한 설정은 다음 공식 매뉴얼을 참고하자. https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.datasource.embedded

 

Spring Boot Reference Documentation

The reference documentation consists of the following sections: Legal Legal information. Getting Help Resources for getting help. Documentation Overview About the Documentation, First Steps, and more. Getting Started Introducing Spring Boot, System Require

docs.spring.io

 

728x90