Druid SQLUtils API 활용 방법
SQLUtils는 Druid 내장 SQL 파서의 핵심 유틸리티 클래스로, SQL 포맷팅, 파싱, AST 변환, 테이블/필드 추출, SQL 검증 및 재작성 기능을 제공합니다. MySQL, Oracle, SQL Server 등 다양한 데이터베이스 방언을 지원하며, SQL 감사, 권한 제어, 동적 SQL 생성에 활용되는 필수 도구입니다.
- 의존성 추가
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;
- 핵심 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. 지원하는 데이터베이스 유형 (DbType)
DbType.mysql: MySQLDbType.oracle: OracleDbType.sqlserver: SQL ServerDbType.postgresql: PostgreSQLDbType.db2: DB2
2. 포맷팅 옵션 (FormatOption)
SQLUtils.DEFAULT_FORMAT_OPTION: 기본 대문자SQLUtils.DEFAULT_LCASE_FORMAT_OPTION: 소문자- 사용자 정의:
new FormatOption().setUppercase(false).setIndent(" ")
- 일반적인 사용 시나리오
-
SQL 감사: SQL을 파싱하여 테이블/필드를 추출하고 작업 로그를 기록하며, 민감한 테이블 접근을 방지합니다.
-
데이터 권한 제어: 테넌트 ID, 부서 ID 등 조건을 동적으로 추가하여 행 수준 권한을 구현합니다.
-
SQL 최적화: SQL을 포맷팅하여 가독성을 높이고, AST를 파싱하여 비효율적인 SQL(인덱스 없음, 전체 테이블 스캔)을 분석합니다.
-
동적 테이블명: 분할 테이블 시나리오에서 SQL을 파싱하여 테이블명을 동적으로 교체합니다(예:
product_202305). -
SQL 가로채기: MyBatis 인터셉터에서 SQL 유효성을 검사하여 위험한 작업(예:
drop table)을 차단합니다. -
주의사항
- SQL 호환성: 데이터베이스 방언마다 문법 차이가 크므로, 반드시 올바른
DbType을 지정해야 합니다. 그렇지 않으면 파싱에 실패합니다. - 예외 처리: 유효하지 않은 SQL은
ParserException을 발생시키므로, 프로그램 충돌을 방지하기 위해 예외를 처리해야 합니다. - 성능: SQL 파싱은 AST를 기반으로 하므로, 복잡한 SQL(다중 테이블 조인, 서브쿼리)은 파싱 시간이 다소 소요됩니다. 파싱 결과를 캐싱하는 것이 좋습니다.
- AST 불변성: 파싱된 AST는 기본적으로 불변이며, 수정 후에는 SQL 문자열을 다시 생성해야 합니다.