Druid SQLUtils API 사용 가이드

Druid SQLUtils API 활용 방법

SQLUtils는 Druid 내장 SQL 파서의 핵심 유틸리티 클래스로, SQL 포맷팅, 파싱, AST 변환, 테이블/필드 추출, SQL 검증 및 재작성 기능을 제공합니다. MySQL, Oracle, SQL Server 등 다양한 데이터베이스 방언을 지원하며, SQL 감사, 권한 제어, 동적 SQL 생성에 활용되는 필수 도구입니다.

  1. 의존성 추가

Maven 의존성

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.20</version>
</dependency>

핵심 클래스 임포트

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.visitor.SchemaStatVisitor;
import com.alibaba.druid.sql.parser.ParserException;
  1. 핵심 API와 실제 예제

1. SQL 포맷팅 (format)

기능: SQL 서식을 보기 좋게 정리하며, 대소문자 출력 옵션을 지원하고 사용자 정의 포맷 옵션을 적용할 수 있습니다.

메서드 시그니처

// 기본 대문자 포맷팅
public static String format(String sql, DbType dbType)
// 사용자 정의 포맷팅 옵션 (대문자/소문자)
public static String format(String sql, DbType dbType, FormatOption option)

코드 예제

String sql = "select id,username from user where age>18 and department='개발팀'";
// 1. 기본 대문자 포맷팅
String formattedSql = SQLUtils.format(sql, DbType.mysql);
System.out.println("대문자 포맷팅:\n" + formattedSql);
// 2. 소문자 포맷팅
String lowerFormattedSql = SQLUtils.format(sql, DbType.mysql, SQLUtils.DEFAULT_LCASE_FORMAT_OPTION);
System.out.println("소문자 포맷팅:\n" + lowerFormattedSql);

출력 결과

대문자 포맷팅:
SELECT id, username
FROM user
WHERE age > 18
  AND department = '개발팀'

소문자 포맷팅:
select id, username
from user
where age > 18
  and department = '개발팀'

2. SQL을 AST로 파싱 (parseStatements)

기능: SQL 문자열을 추상 구문 트리(AST)로 변환하며, SQLStatement 목록을 얻을 수 있습니다. 다중 문장 파싱을 지원합니다.

메서드 시그니처

// 단일/다중 SQL 파싱
public static List<SQLStatement> parseStatements(String sql, DbType dbType)

코드 예제

String multiQuery = "insert into employee(id,name) values(101,'홍길동');update employee set salary=5000 where id=101;";
try {
    // 다중 SQL 파싱
    List<SQLStatement> statements = SQLUtils.parseStatements(multiQuery, DbType.mysql);
    for (SQLStatement statement : statements) {
        System.out.println("SQL 유형: " + statement.getClass().getSimpleName());
    }
} catch (ParserException e) {
    System.err.println("SQL 파싱 실패: " + e.getMessage());
}

출력 결과

SQL 유형: SQLInsertStatement
SQL 유형: SQLUpdateStatement

3. AST를 다시 SQL 문자열로 변환 (toSQLString)

기능: 파싱된 AST(SQLStatement)를 다시 SQL 문자열로 생성하며, 포맷팅된 출력을 지원합니다.

메서드 시그니처

// 단일 문장을 SQL로 변환
public static String toSQLString(SQLStatement stmt, DbType dbType)
// 다중 문장을 SQL로 변환
public static String toSQLString(List<SQLStatement> stmts, DbType dbType)

코드 예제

String sql = "select * from employee where emp_id=101";
List<SQLStatement> statements = SQLUtils.parseStatements(sql, DbType.mysql);
// AST를 다시 SQL로 변환
String restoredSql = SQLUtils.toSQLString(statements, DbType.mysql);
System.out.println("복원된 SQL:\n" + restoredSql);

4. 테이블/필드 정보 추출 (SchemaStatVisitor)

기능: SchemaStatVisitor를 사용하여 AST를 순회하며 SQL에 포함된 테이블명, 필드명, 작업 유형(SELECT/INSERT/UPDATE)을 추출합니다.

코드 예제

String sql = "select e.emp_id,e.name,d.dept_name from employee e join department d on e.dept_id=d.id where e.salary>5000";
List<SQLStatement> statements = SQLUtils.parseStatements(sql, DbType.mysql);
// SchemaStatVisitor 생성
SchemaStatVisitor visitor = SQLUtils.createSchemaStatVisitor(DbType.mysql);
statements.get(0).accept(visitor);
// 테이블명 추출
System.out.println("사용된 테이블: " + visitor.getTables().keySet());
// 필드명 추출
System.out.println("사용된 필드: " + visitor.getColumns());
// 작업 유형 추출
System.out.println("작업 유형: " + visitor.getSqlCommandType());

