프로젝트를 시작할 때 "JPA 쓸까, MyBatis 쓸까" 고민은 누구나 한 번쯤 해봤을 겁니다. 어떤 기준으로 선택하면 나중에 후회하지 않을까요?

이 글은 MyBatis 핸드북의 마지막 글로서, MyBatis의 관점에서 JPA와의 차이를 정리합니다. 어느 쪽이 "더 좋다"가 아니라, 프로젝트 특성에 따라 어느 쪽이 더 적합한가 에 초점을 맞춥니다.

핵심 비교표

기준JPAMyBatis
철학객체 중심 → SQL 자동 생성SQL 중심 → 결과를 객체에 매핑
SQL 제어프레임워크가 생성 (JPQL, Querydsl)개발자가 직접 작성
학습 곡선높음 (영속성 컨텍스트, 프록시, N+1)낮음 (SQL + XML/어노테이션)
단순 CRUD매우 빠름 (메서드명만으로 쿼리 생성)SQL을 일일이 작성해야 함
복잡한 쿼리Querydsl 필요, 한계 있음네이티브 SQL 자유롭게
성능 튜닝추상화 뒤에 숨어서 직관적이지 않음SQL이 그대로 보여서 직관적
캐싱1차/2차 캐시 내장직접 구현 또는 Plugin
DB 스키마 의존엔티티 기반, 스키마 변경에 민감SQL 기반, 스키마 자유도 높음

한 줄 요약: **JPA는 "객체를 저장한다"는 관점 **, MyBatis는 "SQL을 실행한다"는 관점 입니다.

MyBatis가 빛나는 순간

레거시 DB를 다룰 때

기존 시스템의 DB가 객체지향적으로 설계되지 않은 경우가 많습니다.

PLAINTEXT
실무에서 흔한 레거시 DB 특징:
  - PK가 복합키 (3개 이상 컬럼 조합)
  - 테이블명이 약어 (TB_ORD_DTL, TB_USR_MST)
  - 정규화되지 않은 컬럼 (하나의 컬럼에 구분자로 여러 값 저장)
  - 뷰(View)를 많이 사용
  - 프로시저/함수 호출이 필요

JPA는 엔티티와 테이블의 매핑을 전제로 합니다. 복합키, 비정규화 테이블, 뷰 등을 JPA 엔티티로 표현하려면 @IdClass, @SecondaryTable, @Subselect 같은 고급 기능을 동원해야 하고, 그래도 깔끔하지 않습니다.

MyBatis는 SQL과 ResultMap으로 어떤 형태의 테이블이든 바로 매핑할 수 있습니다. 테이블 구조에 대한 가정이 없기 때문입니다.

복잡한 리포트/통계 쿼리

다중 조인, 서브쿼리, 윈도우 함수, PIVOT 등이 들어가는 통계 쿼리는 JPQL이나 Querydsl로 표현하기 어렵습니다.

SQL
-- MyBatis XML에 그대로 작성 가능
SELECT d.dept_name,
       COUNT(e.id) AS headcount,
       AVG(e.salary) AS avg_salary,
       RANK() OVER (ORDER BY AVG(e.salary) DESC) AS salary_rank
FROM departments d
JOIN employees e ON d.id = e.dept_id
WHERE e.status = 'ACTIVE'
GROUP BY d.dept_name
HAVING COUNT(e.id) >= 5

JPA에서 이 쿼리를 실행하려면 네이티브 쿼리(@Query(nativeQuery=true))를 써야 합니다. 그렇다면 JPA의 장점인 타입 안전성과 영속성 컨텍스트 관리를 포기하는 것이므로, 처음부터 MyBatis를 쓰는 편이 자연스럽습니다.

DBA와의 협업

금융, 공공 등 DBA가 SQL을 직접 검수하는 조직에서는 MyBatis가 유리합니다.

PLAINTEXT
JPA 환경에서의 DBA 협업:
  개발자: "이 쿼리 검수해주세요"
  DBA: "쿼리가 어디 있나요?"
  개발자: "Hibernate가 생성해서... 로그에서 확인해야 해요"
  DBA: "..."

MyBatis 환경에서의 DBA 협업:
  개발자: "이 XML 파일 검수해주세요"
  DBA: "오, SQL이 바로 보이네요. 여기 인덱스 추가하면 좋겠어요"

DBA가 XML 파일을 직접 열어서 SQL을 확인하고 튜닝 포인트를 짚어줄 수 있습니다. JPA 환경에서는 실행 로그에서 SQL을 추출하는 과정이 추가됩니다.

