JPA 객체 지향 쿼리 언어

유기훈·2025년 3월 9일

JPQL 기본 사용법

JPQL은 테이블 대상으로 쿼리하는 것이 아니라, 엔티티 객체를 대상으로 쿼리한다.

  • 엔티티와 속성은 대소문자 구분 O
  • JPQL 키워드는 대소문자 구분 X (SELECT, from)
  • 별칭 필수 (as는 생략가능)
select m from Student as s where s.age < 18

파라미터 바인딩

파라미터 바인딩 할 때는 아래와 같이 작성한다. 파라미터를 넣는 방법은 :{변수명} 을 작성하면된다. 아래 예시 코드는 Spring Data JPA 인데, @Query에 쿼리를 작성하고, 메서드 파라미터로 쿼리에 들어가는 파라미터의 변수명을 입력하면된다.

public interface QuizRepository extends JpaRepository<Quiz, Long>, QuizRepositoryCustom {

    @Modifying(clearAutomatically = true)
    @Query("update Quiz q set q.status = :status where q.id in :quizIds")
    void changeStatus(@Param(value = "quizIds") List<Long> quizIds, @Param(value = "status") QuizStatus status);

}

프로젝션

JPA 프로젝션(Projection)이란 JPQL 또는 QueryDSL을 사용하여 엔티티의 전체가 아닌 일부 필드만 조회하는 방식을 의미한다.

즉, 특정 컬럼만 선택해서 조회하는 것

예를 들어, User 엔티티에서 id, name만 조회하고 싶을 때 사용한다.

프로젝션 종류

엔티티 프로젝션 (Entity Projection)

엔티티 전체를 조회하는 방식

List<User> users = em.createQuery("SELECT u FROM User u", User.class)
                      .getResultList();

필드 기반 프로젝션 (Scalar Projection)

특정 필드만 조회하는 방식

  • 단일 필드만 조회
  • 엔티티가 아닌 원시 값(Scalar Value)을 가져오기 때문에 영속성 컨텍스트에 관리되지 않음
List<String> names = em.createQuery("SELECT u.name FROM User u", String.class)
                        .getResultList();

여러 필드를 함께 조회하는 방식

  • 여러 필드를 조회하면 Object[] 배열로 반환됨
List<Object[]> results = em.createQuery("SELECT u.name, u.email FROM User u")
                           .getResultList();
for (Object[] row : results) {
    String name = (String) row[0];
    String email = (String) row[1];
}

DTO 기반 프로젝션 (DTO Projection)

JPQL에서 DTO를 직접 매핑하는 방식

  • new 패키지명.DTO클래스명(필드1, 필드2, ...) 형식으로 DTO를 직접 매핑.
  • 필드 개수가 많아지면 생성자 매개변수 개수가 많아져 유지보수가 어려울 수 있음
List<UserDTO> users = em.createQuery(
    "SELECT new com.example.dto.UserDTO(u.name, u.email) FROM User u", 
    UserDTO.class)
    .getResultList();

서브쿼리

서브쿼리에 사용할 수 있는 JPQL 함수

JPQL에서는 SQL에서 사용되는 서브쿼리를 작성할 때 다음과 같은 함수들을 사용할 수 있다.

EXISTS (존재 여부 확인)

  • 특정 조건을 만족하는 데이터가 존재하는지 확인할 때 사용
SELECT u FROM User u 
WHERE EXISTS (
    SELECT o FROM Order o WHERE o.user = u AND o.status = 'DELIVERED'
)

IN (서브쿼리 결과와 비교)

  • 서브쿼리 결과 중 하나라도 일치하면 TRUE
SELECT u FROM User u 
WHERE u.id IN (
    SELECT o.user.id FROM Order o WHERE o.status = 'CANCELED'
)

ALL (모든 결과와 비교)

  • 서브쿼리 결과의 모든 값과 비교
  • = ALL, <= ALL 같은 형태로 사용 가능

SELECT u FROM User u 
WHERE u.age >= ALL (
    SELECT u2.age FROM User u2 WHERE u2.status = 'ACTIVE'
)

ANY (하나라도 만족하면 TRUE)

  • 서브쿼리 결과 중 하나라도 만족하면 TRUE
  • = ANY, <= ANY, = ANY 형태로 사용 가능

