동적 쿼리 설계: 조건 변화에 대응하는 SQL 구성

동적 쿼리의 동작 방식과 구현 패턴, SQL 인젝션 방지, 실행 계획 및 인덱스 관점의 최적화 방법을 정리한다.

2026-08-14 · 최초 발행 2025-08-10

실행 시점에 결정되는 SQL

동적 쿼리는 실행 시점에 SQL 문장을 만들어 수행하는 질의 방식이다. 사용자 입력이나 프로그램 실행 환경에 따라 WHERE 조건, JOIN 조건, ORDER BY가 달라질 수 있다.

정적 쿼리는 컴파일 시점에 문장이 완성되는 반면, 동적 쿼리는 어떤 조건을 포함할지 실행 흐름에서 결정한다. 가격, 카테고리, 평점처럼 선택 항목이 많은 검색 기능이 대표적이다.

검색 조건만이 대상은 아니다. 데이터 상태에 따라 조회 테이블이나 조건이 달라질 수 있고, 사용자 권한에 따라 허용되는 데이터 범위도 달라진다. 관리자는 전체 데이터를 보고 일반 사용자는 제한된 데이터만 조회하는 구조라면, 권한 조건 역시 쿼리 구성에 반영된다.

SQL을 조립하는 방식과 선택 기준

문자열을 이어 붙이는 방식은 가장 단순하다. 하지만 입력값을 SQL 문장에 직접 넣게 되므로 SQL 인젝션 위험이 높다.

// Java에서 문자열 연결을 통한 동적 쿼리 생성 예시
String sql = "SELECT * FROM products WHERE 1=1";
if (category != null) {
    sql += " AND category = '" + category + "'";
}
if (minPrice > 0) {
    sql += " AND price >= " + minPrice;
}
// 실행
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

값을 바인딩할 수 있는 경우에는 준비된 문을 활용한다. 플레이스홀더(?)로 템플릿을 만들고 값은 별도로 전달한다. SQL 인젝션 방지에는 효과적이지만, SQL 구조 자체를 크게 바꾸는 용도에는 제약이 있다.

// 조건에 따라 다른 PreparedStatement 사용
String sql = "SELECT * FROM products WHERE category = ? AND price >= ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, category);
pstmt.setInt(2, minPrice);
ResultSet rs = pstmt.executeQuery();

MyBatis, Hibernate, JPA Criteria API 같은 프레임워크는 조건부 SQL을 객체 또는 선언형 구조로 다룰 수 있게 한다. MyBatis에서는 조건의 존재 여부에 맞춰 WHERE 절을 구성할 수 있다.

<!-- MyBatis 동적 SQL 예시 -->
<select id="findProducts" resultType="Product">
  SELECT * FROM products
  <where>
    <if test="category != null">
      AND category = #{category}
    </if>
    <if test="minPrice != null">
      AND price >= #{minPrice}
    </if>
    <if test="maxPrice != null">
      AND price <= #{maxPrice}
    </if>
  </where>
</select>

쿼리 빌더 패턴은 조건을 객체 단위로 누적해 SQL을 구성한다. 복잡한 조건을 나누어 표현할 수 있어 읽기와 유지보수에 유리하다.

// JPA Criteria API 예시
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> product = query.from(Product.class);

List<Predicate> predicates = new ArrayList<>();
if (category != null) {
    predicates.add(cb.equal(product.get("category"), category));
}
if (minPrice > 0) {
    predicates.add(cb.greaterThanOrEqualTo(product.get("price"), minPrice));
}

query.select(product).where(predicates.toArray(new Predicate[0]));
List<Product> results = entityManager.createQuery(query).getResultList();

유연성의 대가를 운영에서 다루기

동적 쿼리는 비슷한 질의를 템플릿으로 묶어 중복 코드를 줄이고, 다양한 사용자 요구를 처리할 수 있다. 복잡한 필터를 제공하는 서비스에서는 사용자 경험에도 직접 영향을 준다.

