티스토리 뷰
개요
Amazon Athena
는 아마존이 제공하는 서버리스 대화식 데이터 쿼리 서비스 상품으로, 최근 대두되는 주제인 데이터 레이크에 있어 아마존의 주력 서비스라고 할 수 있다. 이번 글에서는 Spring Boot 프로젝트에서 MyBatis를 이용하여 Athena에 자유롭게 동적 쿼리를 작성하는 방법을 소개하고자 한다.
Athena JDBC 연동 과정에서 발견된 문제점
- Athena JDBC 드라이버는 Maven 중앙 저장소에서 제공되지 않아 별도 다운로드하여 관리해야 한다. 버전 관리에서 있어서 상당한 불편을 초래한다.
- 더군다나 Athena JDBC 드라이버는 한 프로젝트 내에서 Amazon SDK 라이브러리와 병행 사용시 충돌이 발생하여 리패키징 작업을 해야 한다. (리패키징 방법은 아래 설명하였다.)
- Athena JDBC 드라이버는 Prepared Statement 기능을 제공하지 않는다. (최신 버전의 Presto는 본 기능을 제공하는데, Athena는 구형의 Presto 엔진 기반으로 제공하지 않는다.) 이 것으로 인해 Spring Data JPA와 QueryDSL을 이용한 높은 생산성을 얻을 수 없게 되었다. (QueryDSL의 강력함은 본 블로그의 이 글을 참고한다.)
- 대안으로는 완전한 ORM은 아니지만 비슷한 수준을 제공하는 MyBatis를 이용하는 것이 가능하다. 개인적으로는 복잡한 쿼리를 다룸에 있어 최선이라는 생각이 든다. 이번 글에서 설명할 주제이기도 하다.
Athena JDBC 드라이버 다운로드
- Athena는 Maven에서 JDBC 드라이버를 제공하지 않아 별도로 다운로드하여 라이브러리에 추가해야 한다. 여기에서
AthenaJDBC42.jar
파일을 다운로드한다. - 만약, 프로젝트에서 이미 Amazon SDK를 사용 중이라면 다운로드 받은 드라이버와 버전 충돌이 발생한다. 해결책은 아래 작업 후 AthenaJDBC42.jar 파일을 재압축하면 된다.
1. AthenaJDBC42.jar 파일을 압축 해제한다.
2. /com/amazonaws 폴더를 삭제한다
3. 기존 AthenaJDBC42.jar을 삭제하고, 동일한 파일명으로 재압축한다.
- 재압축한 파일은 프로젝트의 /libs 폴더를 생성하여 복사한다.
build.gradle.kts
- 프로젝트의
/build.gradle.kts
파일에 아래 내용을 추가한다.
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation(files("libs/AthenaJDBC42.jar"))
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4")
implementation("org.mybatis:mybatis-typehandlers-jsr310:1.0.2")
implementation("com.github.pagehelper:pagehelper-spring-boot-starter:1.3.0")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2020.0.1")
}
}
pagehelper-spring-boot-starter
아티팩트는 MyBatis에서 편리한 페이징을 가능하게 해준다. 자세한 사용 예는 아래 설명한다.
application.yml
- 프로젝트의
/application.yml
파일에 아래 내용을 추가한다.
cloud:
aws:
credentials:
instance-profile: false
access-key: {access-key}
secret-key: {secret-key}
region:
auto: false
static: {region}
stack:
auto: false
spring:
datasource:
url: jdbc:awsathena://User={access-key};Password={secret-key};S3OutputLocation=s3://{};AwsRegion={region};
driver-class-name: com.simba.athena.jdbc.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
open-in-view: false
properties:
hibernate:
format_sql: true
generate-ddl: false
mybatis:
mapper-locations: mapper/*.xml
pagehelper:
helper-dialect: com.jsonobject.example.AthenaDialect
reasonable: true
supportMethodsArguments: true
params: count=countSql
페이지네이션 구현체 작성
- MyBatis는 별도의 페이지네이션 기능을 제공하지 않아 사용자가 직접 구현해야 하는데, JPA에 익숙한 사람이라면 여간 귀찮은 일이 아닐 수 없다. 다행히도
PageHelper
라이브러리가 그 역할을 대신해준다. - 페이지네이션을 위한 쿼리문을 작성하는 방법은 데이터베이스마다 천차만별이다. PageHelper는 미리 다양한 데이터베이스에 맞는 Dialect 구현체를 제공하는데, 안타깝게도 Athena에 대해서는 제공하지 않아 직접 구현해야 했다. Athena의 페이지네이션 쿼리문의 기본 골격은 아래와 같다. 이 것을 기반으로 구현체를 작성할 것이다.
select * from (
select row_number() over() as PAGEHELPER_ROW_ID, *
from (
{SQL}
)
)
where PAGEHELPER_ROW_ID between {START_ROW} and {END_ROW}
- 프로젝트의
/src/main/java
폴더에AthenaDialect.java
파일을 생성 후, 아래와 같이 구현체를 작성한다.
package com.jsonobject.example;
import com.github.pagehelper.Page;
import com.github.pagehelper.dialect.AbstractHelperDialect;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import java.util.Map;
public class AthenaDialect extends AbstractHelperDialect {
@Override
public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
return paramMap;
}
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
sqlBuilder.append("select * from ( ");
sqlBuilder.append(" select row_number() over() as PAGEHELPER_ROW_ID, * FROM ( \n");
sqlBuilder.append(sql);
sqlBuilder.append("\n )) where PAGEHELPER_ROW_ID between ").append(page.getStartRow()).append(" and ").append(page.getEndRow());
return sqlBuilder.toString();
}
}
- Athena의 JDBC 드라이버는 Prepared Statement 기능을 제공하지 않아 위와 같이 쿼리문에 직접 파라메터를 포함하는 형태로 작성하였다.
쿼리 Mapper 작성
- MyBatis는 실제 실행될 쿼리문을
Mapper
에 선언하는데, 다이나믹 쿼리를 자유롭게 구사하기에는 아래와 같이 XML 형식의 Mapper 파일을 작성하는 것이 가장 효율적이다. 프로젝트 내 /src/resources/mapper/fooMapper.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.jsonobject.example.FooRepository">
<resultMap type="com.jsonobject.example.Foo" id="fooMap">
<result property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<select id="selectFooList" parameterType="com.jsonobject.example.Foo" resultMap="fooMap">
select
id, user_id, created_at, updated_at
from
"athena-database".foo
<where>
<if test="userId != null">
and user_id = '${userId}'
</if>
</where>
order by created_at desc
</select>
</mapper>
- 파라메터 맵핑시
$
기호를 사용하여 전달된 파라메터를 쿼리문의 일부로 포함시켜 버린다. 이 기능 덕분에 Prepared Statement 기능이 없는 Athena JDBC 드라이버에 질의가 가능하다. - 앞서 PageHelper의 도움으로 Mapper에 페이지네이션 관련 내용을 작성할 필요가 없어 상당히 편리해졌다.
- Mapper의 보다 자세한 사용 예는 본 블로그의 이 글을 참고한다.
@Repository 작성
- 이제 리파지터리 역할을 수행할 클래스를 작성할 차례이다.
package com.jsonobject.example.repository
import com.jsonobject.example.Foo
import com.github.pagehelper.PageHelper
import org.apache.ibatis.session.SqlSession
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Repository
@Repository
class FooRepository(
@Autowired
@Qualifier("sqlSessionTemplate")
val sqlSession: SqlSession
) {
fun fetchByUserIdWithPage(userId: String, pageable: Pageable): Page<Foo> {
val page: com.github.pagehelper.Page<Foo> =
PageHelper.startPage<Foo>(pageable.pageNumber, pageable.pageSize).doSelectPage {
val list: List<Foo> = sqlSession.selectList(
"com.jsonobject.example.FooRepository.selectFooList",
Foo(userId = userId)
)
}
return PageImpl(page.result, pageable, page.total)
}
}
- PageHelper를 이용하여 앞서 작성한 쿼리문의 실행 결과를
com.github.pagehelper.Page<T>
오브젝트 획득 후 다시org.springframework.data.domain.Page<T>
오브젝트로 변환한 예이다. 이를 통해 Spring Boot와의 호환성을 높일 수 있다.
페이지네이션 조회 결과
- 아래는 org.springframework.data.domain.Page
오브젝트로 변환한 후의 결과를 JSON으로 출력한 예이다. 최소의 작업으로 페이지네이션에 필요한 정보가 담긴 것을 확인할 수 있다.
{
"content": [
{
"id": "902badf9d148ae2b623d41cf",
"userId": "902bad16348eb6167aa020a2",
"createdAt": "2021-02-16T11:35:21.956",
"updatedAt": "2021-02-16T11:35:29.914"
},
...
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageNumber": 0,
"pageSize": 10,
"unpaged": false,
"paged": true
},
"last": false,
"totalElements": 1837,
"totalPages": 184,
"size": 10,
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"first": true,
"numberOfElements": 10,
"empty": false
}
참고 글
댓글
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- kotlin
- graylog
- Kendo UI Web Grid
- java
- DynamoDB
- Spring Boot
- bootstrap
- JHipster
- 평속
- 자전거
- Kendo UI
- 태그를 입력해 주세요.
- maven
- node.js
- Spring MVC 3
- 로드 바이크
- JavaScript
- Tomcat
- 알뜰폰
- jstl
- Eclipse
- CentOS
- 로드바이크
- Docker
- chrome
- 구동계
- jpa
- MySQL
- jsp
- spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함