출력 결과

사용된 테이블: [employee, department]
사용된 필드: [employee.emp_id, employee.name, employee.salary, employee.dept_id, department.dept_name, department.id]
작업 유형: SELECT

5. SQL 유효성 검사 (isValid)

기능: SQL 문법이 유효한지 빠르게 검사하여 파싱 예외를 방지합니다.

코드 예제

// 유효한 SQL
String validQuery = "select * from employee where emp_id=101";
// 유효하지 않은 SQL (문법 오류)
String invalidQuery = "select * from employee where emp_id=101 and";
// 검증 결과
System.out.println("유효한 SQL 검증: " + SQLUtils.isValid(validQuery, DbType.mysql));
System.out.println("유효하지 않은 SQL 검증: " + SQLUtils.isValid(invalidQuery, DbType.mysql));

출력 결과

유효한 SQL 검증: true
유효하지 않은 SQL 검증: false

6. 동적 SQL 수정 (AST 조작)

기능: AST를 기반으로 SQL을 동적으로 수정하며, WHERE 조건 추가, 테이블명 변경, LIMIT 추가 등이 가능합니다.

코드 예제 (조건 추가)

String sql = "select * from product where price>10000";
List<SQLStatement> statements = SQLUtils.parseStatements(sql, DbType.mysql);
SQLSelectStatement selectStmt = (SQLSelectStatement) statements.get(0);
SQLWhere whereClause = selectStmt.getWhere();
// 조건 추가: category_id=3
SQLExpr categoryCondition = SQLUtils.createEqualExpr(
    new SQLIdentifierExpr("category_id"),
    new SQLNumberExpr(3)
);
if (whereClause == null) {
    whereClause = new SQLWhere(categoryCondition);
    selectStmt.setWhere(whereClause);
} else {
    whereClause.addCondition(categoryCondition, SQLBinaryOperator.AND);
}
// 수정된 SQL 생성
String modifiedSql = SQLUtils.toSQLString(selectStmt, DbType.mysql);
System.out.println("수정된 SQL:\n" + modifiedSql);

출력 결과

수정된 SQL:
SELECT *
FROM product
WHERE price > 10000
  AND category_id = 3
  1. 주요 열거형과 설정

1. 지원하는 데이터베이스 유형 (DbType)

  • DbType.mysql: MySQL
  • DbType.oracle: Oracle
  • DbType.sqlserver: SQL Server
  • DbType.postgresql: PostgreSQL
  • DbType.db2: DB2

2. 포맷팅 옵션 (FormatOption)

  • SQLUtils.DEFAULT_FORMAT_OPTION: 기본 대문자
  • SQLUtils.DEFAULT_LCASE_FORMAT_OPTION: 소문자
  • 사용자 정의: new FormatOption().setUppercase(false).setIndent(" ")
  1. 일반적인 사용 시나리오

  1. SQL 감사: SQL을 파싱하여 테이블/필드를 추출하고 작업 로그를 기록하며, 민감한 테이블 접근을 방지합니다.

  2. 데이터 권한 제어: 테넌트 ID, 부서 ID 등 조건을 동적으로 추가하여 행 수준 권한을 구현합니다.

  3. SQL 최적화: SQL을 포맷팅하여 가독성을 높이고, AST를 파싱하여 비효율적인 SQL(인덱스 없음, 전체 테이블 스캔)을 분석합니다.

  4. 동적 테이블명: 분할 테이블 시나리오에서 SQL을 파싱하여 테이블명을 동적으로 교체합니다(예: product_202305).

  5. SQL 가로채기: MyBatis 인터셉터에서 SQL 유효성을 검사하여 위험한 작업(예: drop table)을 차단합니다.

  6. 주의사항


  1. SQL 호환성: 데이터베이스 방언마다 문법 차이가 크므로, 반드시 올바른 DbType을 지정해야 합니다. 그렇지 않으면 파싱에 실패합니다.
  2. 예외 처리: 유효하지 않은 SQL은 ParserException을 발생시키므로, 프로그램 충돌을 방지하기 위해 예외를 처리해야 합니다.
  3. 성능: SQL 파싱은 AST를 기반으로 하므로, 복잡한 SQL(다중 테이블 조인, 서브쿼리)은 파싱 시간이 다소 소요됩니다. 파싱 결과를 캐싱하는 것이 좋습니다.
  4. AST 불변성: 파싱된 AST는 기본적으로 불변이며, 수정 후에는 SQL 문자열을 다시 생성해야 합니다.

태그: Druid SQLUtils SQL 파싱 AST java

6월 20일 19:37에 게시됨