개발자로 후회없는 삶 살기

spring PART.데이터 접근 기술 테스트 본문

[백엔드]/[spring | 학습기록]

spring PART.데이터 접근 기술 테스트

몽이장쥰 2023. 5. 14. 20:11

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com:443

 

본론

- 데이터 접근 기술 테스트

jdbc 탬플릿을 개발하고 테스트한 적은 없습니다. 데이터 접근 기술을 사용할 때 어떻게 테스트를 할 건지 알아보겠습니다. db 연동한 상태에서 테스트 하는 법을 알아봅니다. 데이터 접근 기술은 실제 db에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요합니다.

 

-> 프로퍼티스

테스트 프로필 프로퍼티스에도 데이터 소스 설정 정보를 넣어야합니다. 테스트를 돌릴 땐 테스트의 프로퍼티스를 읽으니 데이터 소스 설정을 하려면 이쪽에도 데이터 소스 설정 정보를 줘야합니다. @SpringBootTest가 있으면 @SpringBootApp을 찾아서 app에 있는 것이 설정에 사용됩니다.

 

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

app도 뜰 때 설정 정보를 줄 수 있습니다. 그래서 import도 하고 프로필에 따를 초기화 데이터 클래스도 등록합니다. 따라서 지금은 V3Config인 이름기반 탬플릿 레포지토리를 사용하여 테스트를 하게 됩니다. 빈도 작성되어 있으니 등록할 수 있는데 그냥 app이 스프링이 시작할 때 실행되니 그 안에 하고자 하는 설정 정보를 넣으면 되는 것입니다.

 

 

실행해보면 updateItem과 save는 성공하고 findItems는 실패합니다. 왜 그런지 보면 저장하는 것은 기본키가 id여서 같은 상품명을 등록해도 계속 성공합니다.

 

test(null, null, item1, item2, item3);

find는 조건이 없으면 3개가 나와야하는데 여러개가 나와서 실패합니다. 이 테스트를 실행할 때는 이전 데이터가 쌓여있으면 실패를 하는 것입니다. DB 테스트는 따라서 초기화 데이터나 이전에 넣은 데이터를 남겨두면 안됩니다. 서버를 띄울 때랑 테스트를 할 때 같은 db를 쓰니깐 테스트 환경은 격리된 환경에서 테스트를 해야하는데 그러지 못한 것입니다. 그래서 테스트 전용으로 db를 분리해야합니다.

 

- db 분리

로컬 앱 서버와 테스트 서버를 분리해야 합니다. 따라서 새로운 testcase db를 만듭니다. 현재 db 2개를 만든 상황입니다. 여기도 테이블을 똑같이 만듭니다.

 

테스트 프로퍼티스에 접속 정보도 바꿔야합니다. 이렇게 되면 로컬은 test라는 이름의 db로 접속하고 test의 경우 testcase로 접속하는 것이고 이제 테스트를 다시 돌려보면 테스트를 성공합니다. DB에 데이터가 없는 상태에서 3개 저장하고 테스트 한 것입니다.

 

> 근데 한 번 더 실행하면 이전에 실행한 데이터가 남아서 실패합니다. 지금 테스트를 반복해서 돌려서 데이터가 계속 축적이 되는 것이 문제입니다. 그래서 테스트가 끝나면 데이터를 초기화를 해줘야합니다. 이전에는 afterEach로 테스트 간의 연관성을 해결했습니다. 

 

-> 테스트 원칙

테스트는 다른 테스트와 격리해야 하고 반복해서 실행할 수 있어야합니다. 물론 테스트가 끝 날 때마다 delete sql해서 해결해도 되지만 delete sql 전에 예외가 터져서 어플이 종료해버려서 delete를 못해서 기존 데이터가 남게 될 수도 있어서 궁극적인 해결이 안됩니다.

 

- 데이터 롤백

이 문제를 커밋과 롤백으로 해결합니다. 바로 트랜잭션을 사용하는 것입니다. 테스트 중간에 실패해서 롤백을 호출하지 못해도 커밋하지 않아서 반영이 안되고 트랜잭션을 활용하여 데이터를 깔끔하게 되돌릴 수 있습니다.

 

-> 예를들어

트랜잭션 시작하고 테스트 save하고 롤백하고 > 트랜잭션 시작하고 테스트 find하고 롤백하고 이런 순서로 진행합니다. 이렇게 하면 데이터가 다 롤백되어 다음 테스트에 영향을 주지 않습니다. afterEach와 beforeEach로 트랜잭션을 적용하면 트랜잭션 시작과 테스트 후 롤백을 할 수 있습니다.

 

