[Spring Boot] JPA, Spring Data JPA, QueryDSL의 이해

2026. 5. 16. 09:00·Spring
 

JDBC(Java Database Connectivity)

JPA를 이해하기 위해서는 JPA가 나오게 된 배경부터 이해해야 한다. 이를 이해하기 위해서는 먼저 JDBC에 대해서 알아야 한다.

JDBC는 자바에서 제공하는 API로 DB에 접근하는 가장 기본적인 방법이며, 이는 DB에 연결하고 SQL을 직접 작성하고 실행하는 방식이다.

 

예시 코드

다음 코드는 JDBC를 활용한 회원 조회 코드이다. 회원 하나를 조회하는 코드임에도 DB 연결, SQl 작성 등 반복되는 코드가 매우 많다. 테이블이 30개라면 이 패턴이 30번 반복된다.

Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM member WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();

if (rs.next()) {
    Member member = new Member();
    member.setId(rs.getLong("id"));
    member.setName(rs.getString("name"));
    member.setEmail(rs.getString("email"));
    return member;
}
rs.close(); ps.close(); conn.close();
무엇보다 자바는 객체 지향 언어고, DB는 테이블 기반이라는 패러다임 불일치다. 자바에서는 객체가 다른 객체를 참조하는 방식으로 관계를 표현하지만, DB는 외래 키와 JOIN으로 관계를 표현한다. 이 간극을 매번 개발자가 직접 변환 코드로 메워야 했다.

JPA(Java Persistence API) - 객체와 테이블 자동 매핑

JPA(Java Persistence API)는 이 문제를 해결하기 위해 등장한 자바 표준 ORM 스펙이다. ORM은(Object-Relational Mapping)자바 객체와 DB 테이블을 자동으로 매핑해주는 기술이다.
JPA는 스펙(인터페이스)이고, 실제 구현체는 Hibernate가 사실상 표준으로 사용된다. 개발자는 JPA 인터페이스를 사용해서 코드를 작성하고, 내부적으로 Hibernate가 동작한다.

자바 표준 ORM 스펙

✔️ 스펙은 "이렇게 만들어야 한다"는 규칙으로 인터페이스만 정의한 것으로, 실제 동작하는 구현 코드는 없다. 자바 표준이라는 건 Oracle(자바 관리 주체)이 공식으로 정한 규칙이라는 뜻이다.

Hibernate

JPA 스펙을 실제로 구현한 라이브러리이다. JPA가 "객체와 테이블을 매핑하고, save()와 find()를 만들어" 라고 규칙을 정해놓으면, Hibernate가 그 규칙에 맞춰 실제 SQL을 생성하고 실행하는 코드를 구현한 것이다.

즉, JPA는 규칙이며 Hibernate는 실제로 내부적으로 실행하는 구현체이다. Spring Boot 프로젝트에서 data-jpa 의존성을 추가하면 JPA와 Hibernate가 같이 들어온다.
✔️개발자 - JPA 인터페이스로 코드 작성
em.persist(member);  // JPA

✔️ 내부에서 Hibernate가 실제 SQL을 만들어 실행
INSERT INTO member (name, email) VALUES (?, ?)

Entity - 객체와 테이블 연결

JPA에서는 DB 테이블과 1:1로 대응하는 자바 클래스를 엔티티라고 부른다. 애노테이션으로 어떤 테이블의 어떤 컬럼과 매핑되는지 선언한다.

예시 코드

@Entity
@Getter
@Setter
@NoArgsConstructor
public class UserEntity {

    @Id
    private String id;
    private String pass;
    private String name;
    private int gender;
    private String tel;
    private String email;
    private String picture;

    public UserEntity(UserDto dto) {
        this.id = dto.getId();
        this.pass = dto.getPass();
        this.name = dto.getName();
        this.gender = dto.getGender();
        this.tel = dto.getTel();
        this.email = dto.getEmail();
        this.picture = dto.getPicture();
    }
}

EntityManager - JPA의 핵심 인터페이스

JPA에서 DB 작업은 EntityManager를 통해 수행된다. 저장, 조회, 수정, 삭제 모두 EntityManager가 담당한다.
✔️ 저장 - INSERT SQL 자동 생성
em.persist(member);

✔️ 조회 - SELECT SQL 자동 생성
Member member = em.find(Member.class, id);

