[Spring Boot] 가끔은 mybatis를 쓰고 싶을 때가 있어 ...

Jinseok Lee·2022년 12월 26일
0

사연

스프링 어플리케이션을 개발하면서 보통은 jpa로 개발을 진행하고 있다.
그러나 어떤 경우에는 native 쿼리를 활용하여 퍼포먼스를 끌어 올리거나 조금 더 DB와 근접한 쿼리를 사용하고 싶을 때가 있었다.

mybatis

간단한 쿼리의 경우 NamedJdbcTemplate을 써도 괜찮을 거 같다. 하지만 쿼리가 복잡해지고 관리해야 하는 쿼리의 양이 많아지는 경우에는 마이바티스가 괜찮은 선택인것 같아서 도입하기로 했다

mybatis 설정

config 파일

@Configuration
@RequiredArgsConstructor
@MapperScan(basePackages = {"com.example.infra.db.*"}, annotationClass = Mapper.class)
public class KpopDbMybatisConfiguration {

    private final KpopDbPropertyBinder kpopDbPropertyBinder;

    @Bean
    public DataSource kpopMybatisDataSource() {
        DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
        dataSourceBuilder.url(kpopDbPropertyBinder.getJdbcUrl());
        dataSourceBuilder.username(kpopDbPropertyBinder.getUsername());
        dataSourceBuilder.password(kpopDbPropertyBinder.getPassword());
        return dataSourceBuilder.build();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(kpopMybatisDataSource());
        sqlSessionFactory.setTypeAliasesPackage("com.example.infra.db.*");
        sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/**/*.xml"));
        org.apache.ibatis.session.Configuration configuration =new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.setConfiguration(configuration);
        return sqlSessionFactory.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

@MapperScan을 사용하여 mapper xml파일의 쿼리와 매핑되는 java interface가 위치하는 위치를 지정해준다.
annotationClass는 반드시 지정해주도록 한다. 보통은 mybatis에서 제공하는 @Mapper class를 지정해주면 될것이다.

첫번째로는 DataSource 의존성이 필요하다.
코드에서 보다시피 DataSource Bean을 먼저 등록했다.

그리고 SqlSessionFactory Bean이 필요하다.
dataSource를 설정하고, typeAlaisesPackage를 문자열로 등록하게끔 되어 있는데 클래스 방식으로 등록할 수 없는것이 아쉽다.

그리고 mapperLocations를 등록해줘야 하는데 이 부분은 classpath에 mybatis mapper파일이 위치한 곳을 명시해주면 된다.

mapperUnderscoreToCamelCase를 true로 해주었는데 예상하기로는 보통 DB에서는 underscore를 많이 쓰는데 이 방식을 camelCase의 dto형식으로 변환이 가능하게 해주는 것으로 보인다. (ex: DB: created_date -> dto: createdDate)

SqlSessionTemplate은 정확히 어떤 역할인지 모르겠지만 설정된 SqlSessionFactory를 참조하여 생성한다.

mapper interface

@Mapper
public interface IpStatisticsMybatisRepository {
	...
    List<IpUseRowDto> findIpUseCountAndRank(String startDate, String endDate, Integer shopNo, Long artistId);
	...
}

보통 나는 jpa는 UserJpaRepository, queryDsl은 UserQueryDslRepository, jdbcTemplate은 UserJdbcRepository와 같은 형식으로 repository의 이름을 정하는데 mybatis는 UserMybatisRepository 형식으로 짓는 편이다.

이 부분은 통계에 관련한 쿼리이기 때문에 위와 같이 이름을 지정하였다.

위치는 위에 config에서 지정한 "com.example.infra.db.*" 패키지 내부 어딘가에 적당한 곳에 지정해주었다.
쿼리에서의 응답형과 (보통 select에서는 List 형식이 될거라고 예상한다) 인자로 받을 파라미터를 지정한다.

mapper xml 파일

mapper xml 파일 작성

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.infra.db.kpop.domain.statistics.ip.repository.IpStatisticsMybatisRepository">
  ...
  <select id="findIpUseCountAndRank" resultType="com.example.infra.db.kpop.domain.statistics.ip.dto.IpUseRowDto">
    select
    	results.ip_name,
    	case
    		when results.ip_low_quality_preview_url != null
    		then results.ip_low_quality_preview_url
    		else results.ip_preview_url
    	end as image_url,
    	results.ip_use_count as ip_use_count
    from (
    	select
    		ip.name as ip_name,
            resource.resource_type as ip_resource_type,
            resource.preview_url as ip_preview_url,
            resource.low_quality_preview_url as ip_low_quality_preview_url,
            count(ip.id) as ip_use_count
    	from dtb_ip ip
          inner join dtb_design_ip_relation designIpRelation
          	on designIpRelation.ip_id = ip.id

          inner join dtb_design design
          	on design.id = designIpRelation.design_id

          inner join dtb_artist artist
          	on artist.id = ip.artist_id

          inner join dtb_member member
          	on design.member_id = member.id

          inner join dtb_resource resource
          	on ip.resource_id = resource.id
    	where
    		design.created_at between DATE_FORMAT(#{startDate},'%Y-%m-%d %H:%i:%s') and DATE_FORMAT(#{endDate},'%Y-%m-%d %H:%i:%s')
    and artist.id = #{artistId}
    <if test="shopNo != null">
      and member.shop_no = #{shopNo}
    </if>
    group by ip.id
    ) results
    order by results.ip_use_count desc
  </select>
...
</mapper>

먼저 mapper에 namespace를 연결하고자 하는 inerface (repository)를 입력해준다.
id는 위에 inerface의 메소드 명과 동일하게 맞추어서 작성해주면 되며 (findIpUseCountAndRank), 내부에 쿼리를 작성해주면 된다.

중간 중간 보이는 #{startDate}이런 방식은 메소드를 통해 전달되는 파라미터를 매핑해주는 mybatis의 표현 방식이다.

resultType의 경우 해당 쿼리의 응답형을 작성해둔 dto에 매핑시키는 정보로 활용된다.

public class IpUseRowDto {
    private String ipName;
    private String imageUrl;
    private Long ipUseCount;
    private Integer rank;
}

쿼리의 실행

@Slf4j
@Service
@RequiredArgsConstructor
public class IpStatisticsService {

    private final IpStatisticsMybatisRepository ipStatisticsMybatisRepository;
  	
  	...
  
	public IpUseExcelFileDto extractStatistics(LocalDateTime startDate, LocalDateTime endDate, Integer shopNo, Long artistId) {
  		...
  		List<IpUseRowDto> ipUseRowDto = ipStatisticsMybatisRepository.findIpUseCountAndRank(startDateStr, endDateStr, shopNo, artistId);
  		...
	}
  
	...

mybatisRepository를 주입받아온 이후 repository에 작성한 메소드의 파라미터를 함께 전달해서 실행하면, 쿼리에 문제가 없고 파라미터가 정확하게 전달이 되었다면 DB에서 검색한 내용을 파싱해서 지정한 dto 형식으로 돌려주는 것을 확인할 수 있을 것이다.

나의 경우에는 기존에 jpa로 버겁게 통계쿼리를 개발해서 excel 파일을 추출하는데에 약 5분 이상 걸리던 작업을 40초 정도로 줄인바 있다.

참고

  • 글에 문제가 있거나 궁금증이 있다면 sonaky47@gmail.com으로 메일 주시길 바란다.

profile
전 위메프, 이직준비중

0개의 댓글