모든 SELECT 쿼리에 페이징 처리를 넣고 싶은데, 각 Mapper XML을 하나하나 수정해야 할까요?

MyBatis Plugin(Interceptor)을 사용하면 쿼리 실행 전후에 공통 로직을 끼워넣을 수 있습니다. SQL을 가로채서 수정하거나, 실행 시간을 측정하거나, 감사 로그를 남기는 등의 횡단 관심사를 한 곳에서 처리합니다.

개념 정의

MyBatis Plugin 은 MyBatis의 핵심 객체(Executor, StatementHandler, ParameterHandler, ResultSetHandler)의 메서드를 가로채는 인터셉터 입니다. 자바의 동적 프록시를 사용하여, 지정한 메서드 호출 전후에 커스텀 로직을 삽입합니다.

왜 필요한가

  • **자동 페이징 **: 모든 조회 쿼리에 LIMIT/OFFSET을 자동으로 추가합니다. Mapper마다 페이징 SQL을 작성하지 않아도 됩니다.
  • ** 슬로우 쿼리 로깅 **: 실행 시간이 임계치를 초과하는 쿼리를 자동으로 로그에 남깁니다.
  • ** 감사 로그 **: INSERT/UPDATE 시 created_by, updated_at 같은 공통 필드를 자동으로 채워넣습니다.

동작 원리

MyBatis가 쿼리를 실행하는 내부 흐름에서 Plugin이 개입하는 지점입니다.

가로챌 수 있는 객체역할대표 메서드
Executor쿼리 실행의 최상위update, query
StatementHandlerSQL 문 준비prepare, parameterize
ParameterHandler파라미터 바인딩setParameters
ResultSetHandler결과 매핑handleResultSets
  1. MyBatis가 Mapper 메서드를 호출받습니다.
  2. 내부적으로 Executor → StatementHandler → ParameterHandler → ResultSetHandler 순으로 처리합니다.
  3. Plugin이 등록되어 있으면, 해당 객체가 ** 동적 프록시로 래핑 **됩니다.
  4. 프록시가 원래 메서드 호출을 가로채서, Plugin의 intercept() 메서드를 먼저 실행합니다.

Plugin은 Spring AOP와 비슷한 개념입니다. 차이점은 가로채는 대상이 Spring 빈이 아니라 MyBatis 내부 객체 라는 것입니다.

구현 예제 — 슬로우 쿼리 로거

실행 시간이 500ms를 초과하는 쿼리를 경고 로그로 남기는 플러그인입니다.

JAVA
@Intercepts({
    @Signature(
        type = Executor.class,          // 가로챌 객체
        method = "query",               // 가로챌 메서드
        args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class})
})
public class SlowQueryPlugin implements Interceptor {

    private static final long THRESHOLD = 500L;

    @Override
    public Object intercept(Invocation invocation)
        throws Throwable {
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();  // ← 원래 쿼리 실행
        long elapsed = System.currentTimeMillis() - start;

        if (elapsed > THRESHOLD) {
            MappedStatement ms = (MappedStatement)
                invocation.getArgs()[0];
            log.warn("슬로우 쿼리 감지: {} ({}ms)",
                ms.getId(), elapsed);
        }
        return result;
    }
}

위 코드에서 @Intercepts@Signature가 "어떤 객체의 어떤 메서드를 가로챌 것인지" 선언합니다. invocation.proceed()가 원래 메서드를 실행하고, 그 전후에 시간을 측정합니다.

등록은 설정 파일에서 합니다.

XML
<plugins>
  <plugin interceptor="com.example.plugin.SlowQueryPlugin"/>
</plugins>

Spring Boot에서는 @Component로 등록하면 자동으로 인식됩니다.

JAVA
@Component
public class SlowQueryPlugin implements Interceptor {
    // ...
}

구현 예제 — 감사 필드 자동 채우기

INSERT/UPDATE 시 created_at, updated_at 필드를 자동으로 설정합니다.

JAVA
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class})
})
@Component
public class AuditPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation)
        throws Throwable {
        Object param = invocation.getArgs()[1];
        if (param instanceof Auditable entity) {
            entity.setUpdatedAt(LocalDateTime.now());
            MappedStatement ms = (MappedStatement)
                invocation.getArgs()[0];
            if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
                entity.setCreatedAt(LocalDateTime.now());
            }
        }
        return invocation.proceed();
    }
}

Auditable 인터페이스를 구현한 엔티티만 대상이 됩니다. INSERT일 때는 createdAt도 함께 설정하고, UPDATE일 때는 updatedAt만 갱신합니다.

주의할 점

플러그인 체이닝 순서

여러 Plugin을 등록하면 역순 으로 래핑됩니다. 먼저 등록된 Plugin이 가장 바깥 프록시가 됩니다.

PLAINTEXT
등록 순서: A → B → C
실행 순서: A.intercept → B.intercept → C.intercept → 원본 메서드

페이징 플러그인과 슬로우 쿼리 로거를 함께 쓴다면, 페이징을 먼저 등록해야 변경된 SQL의 실행 시간을 측정할 수 있습니다. 순서를 바꾸면 페이징 전의 원본 쿼리 시간을 측정하게 됩니다.

@Signature의 args 불일치

@Signatureargs 배열이 실제 메서드 시그니처와 정확히 일치해야 합니다. Executor.query()는 오버로딩이 2개 있는데, 4개 파라미터 버전과 6개 파라미터 버전이 다릅니다. 틀리면 Plugin이 등록은 되지만 가로채기가 동작하지 않아서 디버깅이 어렵습니다.

프로덕션에서 SQL 변경 플러그인의 위험

페이징 플러그인처럼 SQL 자체를 수정하는 Plugin은 강력하지만 위험합니다. 모든 쿼리에 적용되므로 서브쿼리, UNION, 복잡한 JOIN에서 예상치 못한 SQL이 생성될 수 있습니다. SQL 변경 플러그인은 반드시 다양한 쿼리 패턴으로 테스트해야 합니다.

정리

항목설명
Plugin이란MyBatis 내부 객체의 메서드를 가로채는 인터셉터
가로챌 수 있는 객체Executor, StatementHandler, ParameterHandler, ResultSetHandler
구현 방법Interceptor 인터페이스 구현 + @Intercepts/@Signature
체이닝 순서먼저 등록된 Plugin이 먼저 실행
대표 활용페이징, 슬로우 쿼리 로깅, 감사 필드 자동 채우기
주의 사항@Signature args 정확히 일치, SQL 변경 플러그인은 충분히 테스트
댓글 로딩 중...