[JPA] JPA 쿼리 (3)

2026. 3. 17. 21:29·Development/개발 공부

이번에는 "데이터 조회나 변경 같은 DB 작업을 자바 코드로 구현"하는 방식과, 해당 과정에서 주의해야 할 점에 대해 알아보도록 하겠다.

 

JPA로 쿼리 수행

원하는 데이터를 가져오려면 쿼리를 수행해야 하는데, JPA는 이러한 쿼리 수행을 자바 코드에서 처리할 수 있도록 도와준다.

기본적인 사용 방법은 다음과 같다.

@Repository
@Transactional
public class MemberJpaRepository {
	@PersistenceContext
	private EntityManager entityManager;
	
	// 행 삽입
	public void insert(Member member) {
		entityManager.merge(member); 
	}
	
	// 행 조회
	public Member findById(long id) {
		return entityManager.find(Member.class, id);
	}
	
	// 행 삭제
	public void deleteById(long id) {
		Member member = entityManager.find(Member.class, id);
		if (member != null) {
			entityManager.remove(member);
		}
	}
}

 

데이터를 조회, 삽입, 삭제할 수 있는 MemberJPARepository이다.

  • @PersistenceContext: 엔티티의 생명주기를 관리하는 EntityManager를 사용하기 위한 어노테이션
  • @Transactional: insert(), findById()와 같은 메소드를 트랜잭션 단위로 관리하여 데이터의 일관성을 보장하는 어노테이션
    ⇒ 뒤에서 더 자세히 설명

 

커스텀 쿼리 작성(JPQL, Query DSL)

원하는 데이터를 가져오기 위해서는, 하나의 엔티티만 사용해서는 조건에 맞는 데이터를 충분히 가져올 수 없는 경우가 많다.

JPQL

JPQL은 JPA에서 복잡한 쿼리를 실행할 수 있게 해주는 "객체지향 쿼리 언어"이다.

  • 하나의 엔티티만으로 쿼리를 수행할 수 없는 경우, 즉 두 개 이상의 테이블이 데이터 조회에 영향을 미치거나
  • 하나의 엔티티에 복잡한 조건이 필요할 때 사용한다.

사용 예시는 다음과 같다.

@Repository
@Transactional
public class MemberJpaRepository {
    @PersistenceContext
    private EntityManager entityManager;

    public List<Member> findAllByMinOrderPrice(int minPrice) {
        String jpql = "select m from Member m join fetch m.orders o where o.totalPrice >= :minPrice";

        return em.createQuery(jpql, Member.class)
                 .setParameter("minPrice", minPrice)
                 .getResultList();
    }
}

 

  • 실행하고자 하는 쿼리(jpql)를 em.createQuery()로 직접 실행한다.
  • em.createQuery()의 두 번째 파라미터인 Member.class는 쿼리 실행 결과로 반환될 엔티티의 타입을 의미한다.

JPQL 사용 시 유의할 점은 다음과 같다.

  • jpql 내 join 내에 fetch를 붙이면 N+1 문제를 해결할 수 있다.
    • join fetch를 사용하면 연관 관계의 FetchType이 LAZY로 설정되어 있더라도, 조회 시점에 EAGER 처럼 동작
      ⇒ 따라서 Lazy Loading 대상 연관 엔티티에 접근해도 LazyInitializationException이 발생하지 않는다.
    • 따라서 연관된 엔티티를 프록시(proxy)로 가져오는 것이 아니라, 실제 객체로 채워줌
  • JPQL는 컴파일 시점에는 오류 여부를 알 수 없기 때문에, 쿼리가 정확히 동작하도록 꼼꼼히 작성해야 한다.
  • 1:N 관계에서 join fetch를 사용할 경우 Paging을 올바르게 적용할 수 없다.
    • setFirstResult(), .setMaxResults()를 설정하여도 연관된 엔티티가 몇 개의 rows를 가져올 지 모르기 때문

 

QueryDSL

QueryDSL은 SQL 쿼리를 자바 코드로 작성하여 컴파일 시점에 오류를 검증할 수 있는 프레임워크다.

따라서 쿼리에 오류가 있다면 컴파일 시점에서 즉시 발견이 가능하다.

[사용 예시]

