[Spring]Mapper 테스트 - @MybatisTest

Inung_92·2024년 12월 27일
1

Spring

목록 보기
17/17

들어가기 앞서

목적

Springboot + Mybatis를 사용하는 프로젝트에서 @Mapper에 대한 단위 테스트를 수행하는 방법과 주의사항에 대해서 알아본다.

버전 정보

  • SpringBoot : 3.1.6
  • Java : openjdk-17
  • Mybatis : 3.0.2
  • Mybatis-test : 3.0.2
  • DB : postgresql(write 전용), trino(read 전용)

DataSource 구성

내가 진행하는 프로젝트는 읽기 전용과 쓰기 전용 DB가 구분되어 있으며, 이런 이유로 DataSource를 두 개로 나누어 설정하여 개발을 진행 중에 있다.

이렇게 개발을 진행하지 않더라도 지금부터 알아보는 @Mapper에 대한 테스트는 적용하는데 문제가 없을 것이다.

DataSource 설정 참고

구분readwrite
클래스ReaderDataSourceConfig.classWriterDataSourceConfig.class
드라이버Trino DriverPostgreSQL Driver
트랜잭션 지원여부XO

테스트

사전준비

테스트를 수행하기 앞서 DataSource의 연결 정보를 설정하는 작업이 필요하다. 나는 복수 개의 DataSource를 사용했기 때문에 여기서 설정 방법을 확인하면 된다.

@Mapper

@Mapper
public interface SomeReaderMapper {
    List<SomeResultDTO> selectSomeResult(String code);
}

먼저 Mybatis@Mapper를 정의해주자. 해당 매퍼의 경로는 아래 설정 중 편한 방식으로 지정한다.

application.yaml

mybatis:
	...생략
	mapper-locations: classpath:/sqlmap/mapper/*.xml

Java Config

// ReaderDataSourceConfig.java 일부
@Bean(name = "readerSqlSessionFactory")
public SqlSessionFactory readerSqlSessionFactory(@Qualifier("readerDataSource") DataSource dataSource)
            throws Exception {
	SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
	sqlSessionFactoryBean.setDataSource(dataSource);
	sqlSessionFactoryBean.setTypeAliasesPackage("...생략"); // DTO
	sqlSessionFactoryBean.setTypeHandlersPackage("...생략"); // 타입 핸들러
	sqlSessionFactoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/sqlmap/mapper/reader/**/*.xml"));

	return sqlSessionFactoryBean.getObject();
}

MapperTest

의존성 설정

build.gradle 파일에 아래와 같이 mybatis의 테스트 의존성을 추가하자.

...생략
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2'

테스트 코드 작성

정의한 매퍼를 테스트하기 위한 클래스를 생성하고, 정상 호출과 의도된 불량 호출을 정의한다.

@MybatisTest
@Import({
        DataSourceBeanConfig.class, // DataSource 설정
        ReaderDataSourceConfig.class, // ReaderDataSource 설정
        JasyptConfigAES.class // DB 접속 정보 복호화
})
// 선택 사항
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class SomeReaderMapperTest {
    @Autowired
    private SomeReaderMapper someReaderMapper;

    @Test
    @DisplayName("결과 목록 조회 정상 테스트")
    void someResult_returnsResults() {
        String code = "valid code";

        List<SomeResultDTO> results = someReaderMapper.selectSomeResult(
                code);

        Assertions.assertFalse(results.isEmpty());
    }

    @Test
    @DisplayName("결과 목록 조회 비정상 테스트")
    void someResult_returnsEmptyList() {
        String code = "invalid code";

        List<SomeResultDTO> results = someReaderMapper.selectSomeResult(
                code);

        Assertions.assertTrue(results.isEmpty());
    }
}

위 코드대로 실행하면 아래와 같은 결과가 나온다.

코드 설명

테스트 코드에서 중요하게 봐야하는 부분은 클래스명 상단에 위치한 어노테이션들이다.