✔️ 수정 - 변경 감지(Dirty Checking)로 UPDATE 자동 실행
member.setName("newUserName");

🎈 트랜잭션 커밋 시점에 UPDATE SQL이 자동으로 실행

 

✔️ 변경 감지 (Dirty Checking)
JPA는 트랜잭션 안에서 조회한 엔티티의 최초 상태를 스냅샷으로 기억한다. 트랜잭션이 커밋될 때 현재 상태와 스냅샷을 비교해서 변경된 필드가 있으면 자동으로 UPDATE SQL을 실행한다. 개발자가 명시적으로 save()를 호출하지 않아도 된다.

JPQL(Java Persistence Query Language) - 객체 대상 쿼리

JPQL은 단순 조회가 아닌 조건이 있는 쿼리를 작성할때 사용한다. SQL과 문법이 거의 같지만, 테이블명 대신 엔티티 클래스명을, 컬럼명 대신 필드명을 사용한다.
✔️ SQL:   SELECT * FROM member WHERE name = ?

✔️ JPQL:  엔티티 클래스명과 필드명을 사용

List<User> result = em.createQuery(
    "SELECT u FROM User u WHERE u.name = :name", User.class)
    .setParameter("name", name)
    .getResultList();

Spring Data JPA - CRUD 자동화 기술

JPA만 써도 JDBC보다 훨씬 편해졌지만, 모든 Repository 클래스마다 save(), findById(), findAll(), delete() 같은 기본 CRUD 코드를 직접 구현해야 한다. 엔티티가 10개면 같은 패턴의 코드가 10번 반복되지만, Spring Data JPA는 이 반복을 없애준다. 

JpaRepository 인터페이스를 상속하는 것만으로 기본 CRUD 메서드를 모두 자동으로 제공받는다.
public interface SpringDataJpaUserRepository extends JpaRepository<UserEntity, String> {

}

✔️ 자동제공
memberRepository.save(user);           
memberRepository.findById(id);           
memberRepository.findAll();              
memberRepository.delete(user);

메서드 이름 규칙에 따른 쿼리 자동 생성

Spring Data JPA의 강력한 기능 중 하나는 메서드 이름 규칙에 따라 쿼리를 자동으로 만들어주는 것이다. SQL을 작성하지 않아도 된다.
public interface UserRepository extends JpaRepository<Member, Long> {

    ✔️ SELECT * FROM user WHERE name = ?
    List<User> findByName(String name);

    ✔️ SELECT * FROM user WHERE email = ? AND name = ?
    Optional<User> findByEmailAndName(String email, String name);

    ✔️ SELECT * FROM user WHERE name LIKE ?
    List<User> findByNameContaining(String keyword);

    ✔️ SELECT COUNT(*) FROM user WHERE name = ?
    long countByName(String name);
}

내부 동작 원리

Spring Data JPA는 애플리케이션이 시작될 때 인터페이스의 메서드 이름을 파싱해서 JPQL을 생성하고, 그 JPQL을 기반으로 실제 동작하는 프록시 구현체를 만든다. 개발자가 직접 구현 코드를 작성하지 않아도 되는 이유다.

QueryDSL(Query Domain Specific Language ) - 복잡한 동적 쿼리를 자바 코드로 타입 안전하게

DSL은 특정 목적을 위해 만들어진 언어라는 뜻으로 QueryDSL은 쿼리 작성이라는 목적에 특화된 자바 라이브러리다.
엔티티 클래스 기반으로 Q클래스를 자동 생성하고, 이를 통해 타입 안전한 쿼리 작성이 가능하며 동적 쿼리를 한 개의 메서드로 처리할 수 있다.

등장 배경

Spring Data JPA의 메서드 이름 기반 쿼리는 단순한 조건에서는 편리하지만, 조건이 복잡해지면 한계가 드러난다.

예를 들어 상품 검색 페이지에 이름, 가격 범위, 카테고리, 재고 여부 조건이 있다고 가정하자. 각 조건은 입력할 수도 있고 안 할 수도 있다. 이런 동적 쿼리를 메서드 이름으로 표현하면 조건 조합 수만큼 메서드가 폭발적으로 늘어난다.

JPQL은 문자열로 쿼리를 작성한다. -> " SELECT u User u WHERE u.name = :user"