public void search() {
    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
    QMember member = QMember.member;
    QOrder order = QOrder.order;
    
	// 이름이 sujung인 member의 주문 내역 조회
    List<Order> result = queryFactory
            .selectFrom(order)
            .join(order.member, member)
            .where(
                member.username.eq("sujung"),
                order.status.eq(OrderStatus.ORDER)
            )
            .orderBy(order.orderDate.desc())
            .fetch();
}

@Transactional

@Transactional은 insert(), findById()와 같은 메소드를 트랜잭션 단위로 관리하여 데이터의 일관성을 보장하는 어노테이션이다.

 

트랜잭션의 시작 ~ 커밋하기 까지의 과정은 다음과 같다.

  • 메소드 시작 전
    • 트랜잭션 시작: DB 커넥션을 획득하여 트랜잭션을 시작한다.
    • 영속성 콘텍스트(Persistence Context) 생성: JPA의 영속성 콘텍스트를 생성한 후, 트랜잭션을 연결한다.
  • 메소드 실행 중
    • 비즈니스 로직 수행: JPA를 통해 엔티티를 조회 및 상태 변경을 수행한다.
  • 메소드 종료 후
    • 더티 체킹(Dirty Checking): JPA가 영속성 콘텍스트의 변경 사항을 확인(Dirty Checking)하여 flush() 호출
      • 변경 사항에 맞는 SQL쿼리를 쓰기 지연 SQL 저장소로 보냄
      • flush: 저장소에 있는 SQL들을 JDBC를 통해 DB로 전송하여 동기화하는 flush를 실행한다.
    • 트랜잭션 커밋: DB에 최종적으로 변경 내용을 커밋하여 영구적으로 반영한다.
  • 메소드 실행 중 예외 발생
    • 트랜잭션 롤백: Spring이 해당 트랜잭션을 롤백하여 DB에 아무 변경 사항도 반영되지 않도록 원상 복구한다.

 

@Transactional(readOnly=true)

@Transactional 어노테이션은 readOnly라는 읽기 모드가 존재한다.

만약 readOnly 속성을 true로 설정(default는 false)하면,

  • 해당 트랜잭션은 '읽기 전용' 모드로 동작하게 되며 이때는 더티 체킹(Dirty Checking)을 수행하지 않는다.
  • 이에 flush도 실행되지 않는다.

더티 체킹을 하지 않기 때문에, 엔티티의 초기 상태인 스냅샷을 생성할 필요가 없다.

따라서 해당 옵션이 true라면, 메모리를 절약할 수 있게 된다.

 

[권장 사용 방식]

클래스 레벨 → @Transactional(readOnly=true)를 기본으로 선언

클래스 내 조회 제외 삽입/갱신/삭제 메소드는→ 별도로 메소드 레벨에 @Transactional을 선언(쓰기 권한 부여)

 

Transactional 전파 속성

@Transactional 전파 속성은 트랜잭션을 실행할 때 이미 진행 중인 트랜잭션이 있을 경우, 어떻게 동작할 지 정의하는 속성이다.

전파 속성의 주요 3가지 종류이다.

  • REQUIRED(default): 진행 중인 트랜잭션이 있으면 그것을 사용하고, 없으면 새로운 트랜잭션을 시작
  • REQUIRES_NEW: 진행 중인 트랜잭션이 있으면 잠시 중단시키고, 새 트랜잭션을 시작
    • 새 트랜잭션을 커밋한 후에, 기존 트랜잭션을 재개함
    • 원래 트랜잭션에 영향을 주지 않는 독립적인 작업을 수행할 때 유용
      ex) 회원가입 중 오류가 나더라도, 오류 로그는 DB에 무조건 남겨야 할 경우
  • SUPPORTS: 진행 중인 트랜잭션이 있으면 그것을 사용하고, 없으면 트랜잭션 없이 실행

 

OSIV(Open Session in View) 옵션

OSIV 옵션은 JPA에서 트랜잭션이 끝난 후에도 영속성 콘텍스트(Persistence Context)를 유지하는 옵션이다.

JPA는 트랜잭션이 종료된 후, FetchType이 Lazy인 연관 엔티티에 접근하면 LazyInitializationException이 발생한다.

 

따라서 해당 옵션을 활성화시키면, 트랜잭션이 끝나도 Lazy Loading으로 인한 Exception이 발생하지 않는다.

옵션 설정은 application.properties에서 가능하다.

(default 값은 true)