@MybatisTest
Mybatis의 테스트 의존성을 추가하면 사용할 수 있는 어노테이션으로 mybatis와 관련된 테스트에 필요한 어노테이션을 이용해 구성 요소를 자동으로 설정해준다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(MybatisTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(MybatisTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureMybatis
@AutoConfigureTestDatabase
@ImportAutoConfiguration
public @interface MybatisTest {

내부에 선언된 어노테이션은 위와 같다. 기본적으로 @AutoConfiguration을 오버라이드하여 false로 설정되어 있고, @Transactional어노테이션으로 커밋 또는 롤백을 자동 지원한다. 이 외에도 캐시, 테스트 데이터베이스 연결 등에 대한 정보를 제공하고, 구성한다.

여기서 중요한 부분은 @Transactional이다. 스프링을 이용하다보면 다양한 DB Driver를 연결하는 경우들이 발생한다. 대부분의 DB 플랫폼은 트랜잭션을 지원하지만 필자가 사용하는 Trino의 경우에는 트랜잭션 지원을 하지 않는다.

이러한 이유로 @MybatisTest 내부에 정의된 @Trasactional이 제대로 동작하지 않으면서 다음과 같은 오류를 뱉어낸다.

java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test:

그럼 이런 상황을 어떻게 해결할까?

@Transactinal(propagation = Propagation.NOT_SUPPORTED)
바로 트랜잭션의 전파 속성에서 트랜잭션 지원을 수동으로 꺼주면된다. 스프링에서 지원하는 전파 속성 중 NOT_SUPPORTED트랜잭선을 완전히 비활성화한다.

그렇기 때문에 @MybatisTest 내부에 정의된 트랜잭션 또한 동작하지 않게되어 정상적으로 수행이 가능한 것이다.

다만, 주의해야할 사항은 트랜잭션이 지원하지 않는 DB Driver일 경우에 NOT_SUPPORTED를 사용해야한다.

여기까지 해결이 됐다면 설정 정보에 대해서는 어떻게 가져오는지 알아보자.

@Import
@Import를 통해 명시적으로 설정 관련 클래스를 추가하면 테스트 환경에서도 실제 데이터베이스 연결 정보를 토대로 구성할 수 있다. 위 코드에서 사용한 클래스들에 대한 정보는 다음과 같다.

  • DataSourceBeanConfig : 데이터 소스 생성 및 설정
  • ReaderDataSourceConfig : 읽기 전용 데이터 소스에 대한 Mapper, DTO 등 연결 정보
  • JasyptConfigAES : 암호화된 데이터베이스 연결 정보를 복호화하는 설정 클래스

대부분이 프로젝트와 연결된 데이터베이스 정보로 테스트를 수행하기 때문에 @Import를 사용하여 명시적으로 설정 정보를 넘겨주는 것을 추천한다.

만약, @Import로 정보를 전달하지 않을 경우 다음과 같은 예외가 발생할 수 있다.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sqlSessionFactory' defined in class path resource

정리

어쩌면 간단할지 모르는 @Mapper에 대한 테스트가 나에게는 꽤나 오랜 시간을 소모하게 만들었다. 이렇게 무언가를 해결하면서 매번 느끼는 점은 내가 사용하는 메서드, 어노테이션의 내부 동작을 확인하는 것이 가장 빠르고 명확한 해결 방법이 될 수 있다는 점을 인식한다.

AI를 이용하여 해결하는 것도 빠를 수 있지만 버전 정보, 환경 구성 등에 따라 제대로 유추하지 못하는 답변을 주는 경우도 있기 때문에 최근에는 공식 문서, 내부 코드 구성 등을 확인하며 에러를 해결하려고 노력하고 있다.

마지막으로 테스트에서 사용할 수 있는 트랜잭션 전파 속성에 대해 다시 한번 정리하고 마무리하겠다.

전파속성설명
REQUIRED(기본)현재 트랜잭션이 있으면 그대로 사용, 없으면 생성
REQUIRED_NEW항상 새로운 트랜잭션 생성, 기존은 일시 정지
NESTED중첩된 트랜잭션 생성, 내부적으로 중첩될 경우가 예상되면 사용
MANDATORY반드시 기존 트랜잭션이 존재해야하고, 없을 경우 예외 발생
SUPPORTS트랜잭선이 있으면 트랜잭션 내에서 실행, 없으면 트랜잭션 없이 실행
NOT_SUPPORTED트랜잭션 없이 실행하고, 기존에 있는 트랜잭션은 일시 정지
NEVER이미 트랜잭션이 존재하면 예외 발생, 트랜잭선 없이 실행
profile
서핑하는 개발자🏄🏽

0개의 댓글