기본 콘텐츠로 건너뛰기

JPA와 Hibernate, Spring Data JPA를 헷갈리지 않게 구분하는 방법

JPA와 Hibernate, Spring Data JPA를 헷갈리지 않게 구분하는 방법

빠른 답

  • JPA는 표준 명세이고, Hibernate는 그 명세를 구현하는 ORM 엔진이다.
  • Spring Data JPA는 JPA 위에 Repository 추상화를 얹어 반복 코드를 줄여준다.
  • 현업의 흔한 조합은 Spring Data JPA + Hibernate이지만, 세 개는 같은 계층의 기술이 아니다.
  • 성능이나 쿼리 문제를 볼 때는 Repository 이름보다 결국 Hibernate가 만든 SQL과 JPA 설정을 함께 봐야 한다.

초안을 발행용 본문으로 재구성하되, 버전 민감한 부분은 공식 문서 기준으로 먼저 확인하겠습니다. 이후 비교 축, 실행 흐름, 현재 기준 migration 포인트가 초반부터 보이도록 문단과 예시를 정리하겠습니다.# JPA와 Hibernate, Spring Data JPA를 헷갈리지 않게 구분하는 방법

한눈에 비교

  • JPA: 무엇을 표준화하나가 핵심이다. EntityManager, 엔티티 매핑, JPQL, 영속성 컨텍스트, 엔티티 생명주기 같은 공통 규칙을 정의한다.
  • Hibernate: 어떻게 실행하나가 핵심이다. JPA 규약을 받아 SQL을 만들고, DB dialect를 적용하고, 캐시와 배치, flush, proxy, lazy loading을 처리한다.
  • Spring Data JPA: 어떻게 덜 쓰고 빨리 만들까가 핵심이다. JpaRepository, 메서드 이름 기반 쿼리, @Query, 페이지네이션, 정렬, Specification 같은 생산성 기능을 제공한다.
  • 교체 가능성도 다르다. JPA는 표준이라 남고, Hibernate는 구현체라 다른 provider로 교체할 수 있으며, Spring Data JPA는 그 위에서 동작하는 스프링 계층이다.
  • 문제를 추적하는 위치도 다르다. 매핑과 영속성 컨텍스트는 JPA 개념, SQL과 성능은 Hibernate 동작, 인터페이스 설계와 반복 제거는 Spring Data JPA 책임에 가깝다.

흐름으로 보기

1
서비스 계층 호출
2
Repository 메서드 해석
3
EntityManager 위임
4
Hibernate SQL 생성
5
DB 실행과 결과 매핑

실무에서는 이 흐름이 한 번에 이어져 보입니다. 개발자는 보통 JpaRepository만 직접 만지지만, 그 아래에서는 EntityManager가 영속성 컨텍스트를 관리하고, Hibernate가 실제 SQL을 생성해 데이터베이스와 통신합니다. 그래서 코드에서는 Spring Data JPA를 쓰고, 개념은 JPA를 따르며, 실행 결과는 Hibernate가 결정하는 구조가 됩니다.

왜 셋이 늘 한 묶음처럼 보일까

스프링 부트 프로젝트에서는 세 기술이 거의 동시에 등장합니다. 의존성은 spring-boot-starter-data-jpa로 시작하고, 엔티티에는 @Entity를 붙이며, 조회 코드는 JpaRepository로 작성하고, 로그에는 Hibernate SQL이 찍힙니다. 그러니 처음 보면 셋이 모두 같은 기술처럼 느껴지기 쉽습니다.

가장 흔한 시작점은 아래와 같습니다.

dependencies {
    implementation "org.springframework.boot:spring-boot-starter-data-jpa"
    runtimeOnly "com.mysql:mysql-connector-j"
}

이 한 줄로 끝나는 것처럼 보여도 실제로는 여러 층이 묶여 있습니다. Spring Data JPA가 Repository를 제공하고, JPA 인프라가 영속성 컨텍스트를 연결하며, 기본 provider로 Hibernate가 붙어 SQL을 실행합니다.

운영에서 가장 먼저 필요한 것은 “지금 누가 어떤 SQL을 만들고 있나”를 보는 설정입니다.

spring:
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate.format_sql: true

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.orm.jdbc.bind: trace

이 설정을 보면 감이 옵니다. 애플리케이션 코드에서는 Repository를 호출하지만, 실제 DB와 대화하는 층은 결국 Hibernate입니다. 성능 문제나 예상치 못한 쿼리가 나갈 때 Repository 이름만 보고 있으면 답이 잘 안 나오는 이유도 여기 있습니다.

JPA는 명세이고, Hibernate는 구현체이고, Spring Data JPA는 추상화다

이 문장을 단순 암기용 문장으로만 보면 금방 다시 헷갈립니다. 중요한 것은 “그래서 내가 무엇을 배우고 어디를 봐야 하느냐”입니다.

JPA를 공부한다는 것은 표준 계약을 이해하는 것입니다. 엔티티가 언제 영속 상태가 되는지, flush가 왜 필요한지, mergepersist가 어떻게 다른지, JPQL이 어떤 규칙으로 동작하는지를 이해하는 층입니다. 구현체를 Hibernate에서 EclipseLink로 바꾸더라도 이 개념은 그대로 살아남습니다.

