Java와 Kotlin 애플리케이션이 관계형 DB에 접근하는 도구는 층으로 쌓여 있다. 맨 아래에 JDBC가 있고, 그 위에 JPA나 MyBatis 같은 도구를 하나 골라 얹는다. 두 도구의 가장 큰 차이는 SQL을 누가 쓰느냐다.
애플리케이션 코드
└ JPA(Hibernate) 또는 MyBatis ← 여기서 고름
└ JDBC ← 항상 깔려 있음
└ JDBC 드라이버(mysql-connector-j) → MySQLJDBC
JDBC(Java Database Connectivity)는 DB에 연결해 SQL을 보내고 결과를 받는 Java 표준 API다. 가장 낮은 층이라 연결을 열고, SQL을 준비하고, 결과 행을 하나씩 읽어 객체에 옮기는 일을 모두 손으로 한다.
dataSource.connection.use { conn ->
conn.prepareStatement("SELECT id, status FROM orders WHERE member_id = ?").use { stmt ->
stmt.setLong(1, memberId)
stmt.executeQuery().use { rs ->
while (rs.next()) {
// 결과 행을 객체로 옮기는 일을 직접 함
orders += Order(id = rs.getLong("id"), status = OrderStatus.valueOf(rs.getString("status")))
}
}
}
}JPA를 쓰든 MyBatis를 쓰든 실제로 DB와 말하는 것은 결국 JDBC다. JDBC는 DB 종류와 상관없는 인터페이스이고, MySQL과 실제로 통신하는 구현은 JDBC 드라이버가 맡는다. 그래서 Spring Boot 프로젝트에서 spring-boot-starter-data-jpa를 넣어도 mysql-connector-j는 따로 넣는다.
MyBatis와 JPA
| MyBatis | JPA(Hibernate) | |
|---|---|---|
| SQL | 개발자가 직접 씀 | 대부분 JPA가 만들어 줌 |
| 자동으로 해 주는 일 | 결과 행을 객체로 옮기기 | 객체 저장, 조회, 변경 감지까지 |
| 강점 | 나가는 SQL을 정확히 통제 | 객체를 다루듯 저장, 조회 |
| 약점 | SQL을 매번 씀 | 어떤 SQL이 나가는지 코드에서 안 보임 |
MyBatis는 SQL 매퍼(SQL mapper)다. SQL은 XML이나 어노테이션에 직접 쓰고, 결과를 객체에 옮기는 일만 대신한다. 복잡한 조회나 SQL 튜닝이 중요한 곳에서 선호된다.
JPA는 ORM(Object-Relational Mapping)이다. DB는 데이터를 행과 열의 표로 다루고 코드는 객체로 다루는데, 이 둘 사이의 변환을 대신한다. 엔티티 클래스와 테이블을 한 번 매핑해 두면 repository.save(order) 한 줄이 INSERT SQL이 된다. 매핑 방법은 JPA 엔티티 매핑에 정리했다.
JPA는 표준 규격(인터페이스)이고, Hibernate는 그 규격의 구현체다. Spring Data JPA는 JPA 위에 리포지토리 인터페이스를 더한 라이브러리다.
JPA가 숨기는 것
편한 대신 실제로 어떤 SQL이 언제 나가는지가 코드에 드러나지 않는다. 목록을 조회한 뒤 각 항목의 연관 객체를 읽을 때마다 쿼리가 하나씩 더 나가는 N+1 문제가 대표적이다. 그래서 JPA를 쓸 때도 spring.jpa.show-sql이나 SQL 로그로 실제 SQL을 확인하는 습관이 필요하다. 쓰기 SQL이 나가는 시점은 JPA 쓰기 시점과 영속성 컨텍스트에 정리했다.
다른 ORM과 비교하면, TypeScript의 Prisma나 TypeORM, 이 블로그의 MikroORM이 JPA와 같은 자리에 있는 도구다.