동적 Native SQL 생성 어떻게 할까 - 순수 Java 코드로 생성하기 프로그래밍

앞서 Freemarker Dynamic QL Builder에 이어, 순수 Java 코드로 동적 Native SQL/JPQL/HQL을 생성하는 방법에 대한 고민의 결과를 이제서야 공유한다.

나는 어쩔 수 없이 동적 NativeSQL을 작성해야하는 상황에서라도 MyBatis/iBatis나 Freemarker 같은 외부 템플릿을 사용하는 것을 좋아하지 않는 편이다.

일단 로직이 없는 정적 SQL의 경우 템플릿을 사용할 이유가 없고, 로직이 존재하는 SQL은 템플릿에서 처리 할 수 없는 로직이 분명히 존재하기 때문에 템플릿과 Java 코드간의 로직 분할이 발생하게 되고 이로인해 로직을 수정해야 할 때 둘 중 한 곳을 간과하여 버그를 만들기 쉬워진다. 또한 Code Coverage 측정이 안 되어 코드에 대한 확신감도 떨어진다.

MyBatis를 사용한다 해도 가급적 SQL Builder 클래스를 통해 Java 기반으로 쿼리를 생성하라고 권하고 싶다(물론 이 경우 DBA와의 협업이 어려워지는데, 솔직히 코드 작성하면서 DBA에게 MyBatis XML 얼마나 보여주는지 생각해보면 거의 없을 것이다).

다음과 같은 순서로 살펴본다.

  1. java.util.StringBuilder를 사용하는 기본적인 방법을 알아보고,
  2. StringBuilder 기반으로 string-builder-ql-params를 통해 좀 더 쉽게 하는 방법
  3. 아예 StringBuilder를 개선한 UnderscoreStringBuilder라는 것을 통해 처리하는 방법


3번은 사족같은 것이고, 2번까지만 봐도 도움이 많이 될 것 같다.

1. StringBuilder로 동적 SQL만들기


지난 글에서 말했듯이 동적 SQL 생성의 핵심은 동적으로 쿼리 문자열을 만드는데 있는 것이 아니라 그렇게 만들어진 쿼리에 파라미터를 바인딩(binding)하는 것에 있다.

동적 문자열 생성은 StringBuilder 만으로도 충분하다. 여기에 보태어 순서대로 쿼리 파라미터 객체를 저장할 List<Object> params 객체가 있으면 된다. 아래와 같은 형태가 될 것이다.

먼저 아래와 같은 데이터 객체들이 존재한다고 하자.
User user = new User();
// User.COLUMNS
public static final String[] COLUMNS = new String[]{"user_id", "name", "email", "birthday", "mobile_phone", "home_phone", "address", "zip_code"};