Hibernate를 공부한다는 것은 실제 엔진의 동작을 이해하는 것입니다. 같은 JPQL이어도 어떤 SQL이 나가는지, 지연 로딩이 프록시로 어떻게 동작하는지, N+1이 왜 생기는지, 배치 insert가 언제 묶이는지, dialect 차이로 SQL이 어떻게 달라지는지 같은 문제는 Hibernate를 알아야 풀립니다.

Spring Data JPA를 공부한다는 것은 생산성 계층을 잘 쓰는 것입니다. 메서드 이름 기반 쿼리를 어디까지 믿어도 되는지, PageableSort를 어떻게 쓰는지, 단순 CRUD를 얼마나 줄일 수 있는지, 커스텀 Repository를 어디서 분리해야 하는지를 다룹니다.

실무 질문으로 바꾸면 구분이 더 쉬워집니다.

  • 영속성 컨텍스트와 엔티티 상태를 이해해야 한다면 JPA를 봐야 한다.
  • 왜 이런 SQL이 나가고 왜 느린지 봐야 한다면 Hibernate를 봐야 한다.
  • 반복 CRUD와 조회 인터페이스를 줄이고 싶다면 Spring Data JPA를 봐야 한다.

코드로 보는 역할 차이

가장 위층에서 많이 보는 코드는 JpaRepository입니다. 이 코드는 “반복 코드를 얼마나 줄여주는가”에 초점이 있습니다.

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByEmail(String email);

    List<Member> findByStatusOrderByIdDesc(MemberStatus status);
}

이 정도만 작성해도 기본 CRUD, 정렬, 페이지네이션, 메서드 이름 기반 쿼리 생성이 붙습니다. 작은 팀이나 일반적인 비즈니스 CRUD에서는 생산성이 아주 좋습니다.

하지만 복잡한 조회나 영속성 컨텍스트 제어가 필요해지면 JPA 표준 API인 EntityManager를 직접 다루게 됩니다.

@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {

    private final EntityManager em;

    public List<Member> findActiveMembers() {
        return em.createQuery("""
            select m
            from Member m
            where m.status = :status
            order by m.id desc
        """, Member.class)
        .setParameter("status", MemberStatus.ACTIVE)
        .getResultList();
    }

    public List<Member> loadByIds(List<Long> ids) {
        Session session = em.unwrap(Session.class);
        return session.byMultipleIds(Member.class).multiLoad(ids);
    }
}

여기서 위쪽 메서드는 JPA 표준 API를 사용하고, 아래쪽 메서드는 Hibernate 전용 API인 Session을 사용합니다. 이 차이가 중요합니다. EntityManager 중심 코드는 provider 교체 가능성을 더 열어두지만, Session을 직접 쓰기 시작하면 Hibernate에 더 강하게 결합됩니다.

또 하나 기억할 점이 있습니다. Spring Data JPA의 기본 구현체인 SimpleJpaRepository도 내부적으로 EntityManager를 받습니다. 즉 JpaRepository는 JPA를 대체하는 것이 아니라 JPA 위에 올라탄 더 편한 인터페이스입니다.

실무에서는 어떤 조합으로 쓰게 될까

대부분의 스프링 백엔드 애플리케이션은 다음 구조로 갑니다.

  • 엔티티 매핑은 JPA 애너테이션으로 작성한다.
  • 기본 provider는 Hibernate를 사용한다.
  • 애플리케이션 코드는 Spring Data JPA의 Repository로 작성한다.
  • 복잡한 조회나 성능 최적화가 필요할 때 EntityManager, JPQL, native query, Hibernate API를 함께 쓴다.

즉 “JPA vs Hibernate vs Spring Data JPA”는 셋 중 하나를 고르는 경쟁 구도가 아닙니다. 보통은 같이 쓰되, 각자 맡는 책임이 다릅니다. 이 점을 놓치면 용어도 헷갈리고 디버깅 위치도 잘못 잡게 됩니다.

예를 들어 findByEmail 같은 단순 조회는 Spring Data JPA가 매우 편합니다. 하지만 특정 시점에 flush를 제어해야 하거나, 배치 옵션을 다루거나, lazy loading과 fetch join 문제를 분석할 때는 결국 JPA와 Hibernate 이해가 필요합니다.

지금 기준에서 주의할 버전 포인트