반대로 조건 조합이 달라지면 실행 계획도 매번 달라질 수 있다. 쿼리 캐시 효율이 낮아지고, 인덱스를 최적으로 활용하기 어려운 경우도 생긴다. 문자열 연결 방식은 SQL 인젝션에 취약하며, 실행 시점에 만들어진 SQL은 오류 원인을 추적하기도 복잡하다.

자주 사용하는 패턴은 바인드 변수로 하드 파싱을 줄이고 실행 계획 캐싱을 고려할 수 있다. 특정 인덱스 사용이 필요하다면 데이터베이스 최적화기에 힌트를 제공하는 방법도 있다.

-- Oracle에서 힌트 사용 예시
SELECT /*+ INDEX(products idx_category) */ *
FROM products
WHERE category = :category AND price >= :minPrice;

자주 함께 쓰이는 조건에는 복합 인덱스를 설계하고, 인덱스 사용률을 모니터링하며 조정한다. 조건을 무제한으로 늘리기보다 중요한 조건을 우선 처리해 쿼리 복잡도를 제한하는 편이 낫다.

상품 검색에서 조건이 조립되는 흐름

이커머스 검색은 카테고리, 가격 범위, 평점, 배송 옵션 같은 필터를 조합한다. 선택된 필터는 WHERE 절에 반영되고, 인기순·가격순·최신순 선택은 ORDER BY 절을 바꾼다.

카테고리 필터가격 범위 필터평점 필터재고 여부사용자 검색 요청검색 파라미터 분석필터 조건 확인카테고리 조건 추가가격 조건 추가평점 조건 추가재고 조건 추가최종 쿼리 생성쿼리 실행결과 반환

권한과 집계가 함께 바뀌는 리포트

금융 시스템의 리포트는 요청자의 권한에 따라 조회 범위를 다르게 해야 한다. 관리자에게는 전체 데이터 접근 쿼리를, 부서장에게는 부서 데이터 접근 쿼리를, 일반 직원에게는 개인 데이터 접근 쿼리를 구성한다. 여기에 기간 조건, 집계 방식, 그룹화 기준이 더해지고 복잡한 조인과 서브쿼리도 동적으로 생성될 수 있다.

관리자부서장일반 직원리포트 요청사용자 권한 확인권한 수준전체 데이터 접근 쿼리부서 데이터 접근 쿼리개인 데이터 접근 쿼리기간 조건 추가집계 함수 선택그룹화 기준 설정최종 쿼리 실행리포트 생성

안전하게 유지하는 구현 습관

값은 문자열 결합 대신 파라미터화된 쿼리로 전달한다. PreparedStatement나 ORM의 파라미터 바인딩 기능을 사용하면 SQL 인젝션 위험을 줄일 수 있다.

생성된 동적 쿼리는 로그에 기록하고, 실행 계획과 성능 지표를 함께 모니터링한다. 조건이 많은 쿼리는 작은 단위로 나누고 조건별 모듈화를 적용하면 재사용하기 쉽다. 결과 집합이 과도하게 커지지 않도록 기본 필터를 두고, 대용량 조회에는 페이징 처리가 필요하다. 오프셋 기반 방식 대신 키셋 페이징도 고려할 수 있다.

-- 키셋 페이징 예시
SELECT * FROM products
WHERE (price < :lastPrice OR (price = :lastPrice AND id < :lastId))
ORDER BY price DESC, id DESC
LIMIT 20;

동적 쿼리는 조건 변화에 대응하는 수단이지만, 보안과 성능, 유지보수성을 자동으로 해결하지는 않는다. 어떤 값을 바인딩할지, 어떤 조건 조합을 지원할지, 생성된 SQL을 어떻게 관찰할지를 함께 결정해야 한다.

동적 쿼리SQL데이터베이스쿼리 최적화SQL 인젝션