[스프링 DB 2편] - 테스트

Chooooo·2023년 1월 31일
0

스프링 DB 2편

목록 보기
4/8
post-thumbnail

이 글은 강의 : 김영한님의 - "[스프링 DB 2편 - 데이터 접근 활용 기술]"을 듣고 정리한 내용입니다. 😁😁


데이터 접근 기술을 사용할 떄 어떻게 테스트를 할 것이냐. 매우 중요한 문제.

  • 데이터베이스를 연동한 상태에서 테스트를 어떻게 할 것인지 알아보자.

😀 데이터베이스 연동

데이터 접근 기술은 DB에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요하다. ItemRepositoryTest를 통해서 테스트를 진행한다.

테스트를 실행하기 전에 먼저 지금까지 설정한 application.properties 를 확인해보자.

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

어플리케이션용 application.properties와 테스트용 application.properties에는 각각 Profile에 대한 값이 설정되어있다. 각각의 Profile에 따라 다르게 동작할 수 있다. 그리고 DB 설정 정보를 어떻게 하느냐에 따라 다르게 동작할 수 있다.

@SpringBootTest

⚽ @SpringBootTest어노테이션이 존재하는 클래스는 테스트를 실행하면 @SpringBootApplication 어노테이션을 검색한다.

⚽ @SpringBootApplication에서 설정된 값을 읽어와서, 테스트에서 사용한다.

  • 위의 내용을 정리하면 스프링 컨테이너에 등록된 빈을 불러와서 테스트에 사용한다는 것으로 이해할 수 있다. 즉, 스프링 컨테이너에 의존성이 생기게 된다.

findIems 테스트 코드에서 문제

  • 에러 코드를 확인해보면 3개의 값만 저장되어 있어야 하는데, 2개의 값이 더 저장되었다는 것이다. 위의 테스트 코드를 살펴보면 3개의 아이템만 저장한다. 이렇게 테스트가 실패한 이유는 앞에서 실행되었던 테스트 결과가 다른 테스트에 영향을 주기 때문으로 이해를 할 수 있다. 즉, item2 / itemA라는 애들이 앞선 테스트 결과로 DB에 저장되어있던 것이 영향을 준 것이다.

DB 테스트 코드의 필요성

테스트에서 중요한 것은 격리성이다. 테스트와 테스트는 서로에게 영향을 주지 않아야 하고, 이를 위해서 각 테스트는 서로 격리되어야 한다.

DB 분리

어플리케이션 서버 / 테스트가 동일한 DB를 사용하고 있으므로 테스트에서 문제가 발생할 수 있다. 예를 들어 어플리케이션에서 사용했던 값이 DB에 남아있는 경우, 테스트 코드에 영향을 줄 수 있다. 따라서 이 문제를 해결하기 위해 DB 분리를 고려할 수 있다.

  • 이런 문제를 해결하려면 테스트를 다른 환경과 철저하게 분리 !

⚽ 접속 정보 변경

  • 이제 접속 정보를 변경해야 한다. main의 application.properties는 그대로 유지하고, test에 있는 application.properties만 변경 !

src/main/resources/application.properties

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

src/test/resources/application.properties

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

DB 분리 후 다시 테스트를 실행해도 동일한 이유로 findItems()는 실패한다. 이 때의 실패한 이유는 여전히 updateItem() / save()의 실행 결과가 테스트 DB에 남아있기 때문이다. 이것을 해결 하기 위해서는 테스트 이후 매번 데이터를 삭제 해주어야 한다.

데이터를 삭제해주는 방법은 매 테스트마다 DELETE SQL을 사용하는 방법이 있다. 그렇지만 매번 SQL문을 작성하는 것은 번거롭기 때문에 Transaction 아래에서 테스트를 실행하면 좀 더 손쉽게 테스트 이후 데이터를 삭제할 수 있다.

🎈 테스트의 중요한 원칙

  • 테스트는 다른 테스트와 격리되어야 한다.
  • 테스트는 반복해서 실행할 수 있어야 한다.
    위의 원칙을 지킬 수 있도록 매 테스트가 끝난 후, 테스트의 결과가 DB에 남아있지 않도록 해야한다.

😀 데이터 롤백

트랜잭션과 롤백 전략

  • 테스트 사이에 데이터를 격리하기 위해 도움을 주는 것이 "트랜잭션"이다.
  • 테스트가 끝나고 나서 트랜잭션을 강제로 롤백하면 데이터가 깔끔하게 제거된다. 뿐만 아니라 예외가 발생하더라도, 커밋하지 않았기 때문에 데이터가 반영되지 않는다.

@BeforeEach / @AfterEach를 이용한 트랜잭션 적용

🎈 @BeforeEach : 각각의 테스트 케이스를 실행하기 직전에 호출됨. 따라서 여기서 트랜잭션을 시작하면 된다.

🎈 @AfterEach : 각각의 테스트 케이스가 완료된 직후에 호출됨. 따라서 여기서 트랜잭션을 롤백하면 된다.

Jdbc에서 사용하는 Query들은 결국 Connection을 사용해서 DB에 접근한다. 이 때, 사용하는 Connection은 트랜잭션 매니저가 참조하는 트랜잭션 동기화 매니저에 있는 Connection이다. 따라서 트랜잭션이 자동으로 동기화 된다.

@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus transaction;
  • 먼저 트랜잭션 동기화를 위한 트랜잭션 매니저를 주입받는다.
  • 트랜잭션을 클래스 전체에서 사용하기 위해 클래스 변수로 선언한다.
@BeforeEach
void beforeEach() {
    transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
}