- 구현

@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;

@BeforeEach
void beforeEach() {
    status = transactionManager.getTransaction(new DefaultTransactionAttribute());
}

플랫폼 트랜잭션 매니저를 주입받습니다. before에 getTranscation으로 트랜잭션을 시작하고 con 연결하고 동기화 매니저에 커낵션 넣는 것을 합니다. 그러면 모든 테스트 실행전에 트랜잭션을 시작하는 것입니다. 

 

@AfterEach
void afterEach() {
    // 트랜잭션 적용
    transactionManager.rollback(status);
}

after에서 트랜잭션 롤백을 해버립니다. 이러면 각 테스트가 끝나고 나서 무조건 롤백되어서 save할 때 저장하고 롤백됩니다. 굳이 커밋을 할 필요가 없습니다. 하나의 세션 안에서는 트랜잭션을 시작한 사람이 혼자 보기 때문에 테스트할 때는 커밋할 필요가 없습니다.

 

-> 실행

실행해보면 롤백 로그가 남고 롤백을 합니다.

 

테스트 케이스 db를 확인해봐도 데이터가 없습니다. save 하기 전에 트랜잭션이 시작되고 테스트 끝나고 롤백하고 이것을 반복합니다.

 

- @Transactional

before, after 코드를 작성하는게 피곤합니다. 이것을 @트랜잭션을 테스트에서 사용할 수 있습니다. 조금 특별하게 동작합니다. after, before 코드를 지웁니다. class 단위에 @Transactional을 붙입니다. 이것은 이전에 외부에서 실행되는 메서드에 트랜잭션을 시작하고 성공하면 커밋하는 로직이었습니다.

 

-> 실행

원래 이게 성공하면 커밋해야하는 것이 아닌가 싶습니다. 근데 반복 실행해도 db에 데이터가 안쌓이고 잘 성공합니다.

 

- 원리

스프링이 제공하는 @트랜잭션은 성공하면 커밋하고 예외가 터지면 예외를 던지고 롤백했습니다. 근데 이 어노가 테스트에서 사용하면 예외적으로 동작합니다. 스프링이 이것을 테스트에서 사용하면 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 트랜잭션을 자동으로 롤백합니다. 테스트는 각각의 데이터가 격리되는게 정상이고 남아있는 것이 이상한 것입니다. 그래서 스프링이 테스트에 @트랜잭션이 있으면 롤백하도록 제공합니다.

 

-> 그림

어플에서와 동일하게 클래스 단위에 있으면 전체 적용이고 하나면 하나만 적용입니다. 

 

1. 어노테이션이 적용된 메서드를 실행하면 테스트 실행 전에 먼저 트랜잭션을 시작합니다. 
2. 테스트 실행, 성공해도 커밋하지 않습니다.
3. 테스트가 끝나면 롤백 호출

 

이전에 before, after와 동일하게 동작하고 이를 자동화 해줍니다. 커밋을 안 하는 것입니다.

 

※ 실제 로직에도 @Transactional이 있다면

이전처럼 실제 로직에 Transactional이 있는데 테스트에서도 트랜잭션을 사용해서 테스트를 하면 어떻게 될까요? 로직의 트랜잭션이 테스트의 트랜잭션에 참여합니다. 다른 것들은 다 그 트랜잭션을 이어받아서 같이 씁니다. 따라서 같은 트랙잭션을 사용하고 같은 트랜잭션을 사용한다는 것은 같은 커낵션을 쓴다는 얘기입니다.

 

- 정리

테스트 도중 테스트 예외가 터져서 롤백이 안 되지 않을까 걱정하지 않아도 됩니다. 트랜잭션을 사용하니 자동으로 롤백되기 때문입니다. 보통 db 커낵션이 끊어지면 자동 롤백이 됩니다.

 

-> 강제로 커밋하기

