개발 가이드 백엔드 MyBatis 쿼리 작성 가이드
최종 수정:

MyBatis 쿼리 작성 가이드

JPA 주 사용 원칙 및 MyBatis 복잡 쿼리 작성 규칙

MyBatis 쿼리 작성 가이드

기본 원칙

  • 이 프로젝트는 JPA를 주 ORM으로 사용한다.
  • MyBatis는 JPA로 표현하기 어려운 복잡한 동적 쿼리에만 사용한다.
  • MySQL 8.0 기준으로 쿼리를 작성한다.
  • SELECT * 금지 — 필요한 컬럼만 명시한다.

JPA vs MyBatis 사용 기준

상황권장
단순 CRUDJPA (Spring Data JPA)
조건부 동적 쿼리JPA Specification 또는 MyBatis
복잡한 JOIN + 집계MyBatis
네이티브 쿼리가 불가피한 경우MyBatis 또는 JPA @Query(nativeQuery=true)
대량 배치 INSERTMyBatis (성능 유리)

JPA 쿼리 작성

public interface NewsItemRepository extends JpaRepository<NewsItem, Long> {

    // Spring Data 메서드명 쿼리
    Optional<NewsItem> findByUrl(String url);
    boolean existsByUrl(String url);
    List<NewsItem> findByCategory(String category);

    // JPQL — 파라미터 바인딩 필수
    @Query("SELECT n FROM NewsItem n WHERE n.category = :category ORDER BY n.frstRegistDt DESC")
    List<NewsItem> findByCategory(@Param("category") String category);

    // 페이징 쿼리
    @Query("SELECT n FROM NewsItem n ORDER BY n.frstRegistDt DESC")
    Page<NewsItem> findAllPaged(Pageable pageable);
}

MyBatis XML 매퍼

<!-- src/main/resources/mapper/news/NewsMapper.xml -->
<mapper namespace="com.scraping.agent.domain.news.repository.NewsMapper">

    <!-- #{} 사용 필수 (PreparedStatement — SQL Injection 방지) -->
    <select id="findByKeyword" resultType="com.scraping.agent.domain.news.vo.NewsListRes">
        SELECT
            id,
            title,
            url,
            category,
            frst_regist_dt AS createdAt
        FROM news_items
        WHERE title LIKE CONCAT('%', #{keyword}, '%')
        ORDER BY frst_regist_dt DESC
        LIMIT #{size} OFFSET #{offset}
    </select>

    <!-- 동적 쿼리 — <if> 사용 -->
    <select id="findByFilter" resultType="com.scraping.agent.domain.news.vo.NewsListRes">
        SELECT
            id,
            title,
            url,
            category,
            frst_regist_dt AS createdAt
        FROM news_items
        <where>
            <if test="category != null and category != ''">
                AND category = #{category}
            </if>
            <if test="keyword != null and keyword != ''">
                AND title LIKE CONCAT('%', #{keyword}, '%')
            </if>
        </where>
        ORDER BY frst_regist_dt DESC
        LIMIT #{size} OFFSET #{offset}
    </select>

</mapper>

MyBatis 인터페이스

@Mapper
public interface NewsMapper {
    List<NewsListRes> findByKeyword(@Param("keyword") String keyword,
                                   @Param("size") int size,
                                   @Param("offset") int offset);

    List<NewsListRes> findByFilter(@Param("category") String category,
                                   @Param("keyword") String keyword,
                                   @Param("size") int size,
                                   @Param("offset") int offset);
}

MySQL 8.0 페이징

-- MySQL 페이징 (MyBatis)
SELECT id, title, url
FROM news_items
ORDER BY frst_regist_dt DESC
LIMIT #{size} OFFSET #{offset}
// Service에서 offset 계산
int offset = page * size;
newsMapper.findByFilter(category, keyword, size, offset);

JPA를 사용할 때는 Pageable을 활용한다:

Pageable pageable = PageRequest.of(page, size, Sort.by("frstRegistDt").descending());
Page<NewsItem> result = newsItemRepository.findAll(pageable);

SQL Injection 방지

<!-- 올바른 예 — #{} (PreparedStatement) -->
<select id="findByTitle" resultType="NewsItem">
    SELECT id, title, url FROM news_items
    WHERE title = #{title}
</select>

<!-- ${} 사용 시 — 반드시 화이트리스트 검증 후 전달 -->
<select id="findAllSorted" resultType="NewsItem">
    SELECT id, title, url FROM news_items
    ORDER BY ${sortColumn}  <!-- sortColumn은 Service에서 화이트리스트 검증 -->
</select>

화이트리스트 검증 예:

// Service에서 sortColumn 화이트리스트 검증
private static final Set<String> ALLOWED_SORT_COLUMNS =
        Set.of("frst_regist_dt", "title", "category");

public List<NewsListRes> findAllSorted(String sortColumn) {
    if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
        throw new IllegalArgumentException("허용되지 않은 정렬 컬럼: " + sortColumn);
    }
    return newsMapper.findAllSorted(sortColumn);
}

MyBatis 결과 매핑

<!-- snake_case → camelCase 자동 변환 설정 (application.yml) -->
<!--
mybatis:
  configuration:
    map-underscore-to-camel-case: true
-->

<!-- 자동 변환 시 resultMap 불필요 -->
<select id="findByKeyword" resultType="com.scraping.agent.domain.news.vo.NewsListRes">
    SELECT id, title, url, frst_regist_dt  <!-- → frstRegistDt (camelCase 자동 매핑) -->
    FROM news_items
    WHERE title LIKE CONCAT('%', #{keyword}, '%')
</select>

체크리스트

  • [ ] SELECT * 사용 금지 — 필요 컬럼만 명시
  • [ ] #{} 파라미터 바인딩 사용 (SQL Injection 방지)
  • [ ] ${} 사용 시 화이트리스트 검증 코드 포함
  • [ ] 페이징: LIMIT #{size} OFFSET #{offset}
  • [ ] 동적 쿼리: <where>, <if> 활용
  • [ ] Mapper 인터페이스에 @Mapper 선언
  • [ ] XML 파일 위치: src/main/resources/mapper/{도메인}/
AI 문서 검색

현재 페이지 내용을 기반으로 질문하세요.