❗ 오타가 있어도 컴파일 시점에 잡히지 않음 -> 런타임 에러 발생
❗동적 쿼리 생성 어려움

String query = "SELECT u FROM User u WHERE 1=1";
if (name != null) query += " AND u.name = :name";
if (age != null) query += " AND u.age = :age"

Spring Data JPA만 사용

✔️ 조건 조합마다 메서드 생성 필요

findByName
findByNameAndPrice
findByNameAndPriceAndCategory
findByPriceAndCategory
findByCategory

예시 코드

✔️ 타입이 안전한 이유
문자열로 쿼리를 작성하는 JPQL은 오타가 있어도 컴파일 시점에 잡히지 않고 런타임에 에러가 난다. QueryDSL은 자바 코드로 쿼리를 작성하기 때문에, 오타를 치면 컴파일 에러가 즉시 발생한다. 필드명이 바뀌면 컴파일러가 바로 알려준다.
@Repository
public class UserRepositoryImpl {

    private final JPAQueryFactory queryFactory;

    public List<User> search(String name, Integer minPrice, String category) {

        QUser user = QUser.user;

        return queryFactory
            .selectFrom(user)
            .where(
                nameEq(name),           // null이면 조건 무시
                priceGoe(minPrice),     // null이면 조건 무시
                categoryEq(category)    // null이면 조건 무시
            )
            .fetch();
    }

    private BooleanExpression nameEq(String name) {
        return name != null ? user.name.eq(name) : null;
    }

    private BooleanExpression priceGoe(Integer minPrice) {
        return minPrice != null ? user.price.goe(minPrice) : null;
    }

    private BooleanExpression categoryEq(String category) {
        return category != null ? user.category.eq(category) : null;
    }
}

 


정리

  • JPA는 자바 객체와 DB 테이블을 매핑해주는 ORM 표준 스펙이다. 구현체는 Hibernate.
  • Spring Data JPA는 JPA 위에서 반복되는 CRUD를 자동화한다. JpaRepository 상속만으로 기본 메서드를 모두 제공함
  • QueryDSL은 Spring Data JPA로 처리하기 어려운 복잡한 동적 쿼리를 자바 코드로 타입 안전하게 작성할 수 있게 해준다.
  • 세 기술은 경쟁 관계가 아니라 계층적으로 연결된 역할 분담 관계다.
  • 실무에서는 Spring Data JPA + QueryDSL 조합이 사실상 표준이다.

세 기술의 계층 구조

1. 애플리케이션 코드(Service)가 Repository 호출
2. 단순한 쿼리 - Spring Data JPA / 복잡한 동적 쿼리 - QueryDSL이 처리
    ↳ 두 기술 모두 내부적으로 JPA/Hibernate를 통해 JPQL로 변환되고, 최종적으로 SQL이 생성되어 DB에 전달

 

'Spring' 카테고리의 다른 글

[Spring] Spring Security의 기초 이해  (0) 2026.05.19
[Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃)  (0) 2026.05.13
[Spring Boot] 테스트의 이해와 @Transactional 동작 원리  (0) 2026.05.12
[Spring] - AOP(관점 지향 프로그래밍)의 이해  (0) 2026.04.28
[Spring] - BCryptPasswordEncoder를 이용한 비밀번호 암호화와 보안 원리  (1) 2026.04.16
'Spring' 카테고리의 다른 글
  • [Spring] Spring Security의 기초 이해
  • [Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃)
  • [Spring Boot] 테스트의 이해와 @Transactional 동작 원리
  • [Spring] - AOP(관점 지향 프로그래밍)의 이해
mins0on
mins0on
비전공자의 백엔드 개발자 공부 기록 일지입니다.
  • mins0on
    꾸준함의 가치
    mins0on
  • 전체
    오늘
    어제
    • 분류 전체보기 (65) N
      • Java (7)
      • Spring (9)
      • DataBase (1)
      • Algorithm (1)
      • Network (6)
      • 운영체제 (2)
      • 코드 분석 (26)
      • Trouble Shooting (4) N
      • Project (1)
      • Migration (3)
      • 기타 (1)
      • 개념 정리 (3)
      • Coding Test (1)
        • Baekjoon (1)
  • hELLO· Designed By정상우.v4.10.6
mins0on
[Spring Boot] JPA, Spring Data JPA, QueryDSL의 이해
상단으로

티스토리툴바