@Commit
@Test
void save() {

근데 개발하다보면 save를 하는데 이 경우에는 저장된 데이터를 보고 싶은데 트랜잭션이 적용해서 무조건 사라져서 눈으로 확인이 안됩니다. 눈으로 확인하고 싶으면 @Commit이나 @Rollback(false)를 commit하고 싶은 메서드에만 붙이면 됩니다.

그러면 롤백되지 않고 커밋되어 잘 저장이 되고 데이터가 남습니다.

 

- 테스트 임베디드 모드 DB

테스트 케이스를 위해서 별도의 db를 설치하고 운영하는게 번잡한 작업입니다. 단순히 테스트를 검증할 용도로만 사용하기 때문에 테스트가 끝나면 db의 데이터를 모두 삭제해도 됩니다. 또한 테스트 후에 db 자체를 제거해도 됩니다. 따라서 임베디드 모드라는 것이 있습니다.

 

> h2는 jvm 메모리에 포함하여 db를 함께 띄울 수 있습니다. db를 어플리 케이션에 내장해서 함께 실행한다고 해서 임베디드입니다. 자바 메모리를 사용하는 것이기에 어플이 종료되면 h2 db도 함께 종료됩니다.

 

1. 임베디드 모드 직접 사용

@Bean
@Profile("test")
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("org.h2.Driver");
    dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
    dataSource.setUsername("sa");
    dataSource.setPassword("");
    log.info("메모리 db 초기화");
    return dataSource;
}

app에서 테스트 프로필로 필요한 데이터 소스를 직접 빈으로 등록합니다. 드라이버 매니저를 사용하는 데이터 소스 구현체를 사용하고 classname으로 h2 db 드라이버를 지정합니다. url을 기존에 실제 h2 db에 접속할 때 하듯이 하는 것이 아니라 mem(memory)을 씁니다. username과 pwd는 동일하게 하고 이러면 jvm 내에 db를 만들고 거기에 데이터를 쌓습니다. 테스트할 때만 내장 db를 쓸 것이기에 프로필을 test로 등록하는 것입니다. 이렇게 직접 등록하면 스프링이 자동 등록하는 데이터 소스가 우선순위가 낮아서 테스트를 할 때는 직접 등록한 데이터 소스가 사용이 됩니다.

 

-> 실행

 

테스트 db를 쓰려면 실제 h2 db를 끄고 mem db를 접속해야합니다. 실행해보면 로그는 남습니다. 

 

그런데 table이 없다는 에러가 납니다. 접속은 된건데 테스트 db를 써서 테이블을 만들지 않은 것입니다. 스프링 부트가 기본 SQL 스크립트를 사용해서 db를 초기화하는 기능을 제공합니다. 

 

> 메모리 db는 어플이 종료될 때 함께 사라져서 어플 실행 시점에 항상 테이블도 만들어줘야합니다. 부트가 SQL 스크립트를 실행해서 어플 로딩 시점에 db를 초기화하는 기능을 제공합니다.

 

-> sql 스크립트 사용

/test/resources 밑에 schema.sql을 만듭니다.(반드시 이 이름입니다.)

 

/test/resources 밑에 schema.sql을 만듭니다.(반드시 이 이름입니다.)

 

실행해보면 sql을 실행합니다. item 테이블을 만들어서 메모리 db에 넣어주는 것입니다. 이런 로그가 남는 것이 jdbc로그를 프로퍼티스에 써서 그렇습니다. 이렇게 테스트할 때 간편하게 db를 사용할 수 있습니다. 그런데 이런 임베디드 모드로 개발하는 개발자가 진짜 많을 텐데 이렇게 데이터 소스를 직접 빈 등록하는게 귀찮습니다. 스프링이 이것을 또 해결해줍니다.

 

2. 스프링 부트 임베디드 모드

스프링 부트는 개발자에게 정말 많은 편리함을 제공하는데 db에 별다른 설정이 없으면 임베디드 db를 사용합니다. 먼저 app에 데이터소스 빈 등록을 안해도 되고 test 프로퍼티스에 db 설정 정보도 삭제합니다. 이러면 테스트를 할 때 데이터 소스를 어디에 접근해야할지 애매합니다. 

> 이렇게 별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터 소스를 만들어서 데이터 소스를 제공하고 이전에 직접 빈 등록한 것과 같은 것이 기본 값입니다. 따라서 실행하면 잘 성공합니다. 매번 내장 db가 생성되고 매번 schema.sql이 실행되어서 테스트에 사용됩니다. 

 

+ 결론만 생각하면 db를 활용한 테스트를 할 거면 테스트 코드에 @Transactional만 붙이고 설정 정보 없고 데이터 소스도 test 프로필에 없어도 내장 db로 트랜잭션 동작하면서 db를 활용한 테스트가 되는 것입니다. 아무것도 하지 않고 그냥 @Transactional만 테스크 코드 class에 붙이면 반복적인 db 활용 테스트를 할 수 있습니다.

'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글

spring PART.JPA  (0) 2023.05.16
spring PART.MyBatis  (0) 2023.05.15
spring PART.스프링 JdbcTemplate 적용  (0) 2023.05.14
spring PART.데이터 접근 기술  (0) 2023.05.12
spring PART.중간점검 3  (0) 2023.05.09
Comments