TokyoAJ

도쿄아재

SPRINGBOOT 2025.07.15

MyBatis 바인딩 파라미터와 오라클 실행계획 차이로 인한 데이터 미조회 현상

MyBatis 바인딩 파라미터와 오라클 실행계획 차이로 인한 데이터 미조회 현상

현상: 로그에는 정상, 데이터는 안 들어옴?

실무에서 MyBatis를 사용하다 보면, 쿼리 로그에는 분명히 정상적으로 SQL이 찍히는데 실제로는 데이터가 DTO에 매핑되지 않는 황당한 경험을 하신 적이 있으신가요?

특히 오라클(Oracle)과 같이 바인딩 파라미터(:param) 방식에 민감한 DB에서는 이런 현상이 종종 발생합니다.

예시 상황

예를 들어, 아래와 같이 직원 번호와 이름을 모두 검색하는 동적 쿼리가 있다고 가정해봅시다.

<!-- MyBatis Mapper -->
SELECT *
FROM employee
WHERE REGEXP_LIKE(emp_no, #{keyword}, 'i')
OR emp_no IN (
SELECT emp_no FROM employee_info WHERE emp_name LIKE '%' || #{keyword} || '%'
)

로그에는 아래처럼 실제 값이 치환되어 잘 찍힙니다.

SELECT *
FROM employee
WHERE REGEXP_LIKE(emp_no, 'kim', 'i')
OR emp_no IN (
SELECT emp_no FROM employee_info WHERE emp_name LIKE '%kim%'
)

이 쿼리를 SQL Developer에서 직접 실행하면 정상적으로 결과가 나옵니다.

하지만! 실제 서버에서는?

실제 MyBatis가 서버에서 실행하는 쿼리는 아래처럼 바인딩 파라미터 형태로 동작합니다.

SELECT *
FROM employee
WHERE REGEXP_LIKE(emp_no, :1, 'i')
OR emp_no IN (
SELECT emp_no FROM employee_info WHERE emp_name LIKE '%' || :1 || '%'
)

이렇게 실행했을 때, 아무런 결과도 조회되지 않는 경우가 발생할 수 있습니다.

원인: 오라클의 실행계획(Execution Plan) 차이

오라클은 바인딩 파라미터(:1, :searchValue)와 리터럴('kim')이 들어갔을 때 실행계획을 다르게 세우는 경우가 많습니다.

대표적인 차이점

  1. LIKE, REGEXP_LIKE와 바인딩 파라미터REGEXP_LIKE(emp_no, :1, 'i')처럼 파라미터가 들어가면 옵티마이저가 제대로 인덱스를 타지 않거나, 예상을 벗어난 실행계획을 세울 수 있습니다.
  2. 반면, 리터럴이 들어가면(예: 'kim') 좀 더 효율적인 실행계획을 만듭니다.
  3. OR 조건의 조합OR 조건 내부에 바인딩 파라미터가 들어가면 옵티마이저가 예상치 못한 결과를 낼 수 있습니다.
  4. 특히 LIKE, REGEXP_LIKE, IN 서브쿼리 조합에서 바인딩값 사용시 결과가 아예 안 나올 수도 있습니다.

해결 방법

1. 검색 조건을 명확히 분리

동적 쿼리로, emp_no 검색이면 그 조건만, emp_name 검색이면 그 조건만 실행하도록 쿼리를 분리합니다.

<!-- MyBatis 동적쿼리 예시 -->
<select id="searchEmployee" parameterType="map" resultType="Employee">
SELECT *
FROM employee
<where>
<if test="searchType == 'emp_no'">
AND REGEXP_LIKE(emp_no, #{keyword}, 'i')
</if>
<if test="searchType == 'emp_name'">
AND emp_no IN (
SELECT emp_no FROM employee_info WHERE emp_name LIKE '%' || #{keyword} || '%'
)
</if>
</where>
</select>

2. OR 조합이 불가피하다면

  1. 파라미터를 직접 리터럴로 치환하는 방식도 고려할 수 있습니다.
  2. (단, 이때는 SQL Injection 등 보안 이슈에 주의!)
  3. 혹은 쿼리를 쪼개서 각각 실행 후 결과를 합치는 방식도 실무에서 종종 사용합니다.

3. 실행계획을 강제로 리프레시

  1. 오라클 힌트 사용 (예: /*+ dynamic_sampling(4) */ 등)
  2. 바인딩 파라미터의 타입/포맷을 명확히 맞춰서 전달

정리

  1. MyBatis 로그에 찍힌 쿼리와 실제 서버에서 실행되는 쿼리는 다를 수 있습니다.
  2. 오라클은 바인딩 파라미터로 들어가면, 리터럴과는 다른 실행계획을 세워서 결과가 다르게 나올 수 있습니다.
  3. 동적쿼리로 조건을 분리하거나, 쿼리를 분할하는 방법으로 해결 가능합니다.

실무 팁

  1. 바인딩 파라미터와 실행계획의 차이를 의심해보세요.
  2. SQL Developer에서 바인딩 모드로 쿼리를 실행해보면 서버와 동일한 결과를 볼 수 있습니다.
  3. 로그에만 의존하지 말고, 실제 바인딩 쿼리와 결과를 꼭 확인해보세요!

이런 현상은 실무에서 한 번쯤 꼭 마주치게 됩니다.

문제의 원인을 빠르게 파악하고, 위와 같은 방법으로 해결해보세요!

궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 댓글로 남겨주세요!

댓글