공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 06 / 22

MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

4. 응용 변형 예제

변형 1: 방언별 매퍼 폴더 — 실무 프로젝트의 방식

조사한 프로젝트는 databaseId 대신 폴더를 나눕니다.

text
src/main/resources/mapper/
├── oracle/AT/AuditMapper.xml        ← 같은 namespace, 같은 id, SQL 만 다르다
├── maria/AT/AuditMapper.xml
└── tibero/AT/AuditMapper.xml
yaml
# application-oracle.yml
mybatis.mapper-locations: classpath:mapper/oracle/**/*.xml
# application-maria.yml
mybatis.mapper-locations: classpath:mapper/maria/**/*.xml
powershell
java -jar app.jar --spring.profiles.active=prd,oracle

코어 MyBatis 로 같은 일을 하려면 mybatis-config.xml 의 <mappers> 를 프로퍼티로 바꿉니다.

xml
<properties resource="jdbc.properties"/>                   <!-- db.vendor=oracle -->
<mappers>
    <mapper resource="mapper/${db.vendor}/MemberMapper.xml"/>
    <mapper resource="mapper/${db.vendor}/OrderMapper.xml"/>
</mappers>
databaseId (방법 A) 폴더 분리 (방법 B)
중복 다른 SQL 만 여러 벌. 공통 SQL 은 한 벌 파일 전체가 벤더 수만큼
검토 한 파일에 벤더별 SQL 이 섞여 DBA 가 읽기 불편 DBA 가 자기 벤더 폴더만 검토
실수 한 벤더 버전을 빼먹으면 기본(databaseId 없는) 버전이 조용히 실행 파일이 없으면 기동 시 statement 누락으로 즉시 실패
적합 방언 차이가 소수 statement 방언 차이가 넓고, 벤더별 납품

두 방법을 섞어, 공통 폴더 + 벤더 폴더를 둘 다 mapper-locations 에 넣고 벤더 폴더의 XML 이 같은 id 를 덮어쓰게 하는 변형도 있습니다. 단, MyBatis 는 같은 namespace.id 가 두 번 등록되면 예외를 내므로 공통 파일에서 해당 id 를 빼야 합니다.

변형 2: PageHelper 실제 사용법과 주의점

java
// pom.xml: com.github.pagehelper:pagehelper-spring-boot-starter:1.4.7
// application.yml: pagehelper.helper-dialect=oracle, pagehelper.reasonable=true, pagehelper.support-methods-arguments=true

// ① 기본형
PageHelper.startPage(pageNo, pageSize);                      // ThreadLocal 지시
List<Member> list = mapper.search(cond);                     // 바로 다음 조회에만 적용
PageInfo<Member> info = new PageInfo<>(list);                // total, pages, hasNextPage, navigatepageNums …

// ② 안전형: 조회 전에 예외가 나도 지시가 남지 않게
try {
    PageHelper.startPage(pageNo, pageSize);
    return new PageInfo<>(mapper.search(cond));
} finally {
    PageHelper.clearPage();
}

// ③ count 쿼리 직접 제공: 자동 생성된 SELECT COUNT(*) FROM (복잡한 SQL) 이 느릴 때
//    XML 에 id="search_COUNT" 를 만들면 PageHelper 가 자동 count 대신 이것을 쓴다
xml
<select id="search_COUNT" resultType="_long">
    SELECT COUNT(*) FROM member <include refid="searchWhere"/>
</select>

주의점 세 가지. 첫째, startPage 와 조회 사이에 다른 조회를 넣지 않습니다. 첫 조회에 적용되고 끝입니다. 둘째, startPage 뒤의 조회가 List 를 반환해야 합니다. 단건 selectOne 이나 count 에 걸리면 엉뚱한 결과가 나옵니다.

셋째, 트랜잭션 없는 컨트롤러에서 startPage 를 부르고 서비스 안에서 조회하는 구조는 스레드가 같으므로 동작하지만, 비동기(@Async, CompletableFuture)로 넘어가면 ThreadLocal 이 끊겨 적용되지 않습니다.

예제 4 의 인터셉터가 Page.CURRENT ThreadLocal 로 같은 구조라는 것을 알면 세 주의점이 전부 당연해집니다.