JPA가 빛나는 순간

공정한 비교를 위해, JPA가 더 적합한 경우도 정리합니다.

  • ** 도메인 주도 설계(DDD)**: 엔티티 간 연관관계를 코드로 표현하고, 변경 감지로 상태 변경을 자동 추적하는 JPA의 철학이 DDD와 잘 맞습니다.
  • ** 단순 CRUD 위주 서비스 **: JpaRepository의 메서드명 쿼리 생성이 압도적으로 빠릅니다. findByStatusAndCreatedAtAfter() 한 줄이면 SQL 작성, ResultMap 정의가 모두 생략됩니다.
  • ** 빠른 프로토타이핑 **: 스키마가 자주 바뀌는 초기 단계에서 spring.jpa.hibernate.ddl-auto=update로 테이블을 자동 생성/변경할 수 있습니다.

선택 기준 플로차트

PLAINTEXT
프로젝트 시작 시 질문:

1. DB 스키마를 직접 설계할 수 있는가?
   ├── YES → 2번으로
   └── NO (레거시 DB) → MyBatis

2. 쿼리 복잡도가 높은가? (다중 조인, 통계, 프로시저)
   ├── YES → MyBatis
   └── NO → 3번으로

3. DBA가 SQL을 직접 검수하는 조직인가?
   ├── YES → MyBatis
   └── NO → 4번으로

4. 도메인 모델이 복잡한가? (연관관계 많음, 상태 변경 추적 필요)
   ├── YES → JPA
   └── NO (단순 CRUD) → JPA

함께 쓰는 전략

실무에서는 둘을 함께 쓰는 경우도 있습니다.

계층기술용도
기본 CRUDJPA (Spring Data JPA)엔티티 저장, 수정, 단순 조회
복잡한 조회MyBatis통계, 리포트, 검색
배치 처리MyBatis대량 INSERT, 벌크 UPDATE

주의할 점은 ** 같은 트랜잭션 안에서 JPA와 MyBatis를 섞을 때** 영속성 컨텍스트와 MyBatis의 SQL이 서로의 변경을 인식하지 못한다는 것입니다. JPA의 flush()를 명시적으로 호출하여 영속성 컨텍스트의 변경을 DB에 반영한 후 MyBatis 쿼리를 실행해야 정합성이 보장됩니다.

주의할 점

"MyBatis는 구식이다"라는 오해

MyBatis가 오래된 기술이라는 인식이 있지만, 2024년 기준으로도 활발히 유지보수되고 있고, 한국과 일본의 엔터프라이즈 프로젝트에서 여전히 높은 점유율을 차지합니다. "구식"이 아니라 "SQL 중심 접근"이라는 ** 다른 철학 **입니다.

JPA를 쓰면서 네이티브 쿼리만 잔뜩 쓰는 안티패턴

JPA를 선택했는데 복잡한 쿼리가 많아 @Query(nativeQuery=true)를 남발하게 되면, JPA의 장점(타입 안전성, 변경 감지, 캐시)을 포기하면서 단점(학습 비용, 설정 복잡도)만 안게 됩니다. 네이티브 쿼리 비율이 30%를 넘어간다면, 해당 모듈은 MyBatis로 전환하는 것이 나을 수 있습니다.

MyBatis에서 도메인 로직이 SQL에 스며드는 문제

MyBatis를 쓰다 보면 비즈니스 로직이 자바 코드가 아닌 SQL에 들어가는 경향이 있습니다. CASE WHEN으로 상태를 판단하고, 서브쿼리로 집계를 하면서 SQL이 비대해집니다. SQL은 데이터 접근에 집중하고, 비즈니스 로직은 자바 코드에서 처리하는 원칙을 지켜야 합니다.

정리

상황추천이유
레거시 DB, 복합키, 비정규화MyBatis테이블 구조에 대한 가정 없음
복잡한 통계/리포트 쿼리MyBatis네이티브 SQL 자유롭게 작성
DBA SQL 검수 필요MyBatisXML에서 SQL 바로 확인 가능
단순 CRUD + 도메인 복잡JPA메서드명 쿼리, 변경 감지, 연관관계
빠른 프로토타이핑JPADDL 자동 생성, 개발 속도
둘 다 필요JPA + MyBatis 혼용CRUD는 JPA, 복잡한 조회는 MyBatis
댓글 로딩 중...