SPRING

MyBatis에서 Helper 클래스 적용하기 (StringUtils, CollectionUtils...)

킹명주 2024. 6. 23. 22:09

오늘은 회사 프로젝트를 진행하면서 생긴 이슈와 이를 해결하기 위한 일종의 회고를 써보려고 한다.

 

SI 회사에서 업무를 진행하다보니, MyBatis를 좀 많이 쓰는 것 같다. 일단, 프로젝트를 부탁하는 곳에서 JPA 보다 좀 더 직관적으로 쿼리문을 살펴볼 수 있는 MyBatis를 선호하는 경향이 있는 것 같다.

 

그래서 필자는 회사 경험에서 JPA를 써본적은 없고 MyBatis만 써보았다. 그러다보니 MyBatis에서 좀 더 효율적으로 코드를 작성할 수 있는 방법을 많이 찾게 되었다.

 

이번 글은 단순 호기심에서 해결 방안까지 도출한 경험을 작성하려고 한다! 꽤 유용한 내용도 있으니 재미있게 이 글을 봐주셨으면 좋겠다. (알쓸신잡 느낌 ㅎ)

 


동적 쿼리문에서 StringUtils.hasText()를 사용할 수 없을까?

 

위 질문으로 부터 해당 글은 시작된다.

회사 프로젝트를 진행하다 보니 쿼리문에 이러한 쿼리문이 많았다.

    <select id="getShippingProducts" resultType="ShippingProductVo">
        SELECT ID
             , CODE
             , NAME
             , EXPIRED_DATE
          FROM PRODUCT
         WHERE 1=1
         <if test="code != null and code != ''">
           AND CODE = #{code}
         </if>
        <if test="name != null and name != ''">
           AND NAME = #{name}
        </if>
    </select>

<if> 태그안에 보면 String type의 null와 공백 체크를 진행하고 있다. 이런식으로 사용해야 하는 이유는 mybatis는 OGNL기반 표현식을 사용하고 있기 때문이다.

 

OGNL(Object Graph Navigation Language)을 간단하게 집고 넘어가자면,  MyBatis에서 XML element를 줄이고 다양한 조건을 처리하기 위해 OGNL 표현식을 사용하고 있다.

test 연산을 위해 and, or과 같인 예약어를 제공하고 있고, 싱글쿼테이션('), 더블쿼테이션(") 없이 문자열이 들어오게 되면 파라미터 객체라고 판단한다.

그러다 보니 제공하는 표현식을 바탕으로 <if test="code != null and code != ''"> 이렇게 null 및 empty 체크를 진행해야 한다. (이게 최선인 형태..)

 

그런데,, 아마 springfamework를 사용하는 분들이라면 StringUtils라는 util 패키지를 알고 있을 것이다. StringUtils에는

hasText라는 정적 메서드가 있는데, 정말 심플하게 String의 null 및 공백을 체크할 수 있다.

 

그래서 이를 OGNL 표현식에 녹아들게 할 수 없을까? 라는 호기심이 생겨 킹명주의 실험실을 가동했다.


MyBatis에 StringUtils를 적용해보자.

 

일단 해보자!! 

 

첫번째) StringUtils 바로 써보기

    <select id="getShippingProducts" resultType="ShippingProductVo">
        SELECT ID
             , CODE
             , NAME
             , EXPIRED_DATE
          FROM PRODUCT
         WHERE 1=1
         <if test="StringUtils.hasText(code)">
           AND CODE = #{code}
         </if>
        <if test="StringUtils.hasText(code)">
           AND NAME = #{name}
        </if>
    </select>

위와 같은 에러가 출력되는데, OGNL 원리를 알았다면 당연한 결과이다. ', "가 없으면 파라미터 객체로 인식 즉, StringUtils를 하나의 파라미터로 보고 있는 것이다. (mybatis입장에서는 String StringUtils) 그래서 이 StringUtils이라는 String 객체에 hasText라는 함수는 없어요~ 하고 NoSuchMethodException을 던지고 있다.

 

두번째) @class_path@method 사용하기

MyBatis 공식 문서를 참고해보니 if문 안에서 class를 사용하고 싶을 때, @를 활용하라고 되어 있어 사용해보았다.

    <select id="getShippingProducts" resultType="ShippingProductVo">
        SELECT ID
             , CODE
             , NAME
             , EXPIRED_DATE
          FROM PRODUCT
         WHERE 1=1
         <if test="@org.springframework.util.StringUtils@hasText(code)">
           AND CODE = #{code}
         </if>
        <if test="@org.springframework.util.StringUtils@hasText(code)">
           AND NAME = #{name}
        </if>
    </select>

이렇게 사용하니 가능했다.. 그런데 사실 이렇게 풀 패키지명을 쓰니 != '' and != null이 더 직관적이고 좋은 코드라는 생각이 들었다.

그래서 @org.springframework.util.StringUtils 이 경로에 별칭을 달아 해결할 수 없을까? 라고 생각이 들었다.

 

세번째) mybatis-config.xml의 typeAliases 활용하기