이 주제는 오래된 글과 현재 문서가 쉽게 섞이므로 날짜를 기준으로 봐야 합니다. 2026년 4월 5일 기준 Spring Data JPA 현재 문서, Hibernate ORM Releases, Jakarta Persistence, Spring Boot 3.2.8 의존성 표, Spring Boot 4 의존성 표를 함께 보면 아래처럼 이해하는 것이 가장 안전합니다.

  • 현재 릴리스된 표준 명세는 Jakarta Persistence 3.2다. 예전의 Java Persistence API라는 이름이 사라진 것은 아니지만, 공식 패키지는 이제 jakarta.persistence다.
  • Spring Data JPA 현재 문서에는 stable 라인으로 4.0.43.5.10이 함께 보인다. 메이저 버전에 따라 지원 대상 스프링과 부트 라인이 다르다.
  • Hibernate ORM의 최신 stable 라인은 7.3이다. 다만 Spring Boot 4.0.x가 관리하는 Hibernate는 7.2.7.Final이고, Spring Boot 3.2.8은 hibernate-core 6.4.9.Finaljakarta.persistence-api 3.1.0을 관리한다.
  • 즉 “현재 Hibernate는 7.x”와 “내 프로젝트는 Boot 3.x라 Hibernate 6.x를 쓴다”는 말이 동시에 참일 수 있다. 블로그 글에서 이 둘을 구분하지 않으면 바로 헷갈린다.
  • 오래된 예제의 javax.persistence.* import는 Boot 2.x 시절 설명에 가깝다. Boot 3 이상에서는 jakarta.persistence.*로 옮겨야 한다.
  • 오래된 Spring Data JPA 예제에 자주 보이는 getOne()getById()는 현재 deprecated 흐름이다. 레퍼런스 조회가 필요하면 getReferenceById(), 실제 조회가 필요하면 findById()를 쓰는 편이 맞다.
  • 예전 Hibernate 자료에 보이던 HibernateEntityManager는 이미 오래전에 deprecated 되었고, Hibernate 5.2부터는 SessionEntityManager를 직접 확장하는 방향으로 정리되었다.
  • SQL 바인딩 로그도 세대 차이가 있다. Hibernate 5 시절 글은 BasicBinder 로거를 안내하는 경우가 많지만, Hibernate 6 이상에서는 org.hibernate.orm.jdbc.bind 같은 카테고리를 보는 편이 맞다.

이런 차이를 모르고 오래된 예제를 그대로 복사하면 가장 먼저 import에서 막히고, 그다음에는 메서드 deprecation이나 로그 설정에서 다시 막히게 됩니다.

Spring Data JPA를 써도 Hibernate를 알아야 하는 순간

Spring Data JPA는 편하지만 ORM의 비용을 없애주지는 않습니다. 특히 아래 문제는 Repository 계층만 보고는 원인을 찾기 어렵습니다.

  • 한 번 조회한 것 같은데 SQL이 여러 번 나가는 N+1 문제
  • 트랜잭션 끝에서 예상하지 않은 update가 나가는 dirty checking 문제
  • 트랜잭션 밖에서 연관 엔티티 접근 시 터지는 lazy loading 문제
  • deleteAllInBatch()처럼 빠르지만 1차 캐시, cascade, lifecycle 이벤트와 어긋날 수 있는 작업
  • DB마다 다른 dialect 때문에 native query가 환경별로 다르게 동작하는 문제

특히 배치 삭제 계열 메서드는 “빠르다”만 보고 쓰면 안 됩니다. Spring Data JPA API 설명에도 배치 삭제는 JPA 1차 캐시와 DB 상태를 어긋나게 만들 수 있고, cascade나 lifecycle 이벤트를 그대로 기대하면 안 된다는 주의가 붙어 있습니다.

실무에서 원인을 빨리 찾는 순서는 보통 이렇습니다.

  1. Repository 메서드가 어떤 JPQL 또는 SQL로 번역되는지 본다.
  2. 엔티티 매핑, 연관관계, fetch 전략, cascade 설정을 본다.
  3. Hibernate가 언제 flush하는지와 실제 SQL을 본다.
  4. 마지막으로 인덱스와 실행 계획을 본다.

이 순서를 익히면 “JPA가 느리다” 같은 뭉뚱그린 말 대신, “Spring Data JPA Repository는 단순했지만 Hibernate가 만든 SQL이 비효율적이었다”처럼 훨씬 정확하게 문제를 말할 수 있습니다.

언제 무엇을 기준으로 선택하면 될까

스프링 백엔드에서 일반적인 정답은 세 기술 중 하나만 택하는 것이 아니라, 어느 층까지 직접 다룰지를 선택하는 것입니다.

  • 빠른 CRUD와 일관된 개발 경험이 우선이면 Spring Data JPA부터 시작하는 것이 좋다.
  • provider 교체 가능성과 표준 계약을 중시하면 JPA 표준 API 중심으로 개념을 단단히 잡는 것이 좋다.
  • 배치, 캐시, SQL 생성, fetch 전략, 성능 최적화가 중요하면 Hibernate 동작을 반드시 이해해야 한다.
  • Hibernate 전용 API를 쓰기 시작했다면 vendor lock-in이 생긴다는 사실을 팀 차원에서 명확히 인식하는 편이 좋다.

결국 핵심은 이 한 문장으로 정리됩니다. JPA는 규칙, Hibernate는 실행 엔진, Spring Data JPA는 사용 편의 계층입니다. 셋을 같은 말로 뭉뚱그리지 않으면 개념도 훨씬 선명해지고, 장애와 성능 문제를 볼 때도 어디서부터 봐야 할지가 분명해집니다.

원문 참고

https://www.maeil-mail.kr/question/26

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...