(Lazy Exception이 발생하지 않는 경우는 위의 fetch join도 존재함을 잊지 말기를 ..~)


JPA에서의 equals()와 hashCode() 재정의

JPA 영속성 콘텍스트 내부에서는 같은 ID를 가진 엔티티를 조회하면 동일성(==)을 보장해준다.

하지만 다른 트랜잭션이라면 같은 레코드라도 동일성이 보장되지 않는다.

따라서 다른 영속성 콘텍스트의 동일 레코드를 같다고 비교하기 위해서는 equals()와 hashCode()를 재정의해야 한다.

 

엔티티 내 equals(), hashCode()는 다음과 같다.

public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 1
        if (!(o instanceof Member)) return false; // 2
        Member member = (Member) o;
        return getId() != null && Objects.equals(getId(), member.getId()); // 3
    }

    @Override
    public int hashCode() {
        return 31;
    }
}

 

[1] equals() 재정의

  1. 비교하고자 하는 엔티티의 주소값이 같으면 true를 리턴한다.
  2. 비교하고자 하는 엔티티가 Member 클래스가 아니면 false를 리턴한다.
    1) o.getClass()를 사용하지 않는 이유는 프록시 객체의 경우 Member 객체와 비교했을 때 false를 리턴하기 때문
    2) instanceOf는 프록시 객체여도 Member 객체와 비교했을 때 true를 리턴한다.
    (프록시 객체는 Member의 자식 클래스)
  3. id(PK) 값을 통해 두 엔티티가 같은지 최종적으로 비교한다.

[2] hashCode() 재정의

  1. 고정값 31을 리턴한다.

hashCode()에서 고정값을 리턴하는 이유는 다음과 같다.

DB에 저장하기 전 Set에 엔티티를 추가했을 때의 해시 값과,

DB에 저장한 후 해시 값이 서로 달라지면 데이터의 일관성이 깨지기 때문이다.

 

따라서 고정값 31이나 바뀌지 않는 필드를 해시 값으로 설정하는 것이 좋다.


 

지금까지 JPA 쿼리 수행 방식과 함께 다양한 옵션을 살펴보았다.

비즈니스 로직 뿐만 아니라, DB 관리까지 자바 코드로 제어할 수 있는 점이 JPA의 큰 장점인 것 같다!

다음엔 Spring Data JPA에 대해 알아보도록 하겠다..

 

👏

'Development > 개발 공부' 카테고리의 다른 글

[Docker] 도커 컴포즈(Docker Compose) & 볼륨(Docker Volume) (2)  (1) 2026.04.06
[JPA] 엔티티(Entity) 매핑 (2)  (0) 2026.03.11
[JPA] JPA(Java Persistence API)란? (1)  (0) 2026.02.08
[Git] 깃 브랜치 전략(Git Flow, Github Flow)(+TBD)  (0) 2025.12.14
AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)  (0) 2025.11.10
'Development/개발 공부' 카테고리의 다른 글
  • [Docker] 도커 컴포즈(Docker Compose) & 볼륨(Docker Volume) (2)
  • [JPA] 엔티티(Entity) 매핑 (2)
  • [JPA] JPA(Java Persistence API)란? (1)
  • [Git] 깃 브랜치 전략(Git Flow, Github Flow)(+TBD)
jjangsudiary
jjangsudiary
jjangsudiary 님의 블로그 입니다.
  • jjangsudiary
    jjangsudiary 님의 블로그
    jjangsudiary
  • 전체
    오늘
    어제
    • 분류 전체보기 (81) N
      • 이모저모 (0)
        • 회고 (0)
      • Development (17) N
        • 개발 공부 (14) N
        • 프로젝트 (2)
      • Android (10)
        • Compose (1)
      • AI (15)
      • Computer Science (25)
        • 네트워크 (8)
        • 데이터베이스 (10)
        • 운영체제 (6)
        • 자료구조 (0)
        • 컴퓨터구조 (1)
      • Java (9)
        • 디자인패턴 (2)
      • Spring (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    baekjoon
    운영체제
    TensorFlow
    Ai
    db
    CS
    java
    머신러닝
    인공지능
    자바
    database
    파이썬
    android
    os
    프로그래머스
    Python
    코딩 테스트
    백준
    딥러닝
    안드로이드
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
jjangsudiary
[JPA] JPA 쿼리 (3)
상단으로

티스토리툴바