SELECT p FROM Product p 
WHERE p.price < ANY (
    SELECT p2.price FROM Product p2 WHERE p2.category = 'ELECTRONICS'
)

SOME (ANY와 동일)

  • SOME은 ANY와 동일한 의미로 사용됨
  • 대부분 ANY를 사용하므로 SOME은 잘 사용되지 않음

서브쿼리의 제한 사항

  1. JPQL은 SELECT 절에서 서브쿼리를 지원하지 않는다.
SELECT (SELECT AVG(u.age) FROM User u) FROM User u2  -- 불가능!
  1. FROM 절에서 서브쿼리 사용 불가능
SELECT u FROM (SELECT * FROM User WHERE age > 20) AS subquery -- 불가능!

JPQL 기본 함수

• CONCAT
• SUBSTRING
• TRIM
• LOWER, UPPER
• LENGTH
• LOCATE
• ABS, SQRT, MOD
• SIZE, INDEX(JPA 용도)

조인

명시직 조인, 묵시적 조인

명시적 조인: join 키워드 직접 사용

select m from Member m join m.team t

묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생
(내부 조인만 가능)

select m.team from Member m

묵시적 조인은 가급적 사용하지 말자

  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

✅ fetch join은 따로 정리

벌크 연산

JPA 기본 적으로 dirty checking 을 활용해서 update를 처리한다. 그런데 엄청 많은 데이터를 일괄적으로 변경하려고 하면 데이터 하나에 update 쿼리 하나가 나가기 때문에 성능적으로 좋지 않다. 이럴 때 사용하는 게 벌크 연산이다.

벌크 연산 종류

JPA에서 벌크 연산을 수행하는 방법은 크게 두 가지가 있다.

@Modifying + @Query (Spring Data JPA)

Spring Data JPA에서는 @Modifying 어노테이션을 사용하여 JPQL을 직접 작성하여 벌크 연산을 수행할 수 있다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    @Modifying
    @Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date")
    int deactivateOldUsers(@Param("date") LocalDateTime date);

    @Modifying
    @Query("DELETE FROM User u WHERE u.status = 'DELETED'")
    int deleteInactiveUsers();
}

EntityManager를 사용한 벌크 연산

Spring Data JPA 없이 EntityManager를 직접 사용하여 벌크 연산을 수행할 수도 있다.

@Transactional
public void bulkUpdateStatus(EntityManager entityManager) {
    Query query = entityManager.createQuery(
        "UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date"
    );
    query.setParameter("date", LocalDateTime.now().minusMonths(6));
    int updatedCount = query.executeUpdate();
    System.out.println("비활성화된 사용자 수: " + updatedCount);
}

벌크 연산의 주의점

벌크 연산은 영속성 컨텍스트를 무시하고 직접 DB에 반영하기 때문에 몇 가지 주의해야 할 점이 있다.

벌크 연산 후 영속성 컨텍스트 초기화

벌크 연산은 영속성 컨텍스트를 거치지 않고 DB에 바로 적용되므로, 영속성 컨텍스트에 남아 있는 엔티티 정보와 불일치할 수 있다.
따라서 벌크 연산 후 clear()를 호출하여 영속성 컨텍스트를 초기화하는 것이 좋다.

@Modifying
@Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date")
int deactivateOldUsers(@Param("date") LocalDateTime date);

@Transactional
public void updateUserStatus() {
    int count = userRepository.deactivateOldUsers(LocalDateTime.now().minusMonths(6));
    entityManager.clear();  // 영속성 컨텍스트 초기화
}

clearAutomatically 옵션

Spring Data JPA에서 @Modifying을 사용할 때, 벌크 연산 후 자동으로 영속성 컨텍스트를 초기화하고 싶다면 다음과 같이 설정할 수 있다.

@Modifying(clearAutomatically = true)
@Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date")
int deactivateOldUsers(@Param("date") LocalDateTime date);
  • flushAutomatically 옵션도 있음. (벌크 연산 실행 전에 영속성 컨텍스트를 먼저 flush해서 변경 사항을 DB에 반영)

@Transactional 사용 여부
Spring Data JPA에서 @Modifying을 사용하면 기본적으로 트랜잭션이 필요함.

profile
개발 블로그

0개의 댓글