@BeforeEach에서 클래스 변수에 대한 transaction을 만들어서 주입해준다.
이 때, transaction은 트랜잭션 매니저의 트랜잭션 동기화 매니저 커넥션을 가져옴.

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

    // 트랜잭션 롤백
    transactionManager.rollback(transaction);
}

@AfterEach에서 테스트가 끝날 때, 자동으로 트랜잭션의 데이터를 롤백
테스트 도중 예외가 발생할 경우, 자동으로 트랜잭션이 롤백되므로 원하는 목적을 달성할 수 있다.

🎃 @BeforeEach / @AfterEach를 사용하면 다음과 같이 테스트 데이터를 초기화 할 수 있다. 그렇지만 트랜잭션 및 롤백 처리를 위해서 불필요한 코드 작성이 많아진다. 스프링은 @Transactional 어노테이션을 이용해서 다음 기능을 간소화 시켜준다.

👻 @Transactional

앞서 @BeforeEach / @AfterEach를 이용해서 테스트 데이터를 초기화 했었다. 불필요한 코드 작성이 많아지는데 @Transactional을 이용하면 조금 더 깔끔하게 해결할 수 있다.

  • 스프링이 제공하는 @Transactional은 로직이 성공적으로 수행되면 커밋하도록 동작한다.
  • @Transacation을 테스트에서 사용하면 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백시킨다.

트랜잭션이 적용된 테스트의 동작 방식

🎃 테스트에 @Transactional이 있으면 테스트 메서드나 클래스에 있으면 먼저 트랜잭션을 시작한다.

🎃 테스트 로직을 실행한다. 테스트가 끝날 때 까지 모든 로직은 트랜잭션 안에서 수행된다.

  • 트랜잭션은 기본적으로 전파되기 때문에 리포지토리에서 사용하는 JdbcTemplate도 같은 트랜잭션을 사용한다.

🎃 @Transactional이 테스트에 있으면 테스트가 끝날 때, 트랜잭션을 강제로 롤백시킨다.

- 정리하면 @Transacational을 이용하면 자동으로 트랜잭션을 걸어서 테스트를 실행해주고, 끝날 때는 RollBack 처리를 해준다는 것이다.

사용을 위해서는 @Transactional만 클래스 위에 붙여주면 된다.

@Transactional
@SpringBootTest
public class ItemRepository {
~~~
}

@Transactional 정리

🎃 테스트가 끝난 후 개발자가 직접 데이터를 삭제하지 않아도 되는 편리함을 제공함
🎃 테스트 실행 중에 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없음. 이 경우 트랜잭션을 커밋하지 않기 때문이다.
🎃 트랜잭션 범위 안에서 테스트를 진행하기 때문에 동시에 다른 테스트가 진행되어도 서로 영향을 주지 않는 장점이 있음.
🎃 @Transactional 때문에 다음 원칙을 아주 편리하게 지킬 수 있음.

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

@Commit

🎃 필요에 따라 DB에 데이터가 들어갔는지 확인해야할 경우 @Commit 어노테이션을 붙여주면 된다. 이 경우, 롤백되지 않고 커밋이 되기 때문에 값이 실제로 DB에 들어간다. 비슷한 기능으로는 @RollBack(value = "false")를 붙여주면 된다.

임베디드 모드 데이터베이스

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

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

임베디드 모드 직접 사용이 가능하지만...

스프링 부트 - 기본 SQL 스크립트를 사용해서 DB를 초기화 하는 기능 제공

JDBC나 JdbcTemplate을 직접 사용해서 테이블을 생성하는 DDL을 호출해도 되지만 불편하다. 스프링부트는 SQL 스크립트를 실행해서 어플리케이션 로딩 시점에 DB를 초기화 하는 기능을 제공해준다. 이 기능을 이용하기 위해서는 다음을 조치하면 된다.

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)
);

src/test/resources/schema.sql을 생성함(위치와 파일 이름이 반드시 맞아야 한다 !!)
이렇게 만들어주면 테스트 실행 전 테이블을 만들어 주고, 테스트가 실행된다.

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

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

  • 앞서 직접 설정했던 메모리 DB용 데이터소스를 주석처리하자.
package hello.itemservice;
@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;
}
*/
}
  • 그리고 테스트에서 데이터베이스에 접근하는 설정 정보도 주석처리하자.

src/test/resources/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

spring.datasource.url , spring.datasource.username 를 사용하지 않도록 # 을 사용해서
주석처리 했다

이렇게 하면 데이터베이스에 접근하는 모든 설정 정보가 사라지게 된다.
이렇게 별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터소스( DataSource )를 만들어서 제공한다. 바로 앞서 우리가 직접 만든 데이터소스와 비슷하다 생각하면 된다.

정리

🚀 테스트를 실행할 때는 반드시 다음 두 가지를 만족해야한다.

  • 테스트는 서로 격리 되어야 한다.
  • 테스트는 반복해서 실행될 수 있어야 한다.

위의 규칙을 지키기 위해 어플리케이션 DB / 테스트 DB 분리를 고려할 수 있다.

🎈 스프링 부트는 DB 설정 정보를 등록하지 않을 경우, 임베디드 모드로 DB를 등록해준다. 이 말은 JVM 메모리를 DB처럼 사용한다는 것이다. 즉, 어플리케이션과 테스트의 DB가 분리!!!

🎈 테스트의 실행 결과가 DB에 남는 경우 다른 테스트에 영향을 줄 수 있고, 자신에게도 영향을 줄 수 있음. 따라서 DB에 테스트 결과를 남기면 안된다.

  • 테스트 한 후, 데이터를 반드시 롤백해야 함.

@BeforeEach / @AfterEach를 이용해 트랜잭션을 시작 / 롤백할 수 있다.

@Transactional을 이용해서 자동으로 롤백할 수 있다!

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글