user.setUserId(10001L);
user.setName("UnderscoreQlParams");
user.setBirthday(new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/11"));
user.setEmail("someone@email.com");

List<String> zipCodes  = Arrays.asList("12345", "56789", "58391");

이제 이를 가지고 쿼리를 생성하면,
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.apache.commons.lang3.StringUtils.join;

StringBuilder sb = new StringBuilder();
List<Object> params = new ArrayList<Object>();
sb
  .append("SELECT ")
  .append(join(User.COLUMNS, ", "))
  .append("\n")
  .append("FROM users as u\n");

sb.append("WHERE 1 = 1"); // BUG!! no spaces.
if (user.getUserId() != null) {
  sb.append("AND user_id = ? \n");
  params.add(user.getUserId());
}
if (isNotEmpty(user.getName())) {
  sb.append("AND name = ? \n");
  params.add(user.getName());
}
if (user.getBirthday() != null) {
  sb.append("AND birthday = ? \n");
  params.add(user.getBirthday());
}
// IN Parameter 생성
if (CollectionUtils.isNotEmpty(zipCodes)) {
  List<String> inParams = new ArrayList<String>(zipCodes.size());
  for (int i = 0; i < zipCodes.size(); i++) {
    inParams.add("?");
  }

  sb.append(String.format("AND zip_code IN (%s)", StringUtils.join(inPar",")));
  params.addAll(zipCodes);

}

sb.append("LIMIT 10");


List 하나만 추가해도 그런대로 괜찮은 동적 SQL 생성이 가능해진다. sb.toString()으로 동적으로 생성된 SQL문자열을 가져다가 params에 저장된 각 파라미터 객체를 PreparedStatement.setObject(index, value)(index는 1부터시작)로 저장만 해주면된다. 지금까지 문자열을 더해가며(+) 작업했다면 위 방식으로만 해도 어느정도 괜찮아 진다.

하지만 위에서 굵은 글씨체로 된 부분을 보면 뭔가 문제가 있음을 알 수 있다.

  1. Java Code 사이에서 SQL의 가독성이 떨어지다보니 공백 추가를 잠시 잊을 경우 곧바로 잘못된 SQL을 생성하게 된다. 위 쿼리의 경우 조건에 따라 WHERE 1 = 1AND user_id = ? 이렇게 공백없이 붙어버린 잘못된 쿼리가 생성된다.
  2. WHERE 조건에 대해 언제 AND, OR가 올지 몰라 1 = 1로 방어해줘야 한다. 하지만 대부분의 DB는 최적화가 잘 돼서 이로인한 성능저하는 없다고한다. 따라서 이 문제는 사실상 무시해도 된다.
  3. IN조건의 파라미터를 생성하는 것이 굉장히 복잡하다.

이제 여기서 첫번째와 두번째 문제는 마지막에 알아보고, 세번째 문제를 해결해보자.
그리고 IN 조건절의 경우 Java 8에서 좀 더 간결하게 만들 수도 있는데, 그에 대해서는 논외로한다.

2. StringBuilder 기반으로 string-builder-ql-params를 통해 좀 더 쉽게 하는 방법


위 코드의 List<Object> params에 파라미터를 추가하고 더불어 IN 조건절의 문자열과 파라미터 추가를 좀 더 쉽게 할 수 있는 도우미 클래스를 만들면 쉽게 해결 가능하다. string-builder-ql-params라는 프로젝트에 만들어서 Maven Repository에 올려두었는데, 이 프로젝트 전체를 사용할 필요까진 없고 그냥 DynamicQlParams.java클래스 한개만 복사해 자기 프로젝트에 넣고 사용해도 된다.
DynamicQlParams는 그 안에 파라미터 List객체와 param(Object param), inParams(Iterable|Object[]), bindParameters(PreparedStatement),getParameters()등이 있다.

이 클래스와 StringBuilder를 조합하면 다음과 같이 된다.

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import static java.lang.String.format;

StringBuilder builder = new StringBuilder();
DynamicQlParams dqp = new DynamicQlParams();

builder
  .append("SELECT ")
  .append(StringUtils.join(User.COLUMNS, ", ")).append("\n")
  .append("FROM users as u\n")
  .append("WHERE 1 = 1\n");

if (user.getUserId() != null) {
  builder.append(format("AND user_id = %s %n", dqp.param(user.getUserId())));
}
if (StringUtils.isNotEmpty(user.getName())) {
  builder.append(format("AND name = %s %n", dqp.param(user.getName())));
}
if (user.getBirthday() != null) {
  builder.append(format("AND birthday = %s %n", dqp.param(user.getBirthday())));
}
if (CollectionUtils.isNotEmpty(zipCodes)) {
  builder.append(format("AND zip_code in (%s) %n",dqp.inParams(zipCodes)));
}

builder.append("LIMIT ").append(dqp.param(10));

PreparedStatement preparedStatement = conn.prepareStatement(builder.toString());
dqp.bindParameters(preparedStatement); // bind parameters to preparedStatement


DynamicQlParamsString.format을 조합했더니 좀 더 가독성 좋고 편리하게 동적 SQL을 생성하게 되었다.
이것 말고도 DynamicNamedQlParams도 만들었다. Spring JDBCTempalte과 JPQL, HQL은 Named Parameter를 지원하기 때문이 이를 사용할 수도 있다.
하지만 Named Parameter는 그 자체가 정적 SQL에서 편리한 파라미터 바인딩을 위한 것이지 동적 SQL에서는 불필요한 기능으로 보인다.

여기서도 사실 구문 사이사이의 공백을 하나라도 잊어버렸을 때의 잘못된 SQL을 생성하는 문제는 해결이 안된 상태이다. 그리고 가독성도 조금만 더 높였으면 좋겠다.

3. StringBuilder를 개선한 UnderscoreStringBuilder를 사용해보기


사실 내가 보기엔 나처럼 정말 어쩌다가 한 두번 동적 NativeSQL을 생성하는 사람은 2번째 방법만 사용해도 괜찮아 보인다. 그래도 학습삼아 좀 오버를 해보았다. 아예 StringBuilder를 대체하는 UnderscoreStringBuilder라는 것을 만들고 거기에 동적 Native SQL생성시 StringBuilder가 가진 단점들들 커버해 줄 수 있는 기능을 넣은 것이다.
아래와 같이 의존성을 추가하고 살펴보도록 하자.
compile '"kr.pe.kwonnam.underscorebuilder:underscore-ql-params:0.1"


이 프로젝트는 딱 두 개의 핵심 클래스로 이뤄진다. StringBuilder를 대체하는 UnderscoreStringBuilder와, 2번에서 DynamicQlParams의 역할을 하는 UnderscoreQlParams 클래스이다.

StringBuilder.append() 역할을 하는 UnderscoreStringBuilder의 핵심 메소드는 __ 그 중에서도 아래 메소드 이다.
UnderscoreStringBuilder.__(boolean appendable, A appendee,
   UnderscoreTransformer<A> transformer, UnderscoreTransformer<? super CharSequence>... extraTransformers)

왜 이름이 UnderscoreStringBuilder인지 알 것 같다. append()보다는 가독성이 좋을 것 같아서 이렇게 했는데, 실제로 그런지는 잘 모르겠다.

이 메소드에서 appendable,transformer,extraTransformers는 모두 생략가능하다.
  • appendable : appendee를 추가할지 여부
  • appendee : 문자열에 추가할 객체. 내부적으로 StringBuilder.append() 호출함.
  • transformer,extraTransformers : appendee를 추가하기 전에 변환 작업을 한다. 지정된 transformer의 순서대로 연달아 변환한다.


UnderscoreQlParams는 여기서 동적 쿼리 생성으로 변환하는 transformer 객체를 생성하고 상태를 저장하는 역할을 한다. 일단 UnderscoreQlParams없이 순수 문자열 기반으로 쿼리를 생성하면 다음과 같은 형태가 된다.
import static kr.pe.kwonnam.underscore.stringbuilder.UnderscoreStringBuilderTransformers.*;
import static kr.pe.kwonnam.underscore.stringbuilder.transformers.trim.TrimOpts.trimOpts;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

UnderscoreStringBuilder = usb = new UnderscoreStringBuilder();
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
usb
    .__("SELECT ").suffixNewLine()
    .__(", ", join(User.COLUMNS))
    .__("FROM users as u")
    .sub(user != null, new UnderscoreSubBuild() {
        @Override
        public void subbuild(UnderscoreStringBuilder underscoreSubBuilder) {
            underscoreSubBuilder.prefix("\n    ")
                .__(user.getUserId() != null, "AND user_id = %d", format(user.getUserId()))
                .__(isNotEmpty(user.getName()), "AND name = '%s'", format(user.getName()))
                .__(user.getBirthday() != null, "AND birthday = '%s'", format(sdf.format(user.getBirthday())))
                .__(CollectionUtils.isNotEmpty(zipCodes), ", ", join(zipCodes), wrap("AND zip_code in (", ")"));

        }
    }, trim(trimOpts().prefix("WHERE ").prefixOverrides("AND ", "OR ")))
    .__("LIMIT 10");


UnderscoreStringBuilderTransformers는 각종 UnderscoreTransformer 객체를 쉽게 생성해주는 static method가 있는 클래스이다. 미리 준비한 transformer들은 format, dateFormat, join, wrap, multiply, trim등이다.
위 코드는 파라미터 바인딩이 필요없는 일반 SQL 문자열을 생성한 것이고, 진짜 목표인 ? 혹은 ?1,?2,... 기반의 PreparedStatement 지원 동적 SQL을 생성해보면 다음과 같은 형태가 된다.
import static kr.pe.kwonnam.underscore.stringbuilder.UnderscoreStringBuilderTransformers.join;
import static kr.pe.kwonnam.underscore.stringbuilder.UnderscoreStringBuilderTransformers.trim;
import static kr.pe.kwonnam.underscore.stringbuilder.transformers.trim.TrimOpts.trimOpts;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

UnderscoreStringBuilder usb = new UnderscoreStringBuilder();
UnderscoreQlParams qlParams = new UnderscoreQlParams();
usb
    .__("SELECT ").suffixNewLine()
    .__(", ", join(User.COLUMNS))
    .__("FROM users as u")
    .sub(user != null, new UnderscoreSubBuild() { // user != null일 때만 아래 블럭이 실행되고 문자열과 파라미터 추가됨.
        @Override
        public void subbuild(UnderscoreStringBuilder underscoreSubBuilder) {
            underscoreSubBuilder.prefix("\n   ")
                .__(user.getUserId() != null, "AND user_id = %s", qlParams.params(user.getUserId()))
                .__(isNotEmpty(user.getName()), "AND name = %s", qlParams.params(user.getName()))
                .__(user.getBirthday() != null, "AND birthday = %s", qlParams.params(user.getBirthday()))
                .__(CollectionUtils.isNotEmpty(zipCodes), "AND zip_code in (%s)", qlParams.inParams(zipCodes));
        }
    }, trim(trimOpts().prefix("WHERE ").prefixOverrides("AND ", "OR ")))
    .__("LIMIT %s", qlParams.params(10));

log.info("UnderscoreStringBuilder with UnderscoreQlParams : {}", usb.toString()); // 생성된 SQL
log.info("Query Parameters : {}", qlParams.getQueryParameters()); // 바인딩할 Parameters


params, inParams는 내부적으로 String.format을 호출한다. 따라서 앞에 추가할 문자열도 포맷팅 문자열로 만들어줘야 한다. 간단하다. 파라미터 바인딩 물음표가 들어갈 부분을 %s로 해주기만 하면 된다.

UnderscoreStringBuilder.prefix()|suffix()prefixOff()|suffixOff()가 호출될 때까지 모든 문자열 덧붙이기 할 때마다 지정된 문자열을 앞/뒤에 붙여준다. 이를 통해 공백을 까먹어서 생기는 문제를 해소해준다.
그리고 trim transformer를 통해 MyBatis <trim>와 동일한 효과를 낼 수 있게 하였다.
sub 부분은 Java 8 Lambda를 사용하면 좀 더 간결해진다.

지금까지 생성된 쿼리를 보여준 적이 없는데, log 출력 내용은 다음과 같다.

UnderscoreStringBuilder with UnderscoreQlParams : SELECT user_id, name, email, birthday, mobile_phone, home_phone, address, zip_code
FROM users as u
WHERE user_id = ?
   AND name = ?
   AND birthday = ?
   AND zip_code in (?, ?, ?)
LIMIT ?

Query Parameters : [10001, UnderscoreQlParams, Fri Dec 11 00:00:00 KST 2015, 12345, 56789, 58391, 10]


코드를 보면 맨 위에서 도출했던 세가지 문제가 일단은 다 해결된 것을 볼 수 있다.

JPQL/HQL의 ?1, ?2, .. 형태로 쿼리를 생성하고자 한다면 UnderscoreQlParams.withPositionalIndex()를 사용하면 된다.

사실 다 만들어놓고 보니 굳이 이렇게까지 해야하나 싶긴한데, 만드는 과정 자체가 학습이 많이 되어서 만족한다.

동적 Native SQL 생성 어떻게 할까 - Freemarker Dynamic QL Builder 프로그래밍

나는 ORM 그중에서 JPA/Hibernate를 주로 사용하는 사람이다.

작년 이맘때쯤 꽤 오래된 DB 구조에 iBatis로 만들어진 프로젝트를 JPA로 전환하는 작업을 했는데, 그 와중에 딱 한 개의 쿼리가 도저히 Native SQL을 사용하지 않으면 안되는데, 게다가 매우 동적이라 WHERE 조건문이 10여개 이상의 값의 상태에 따라 달라지게 구성되었다.

이 상황에서는 딱히 방법이 없어 문자열 조합으로 동적 Native SQL을 생성해서 JPA의 Native Query 기능으로 해결했다(JPA/Hibernate는 쿼리 실행은 Native SQL로 해도 그 결과를 JPA Entity로 받을 수 있다).
나 같은 경우에는 Native SQL로 실행하더라도 가급적 Entity로 결과를 받도록 하며, 그럴 펼요가 없는 경우의 쿼리는 그냥 Spring JdbcTemplate 같은 것을 사용한다.

이 프로젝트를 겪으면서 수 많은 종류의 쿼리 중에서 Native SQL은 단 한 개만 필요했다(호출량은 별개의 문제이다). 많은 사람들이 Legacy DB에 ORM을 적용하면 Native SQL을 사용할 일이 엄청 많을 것이라고 지레 짐작하는데 실제로 해보면 별로 그렇지 않다. 현재까지 우리 회사에서 진행된 프로젝트들을 보면 Native SQL이 필요한 경우는 1% 미만이거나 아예 Native SQL을 사용하지 않는 프로젝트도 있는 것으로 보인다.

어쨌든, 매우 동적인 Native SQL이 필요한 상황이 온다고 했을 때, iBatis/MyBatis 없이 현재 있는 솔루션(보통은 JPA/Hibernate와 Spring 기반 프로젝트의 경우 Spring JdbcTemplate이 기본으로 갖춰져 있다)으로 처리하고자 할 때 최대 방해요소는 문자열 연결로 난잡하기 짝이 없는 SQL을 생성해야 하는 것이다.

비록 확률은 적더라 이 문제를 근본적으로 해결하고, 혹여나 Legacy DB를 JPA/Hibernate로 전환하는 작업을 하는 사람들의 불안을 좀 없애줄 수 있는 방안을 1년 전부터 두가지 정도 구상했고 그 중 2번째 안을 무려 1년만에 먼저 구현했다(실제 핵심 logic 구현은 이틀 정도 밖에 안 걸리는 거였다.... 귀차니즘을 극복해야해..).

바로 Freemarker로 동적 SQL을 생성하는 것이다. Freemarker는 특정 포맷에 치우치지 않은 Java의 초강력 범용 문자열 템플릿 처리 엔진이다. MyBatis나 iBatis와는 비할바 없이 강력한 템플릿 기능을 지원한다. 동적으로 SQL 문자열 생성하는데 이보다 더 좋은게 있을까? 그리고 MyBatis가 갖는 장점-DBA와 협업할 때 파일 전달이 편하다는 점도 함께 이를 선택하게 하는데 작용했다.


이를 사용하여 Freemarker Dynamic QL Builder라는 것을 만들었다.
이를 통해 다음과 같은 QL 템플릿('users/select.ql.ftl')을 만들고

SELECT *
FROM somewhere
<@ql.where>
    <#if user.name?has_content>
    name = ${param(user.name)}
    </#if>
    <#if user.birthyear gt 0>
    AND birthyear = ${param(user.birthyear)}
    </#if>
    <#if user.employeeType??>
    AND employeeType = ${param(user.employeeType, 'enumToName')}
    </#if>
    <#list userIds!>
    AND userId IN (<#items as userId>${param(userId)}<#sep>,</#sep></#items>)
    </#list>
</@ql.where>

ORDER BY userId
LIMIT 10


파라미터가 될 객체들을 넘겨서 실행해보면(iBatis/MyBatis 사용자들은 FreemarkerDynamicQlBuilder를 쿼리를 실행만 안하는 SqlMapClient라고 생각해보자)
FreemarkerDynamicQlBuilder dynamicQlBuilder = ....;

User user = new User();
user.setName(""); // empty on purpose
user.setBirthyear(2015);
user.setEmployeeType(EmployeeType.FULLTIME);

Map<String,Object> dataModel = new HashMap<String,Object>();
dataModel.put("user", user);
dataModel.put("userIds", new int[]{100, 200, 300});

DynamicQuery dynamicQuery = dynamicQlBuilder.buildQuery("users/select", dataModel);
// dynamicQuery 에 생성된 QL과 파라미터 목록이 들어 있다.


여기서 dynamicQuery 객체를 살펴보면 아래와 같은 프라퍼티들이 생성돼 있다.
dynamicQuery.getQueryString()
==> String
"SELECT *
FROM somewhere
WHERE birthyear = ?
    AND employeeType = ?
    AND userId IN (?,?,?)
ORDER BY userId
LIMIT 10"


dynamicQuery.getParameters()
==> List<Object> : [2015, FULLTIME, 100, 200, 300] 

dynamicQuery.getQueryParameterArray()
==> Object[] : [2015, FULLTIME, 100, 200, 300] 


이 결과를 보면 동적으로 SQL을 생성해줄 뿐만 아니라, PreparedStatement 파라미터 바인딩에 필요한 값들을 리스트와 배열로 순서대로 저장해서 넘겨주는 것을 볼 수 있다.
이렇게 생성된 SQL과 파라미터는 다음과 같이 PreparedStatement로 실행하거나 Spring JdbcTemplate, JDBI, JPA, Hibernate 등의 JDBC 실행/객체 매핑 프레임워크를 통해 실행하면 된다.
PreparedStatement psmt = connection.prepareStatement(dynamicQuery.getQueryString());
dynamicQuery.bindParameters(psmt);

ResultSet rs = psmt.executeQuery();
// 쿼리 결과 처리


${param(변수)}
Freemarker 템플릿을 보고 한 방에 눈치채신 분들도 있을 것 같은데, 동적인 쿼리 생성의 핵심은 문자열이 동적으로 생기는 것에 있는 것이 아니라는 점이다. 그렇게 동적으로 생성된 문자열 중 PreparedStatement 파라미터로 지정되어야 할 위치는 물음표(?)로 대체하고, 해당 물음표에 들어갈 실제 쿼리 파라미터를 순서대로 모아주는 것이 진짜 핵심이다.

그리고 그 핵심적인 역할을 하는 것이 바로 ${param(변수)}라는 이 프리마커 메소드의 역할이다. 이 메소드를 호출하면 그 부분은 물음표(?)로 문자열이 바뀌고 거기 지정한 변수는 순서대로 List에 저장되어 마지막에 전체 파라미터 List를 순서대로 얻을 수 있게 되는 것이다.
사실상 내가 구현한 것의 핵심은 이 ParamMethod 클래스 하나가 다이고, 나머지는 모두 부차적인 것들이라고 봐도 된다.

<@ql.trim>, <@ql.when>, <@ql.set>
MyBatis에 있는 템플릿 지시자들 중 SQL WHERE, SET 생성에 도움을 주는 <trim>,<where>,<set> 을 흉내낸 Freemarker 지시자를 만들었다. <foreach>는 프리마커가 이미 문법적으로 더 깔끔하게 지원하기 때문에 제외했다.

ParameterConverter
Legacy iBatis 프로젝트를 JPA로 전환하면서 나를 제일 당황케 한 것은 마로 매핑 객체에 있는 String의 향연이다.
Legacy DB의 컬럼들 중 많은 것들이 boolean을 문자열 "Y"/"N"으로 표현하고 날짜는 "20151126" 형태의 문자열로 돼 있다.
enum을 넣을 수 있지만, iBatis가 enum을 자동으로 문자열로 안바꾸는지 대부분 enum없이 그냥 일반 String으로 필드가 지정돼 있다. 이로 인해 문서를 보기 전에는 도대체 해당 필드에 유효한 값이 무엇인지 알수가 없다. 이런 특징으로 인해 코드의 아주 많은 부분이 문자열을 boolean, Date, enum 등의 타입으로 변환하고 또 역변환하는 것들로 채워져 있었다. 물론 변환시의 오류처리도 꼼꼼하게 하지 않으면...

JPA/Hibernate 는 Custom Type Converting 기능이 있기 때문에 여러 종류의 Java 객체를 DB 컬럼에 자유롭게 매핑할 수 있다. 그래서 Entity 객체가 올바른 Java 타입의 필드들로 자연스럽게 활용 가능하다.

올바른 Java 타입을 쉽게 사용하려면 동적 SQL을 생성했다 하더라도 거기에 파라미터로 바인딩되는 Java 객체를 DB Column에 맞게 변환해줄 필요가 있다. 이러한 역할을 하는 ParameterConverter라는 개념을 만들어서 이 문제를 넘어가게 했다. 이를 사용하면 Java Date 객체를 문자열로 변경해서 파라미터로 저장 한다던가 boolean을 "Y"/"N" 혹은 "T"/"F" 같은 것으로, 그 외에 JodaTime 같은 것으로 생성한 날짜 객체를 java.sql.Date 로 변환한다던가하는 일을 할 수 있게 된다.
위 FTL에서 ${param(user.employeeType, 'enumToName')} 이 부분이 파라미터 컨버터로 enum을 문자열로 변환한 것이다.

자세한 것은 문서를 참조하면 된다.

QL Builder?
왜 SQL Builder가 아니고 QL Builder일까?
SQL뿐만 아니라 JPQL(JPA), HQL(Hibernate)도 생성 가능하기 때문이다. JPQL의 ?1, ?2, ... 형태의 Positional Parameter 생성에 대해서는 문서를 참조한다.

주의점
앞서 Freemarker가 매우 강력하다고 했는데, 사실 나는 그런 이유로(그리고 기타 다른 몇가지 이유로) Freemarker를 HTML 템플릿 엔진으로는 사용하지 않는다.

프로그래밍 언어 코드가 아닌 특히 서버 프로그래머가 아닌 사람들과 함께 소통해야 하는 영역에 과도한 로직은 Unit Test 등도 사실상 불가능하게 만들고 유지보수성을 현저히 떨어뜨리게 된다.

따라서 Freemarker가 아무리 강력해도 지나치게 로직을 많이 사용하지는 않는 것이 좋다.

그에 반해 Freemarker의 include 같은 기능을 통해 column 목록 같은 반복되는 데이터를 따로 빼서 저장해놓고 include 하는식으로 개발하는 것도 가능하니 활용처를 찾아보는 것도 좋을 것 같다.

마지막으로, 이것을 만든 이유는 자유롭게 Native SQL을 사용하기 위해서가 아니다. 단순히 MyBatis의 1:1 대체재로 이를 사용하는 것은 좋지 않다고 본다. 가급적 ORM을 사용하고 ORM의 보완책으로 어쩔 수 없을 때만 사용하길 바란다.

조만간 원래 구상했던 1안도 구현해야겠다.

- Freemarker Dynamic QL Builder
- Freemarker Dynamic QL Builder 시작하기

원래 구상했던 1안 - 동적 Native SQL 생성 어떻게 할까 - 순수 Java 코드로 생성하기

1 2 3 4 5 6 7 8 9 10 다음