주로 parameterType, resultType에 자주활용되는 <typeAlias type="" alias=""/> 형태로 StringUtils를 지정하여 사용해보자!

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias type="org.springframework.util.StringUtils" alias="StringUtils"/>
    </typeAliases>
</configuration>

 

    <select id="getShippingProducts" resultType="ShippingProductVo">
        SELECT ID
             , CODE
             , NAME
             , EXPIRED_DATE
          FROM PRODUCT
         WHERE 1=1
         <if test="@StringUtils@hasText(code)">
           AND CODE = #{code}
         </if>
        <if test="@StringUtils@hasText(code)">
           AND NAME = #{name}
        </if>
    </select>

뭔가 그럴싸하다.. 결과는?

흐음... 지금 내 상태 홀리몰리..

이걸 어떻게 해야할까?? 열심히 웹 서핑을 해보았다.

https://github.com/mybatis/mybatis-3/issues/871

 

Request for adding explanation how to call static methods to MyBatis manual. · Issue #871 · mybatis/mybatis-3

Hi, I know that you are able to call static methods in My Batis Mapper XML files, but the manual, I think, doesn't explain the way directly. I'm afraid that some of my colleagues had troubled becau...

github.com

신기하게 나랑 동일한 생각을 가진 사람이 있었다 ㅋㅋㅋㅋㅋㅋㅋㅋ.. 근데 MyBatis 개발자가 요청사항을 수용하지는 않은 것 같다. 이처럼 경로를 지정하여 라이브러리를 활용하는 것을 OGNL에 반하는 행동이라고 판단하고 있는 것 같다.

 


방법이 없는 것인가?

 

방법이 없는 것인가.. 라고 고민할 때, 생각해보니 엄청 쉬운 해결방법이 있었다. 생각해보니 @class_path@method 방법을 활용하면 되었다. 왜 class_path를 서술해야하나? 라고 생각해보니 

  Class<?> classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {
        try {
          return Class.forName(name, true, cl);
        } catch (ClassNotFoundException e) {
          // we'll ignore this until all classloaders fail to locate the class
        }
      }
    }

ClassLoaderWrapper java 파일을 활용하고 있기 때문이였다. <if> 문 안에서 @로 class 경로로 지정해주면 해당 name을 바탕으로 class를 찾는 원리였다.. 

그렇다면?!!!! 해결방법은 하나.. 바로 가장 최상단(default package)에 util class를 생성하면 되겠구나!! 

public class MyBatisHelper {

    private MyBatisHelper(){}
    
    public static boolean hasText(String str){
        return str != null && !str.isBlank();
    }
}

MyBatisHelper라는 파일을 가장 최상단에 지정해주었고 이를 TestMapper에 넣어서 써보려고 한다.

    <select id="getShippingProducts" resultType="ShippingProductVo">
        SELECT ID
             , CODE
             , NAME
             , EXPIRED_DATE
          FROM PRODUCT
         WHERE 1=1
         <if test="@MyBatisHelper@hasText(code)">
           AND CODE = #{code}
         </if>
        <if test="@MyBatisHelper@hasText(code)">
           AND NAME = #{name}
        </if>
    </select>

null, empty 실험에 모두 통과하는 것을 확인할 수 있었다.

 

열심히 삽질해서 해결방안을 도출했는데, 이 방법은 권장하지 않는다.

https://stackoverflow.com/questions/7849421/is-the-use-of-javas-default-package-a-bad-practice

 

Is the use of Java's default package a bad practice?

Is the use of Java's default package a bad practice?

stackoverflow.com

https://docs.spring.io/spring-boot/reference/using/structuring-your-code.html#using.structuring-your-code

 

Structuring Your Code :: Spring Boot

When a class does not include a package declaration, it is considered to be in the “default package”. The use of the “default package” is generally discouraged and should be avoided. It can cause particular problems for Spring Boot applications tha

docs.spring.io

위 자료들을 참고해보면 일단, 스프링부트의 경우 com.example.demo와 같은 패키지 구조 하위에서 모든 파일을 관리하기 때문에, 기본 클래스의 경우 관리가 어렵기도하고, 원칙에 어긋나는 행동으로 볼 수 있다.

그러므로,  mybatis에서 clean code를 작성하기 위해 default package에 Helper class를 만드는 것은 리스크가 있는 행동으로 보여진다.


오늘의 결론

 

1.

default package에 class를 만들면 MyBatis의 OGNL 표현식에서 간단하게 사용 가능하다.

ex. <if test="@MyBatisHelper@hasText(code)">

-> 이 방법을 권장하지 않는다 (spring boot 원칙 위배)

 

2.

<if test="@org.springframework.util.StringUtils@hasText(code)"> vs <if test="code != null and code != ''"> 중에서 아무거나 사용하면 될 것같다.

 

오늘의 실험실은 이것으로 마치겠다.. ㅎ 사실, 요즘 프로젝트를 진행하다보니 그냥 아무생각 없는 개발자가 되기도 해서 좀 아쉬웠다. 

이번 계기로 방법을 찾을 수 있고 문제를 창의적으로 해결할 수 있는 개발자가 되어야겠다.