고객사 업무를 하다 보니 MyBatis를 만지는 일이 늘었다. SQL 매핑 프레임워크의 흐름은 JPA와 Hibernate 쪽으로 기운 지 오래지만, 한국에서는 아직 MyBatis 비율이 높다. 정부 표준 프레임워크가 MyBatis를 채택한 게 한몫했다고 본다.
이 글에서는 MyBatis로 SQL을 동적으로 만들 때 쓰는 <bind> 태그를 예제 세 개로 정리한다. 미리 말하면 앞의 두 예제는 쿼리만으로도 풀 수 있는 걸 번거롭게 만든 느낌이 있다. 실무에서 쓸모가 있는 건 세 번째다.
bind는 SQL 앞에서 변수를 하나 만든다
<select id="getUsersByAge" resultType="User">
<bind name="maxAge" value="age + 5" />
SELECT * FROM users
WHERE age BETWEEN #{age} AND #{maxAge}
</select>
<bind>가 maxAge라는 변수를 만든다. 파라미터로 받은 age에 5를 더해 다섯 살 많은 사람까지 조회한다. 솔직히 이 예제는 #{age} + 5로 끝낼 일을 코드만 길게 만든 쪽에 가깝다. 그래도 방향은 보인다. 한 번 만든 변수를 쿼리 안에서 여러 번 재사용할 수 있고, 계산 로직을 SQL 본문 밖으로 빼니 쿼리도 읽기 쉬워진다.
LIKE 패턴은 bind로 한 번만 만든다
<select id="searchProducts" resultType="Product">
<bind name="searchPattern" value="'%' + keyword + '%'" />
SELECT * FROM products
WHERE name LIKE #{searchPattern}
OR description LIKE #{searchPattern}
</select>
검색 키워드 앞뒤에 %를 붙여 searchPattern을 만들고, 부분 일치 검색 두 곳에 같이 쓴다. 여기서 봐야 할 점은 <bind>의 value는 SQL이 실행되기 전에 Java 쪽에서 먼저 평가된다는 것이다. 문자열 연산만 되는 게 아니라 Java 코드를 부를 수 있다는 뜻이고, 세 번째 예제가 이걸 쓴다.
Enum 메서드의 반환값을 SQL 안으로 끌어온다
AWS Savings Plans 구매 유형을 다루는 쿼리다. Savings Plans 적용 대상일 때 특정 구매 유형 코드를 조회에서 빼야 했다.
<sql id="autospFilterSp">
<bind name="purchaseCdListIncludedInSp" value="@com.a.b.c.common.constant.d.EnumPurchaseType@getPurchaseCdListIncludedInSp()"/>
AND IF(ATSP.SP_YN = 'Y', RCM.OFFR_TYP_NM NOT IN (<foreach collection="purchaseCdListIncludedInSp" item="item" separator=",">#{item}</foreach>), TRUE)
</sql>
구매 유형 코드는 이미 Enum에 정의되어 있다.
@Getter
public enum CmmtOptiSpPurchaseType {
/** ComputeSavingsPlans */
COMPUTE("ComputeSavingsPlans", "Compute",
"AmazonEC2",
"AmazonECS",
"AmazonEKS",
"AWSLambda"
),
/** EC2InstanceSavingsPlans */
EC2_INSTANCE("EC2InstanceSavingsPlans", "EC2 Instance",
"AmazonEC2"
),
/** SageMakerSavingsPlans */
SAGEMAKER("SageMakerSavingsPlans", "SageMaker",
"AmazonSageMaker"
)
/** 코드 */
private final String cd;
/** 이름 */
private final String nm;
/** 서비스 코드 집합 */
private final Set<String> svcCdSet;
CmmtOptiSpPurchaseType(String cd, String nm, String... svcCdArray) {
this.cd = cd;
this.nm = nm;
this.svcCdSet = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(svcCdArray)));
}
...생략...
// SQL에서 제외할 구매 유형 코드 목록을 돌려준다
public static List<String> getPurchaseCdListIncludedInSp() {
return Arrays.asList(EnumPurchaseType.COMPUTE.getCd(), EnumPurchaseType.EC2_INSTANCE.getCd());
}
}
흐름은 두 단계다.
@패키지.클래스@메서드()문법으로com.a.b.c.common.constant.d패키지의EnumPurchaseType에서getPurchaseCdListIncludedInSp를 실행하고, 반환값을purchaseCdListIncludedInSp변수에 담는다. 반환값은["ComputeSavingsPlans", "EC2InstanceSavingsPlans"]다.그 리스트를
<foreach>로 풀어NOT IN (...)절을 만든다.
그러면 이런 SQL이 나온다.
AND IF(ATSP.AUTOSP_YN = 'Y', RCM.OFFR_TYP_NM NOT IN ('ComputeSavingsPlans', 'EC2InstanceSavingsPlans'), TRUE)
#{item}으로 넣었으니 실제로는 ? 자리에 두 값이 바인딩된다. 위 SQL은 값이 들어간 모습을 풀어 쓴 것이다.
<bind>의 가치는 이미 있는 Enum과 메서드를 SQL이 그대로 가져다 쓰게 하는 데 있다. 제외할 코드 두 개를 쿼리에 문자열로 적었다면, 구매 유형이 바뀔 때 Enum과 매퍼 XML을 둘 다 고쳐야 한다. 지금은 Enum 한 곳만 고치면 된다.