변형 3: 다중 INSERT — Oracle 과 MariaDB 의 문법 차이를 databaseId 로

xml
<!-- MariaDB / H2 기본: VALUES (...), (...), (...) -->
<insert id="insertAll" databaseId="maria">
    INSERT INTO member (id, name, email, grade, joined_at, balance, active_yn) VALUES
    <foreach collection="list" item="m" separator=",">
        (#{m.id}, #{m.name}, #{m.email}, #{m.grade}, #{m.joinedAt}, #{m.balance}, #{m.active})
    </foreach>
</insert>

<!-- Oracle: 다중 VALUES 문법이 없다. INSERT ALL INTO … SELECT 1 FROM DUAL -->
<insert id="insertAll" databaseId="oracle">
    INSERT ALL
    <foreach collection="list" item="m">
        INTO member (id, name, email, grade, joined_at, balance, active_yn)
        VALUES (member_seq.NEXTVAL, #{m.name}, #{m.email}, #{m.grade}, #{m.joinedAt}, #{m.balance}, #{m.active})
    </foreach>
    SELECT 1 FROM DUAL
</insert>

Oracle 의 INSERT ALL 에서 시퀀스 NEXTVAL 은 문장 전체에서 한 번만 평가되어 모든 행이 같은 id 를 받는 함정이 있습니다.

실무에서는 INSERT INTO … SELECT member_seq.NEXTVAL, … FROM (SELECT … FROM DUAL UNION ALL SELECT … FROM DUAL) 형태로 우회하거나, 애플리케이션에서 시퀀스를 미리 받아 #{m.id} 로 넘깁니다. 이 정도 차이가 쌓이면 변형 1 의 폴더 분리가 낫습니다.

어느 쪽이든 1000건 단위로 잘라 실행합니다. Oracle 은 바인드 변수 상한이 있고, 한 문장이 너무 커지면 파싱 시간이 실행 시간을 넘습니다. 대량이면 레슨 05 의 ExecutorType.BATCH 가 정답입니다.

변형 4: 슬로우 쿼리 로깅 인터셉터

java
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SlowQueryInterceptor implements Interceptor {
    private final long thresholdMs;
    public SlowQueryInterceptor(long thresholdMs) { this.thresholdMs = thresholdMs; }

    @Override public Object intercept(Invocation inv) throws Throwable {
        long t0 = System.nanoTime();
        try {
            return inv.proceed();
        } finally {
            long ms = (System.nanoTime() - t0) / 1_000_000;
            if (ms >= thresholdMs) {
                MappedStatement ms0 = (MappedStatement) inv.getArgs()[0];
                String sql = ms0.getBoundSql(inv.getArgs()[1]).getSql().replaceAll("\\s+", " ");
                System.err.printf("[SLOW %d ms] %s : %s%n", ms, ms0.getId(), sql);
            }
        }
    }
    @Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
    @Override public void setProperties(Properties p) { }             // <plugin> 의 <property> 로 threshold 를 받을 수도 있다
}
xml
<plugins>
    <plugin interceptor="SlowQueryInterceptor"><property name="thresholdMs" value="500"/></plugin>
    <plugin interceptor="PageInterceptor"/>
</plugins>

Executor 레벨은 statement id(MemberMapper.search)를 알 수 있어 "어느 매퍼의 어느 SQL 이 느린가"를 바로 찍습니다.

조사한 프로젝트는 Logback SiftingAppender 로 app 로그와 batch 로그를 파일로 나누는데, 이 인터셉터의 출력을 별도 로거(slow-sql)로 보내면 운영에서 튜닝 대상을 자동으로 모으는 슬로우 쿼리 로그가 됩니다. 플러그인은 등록 순서의 역순으로 감싸이므로 여러 개를 쓸 때 순서에 의미가 있습니다.

응용 변형 예제
  • 변형 1: 방언별 매퍼 폴더 — 실무 프로젝트의 방식
  • 변형 2: PageHelper 실제 사용법과 주의점
  • 변형 3: 다중 INSERT — Oracle 과 MariaDB 의 문법 차이를 databaseId 로
  • 변형 4: 슬로우 쿼리 로깅 인터셉터
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)