Spring/강의

[📕 JPA 심화] 4. SpringData JPA 기본

가지코딩 2025. 6. 26. 15:18

📕 목차

  1. 테이블 객체 다루는 법
  2. 테이블 객체로 자동 쿼리 생성하기
  3. 테이블 객체로 페이지 조회하기
  4. 테이블 객체로 수동 쿼리 생성하기

❤️ 학습 목표

  • Spring Data JPA를 사용하여 엔티티 객체를 효율적으로 관리하는 방법을 배웁니다. 리포지터리 인터페이스의 역할과 기본적인 메소드 사용법을 설명합니다.
  • JpaRepository 인터페이스를 활용하여 CRUD 작업을 자동으로 처리하는 방법을 배웁니다. 메소드 이름만으로 쿼리를 생성하는 방법과 그 장점을 학습합니다.
  • 페이지네이션을 사용하여 대량의 데이터를 효율적으로 조회하는 방법을 배웁니다. Spring Data JPA에서 제공하는 Pageable 인터페이스를 사용하는 실습을 통해 데이터 처리 성능을 최적화하는 방법을 학습합니다.
  • JPQL을 사용하여 복잡한 쿼리를 수동으로 작성하고 실행하는 방법을 배웁니다. 커스텀 쿼리를 통해 표준 SQL에서 지원하지 않는 기능을 구현하는 방법을 배웁니다.

1. 테이블 객체 다루는 법

Cascade (영속성 전이)

  • 목적: 부모 엔티티의 연산을 자식 엔티티로 전이시켜 한 번의 작업으로 함께 처리
  • 주요 사용 위치: @OneToMany, @OneToOne 관계의 부모 엔티티
  • 조건: 부모-자식 엔티티의 생명주기가 유사할 때만 설정
  • 주요 옵션
    • ALL: 모든 작업 전이
    • PERSIST: 저장 전이
    • REMOVE: 삭제 전이
    • MERGE: 병합 전이
    • REFRESH: 갱신 전이
    • DETACH: 영속성 컨텍스트 분리 전이

 

orphanRemoval (고아 객체 제거)

  • 목적: 부모 엔티티에서 자식 객체를 컬렉션에서 제거했을 때, 자동으로 DB에서도 삭제
  • 사용 위치: @OneToMany, @OneToOne 관계의 부모 엔티티
  • Cascade.REMOVE 와 차이점
    • Cascade.REMOVE: 부모 삭제 시 자식 삭제
    • orphanRemoval = true: 컬렉션에서 자식 제거 시 자식 삭제

 

Fetch (조회 시점 설정)

  • 목적: 연관된 엔티티를 조회할 시점을 설정하여 성능 최적화
  • 옵션
    • EAGER: 즉시 로딩 (조회 시 연관 엔티티도 즉시 로딩)
    • LAZY: 지연 로딩 (필요할 때 조회, 기본값)
  • 권장 전략
    • 기본은 LAZY
    • 실제 사용 시점에서 fetch join 활용

 

 

영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL

⭢  위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다.


2. 테이블 객체로 자동 쿼리 생성하기

JpaRepository 구조 및 등록 원리

  • JpaRepository = CrudRepository + PagingAndSortingRepository + JPA 기능
  • 내부적으로 SimpleJpaRepository 구현체가 Spring Boot 실행 시 자동 등록됨
  • 자동 등록은 @EnableJpaRepositories → JpaRepositoriesRegistrar → ImportBeanDefinitionRegistrar 를 통해 수행됨

 

기본 사용법

  • 인터페이스만 작성하면 CRUD, 페이징, 정렬 메서드 자동 생성됨
public interface UserRepository extends JpaRepository<User, Long> {
}

 

 

쿼리 메서드 규칙

  • 접두어: find, get, read, count 등
  • 조건절: By + 필드명
  • 연산자: And, Or, Between, Like, GreaterThan, ...
  • 정렬: OrderBy + 필드명 + Asc|Desc
  • 반환형: Entity, List<Entity>, Optional, Page, Slice, Stream
List<User> findByNameAndEmail(String name, String email);
Optional<User> findByNameIgnoreCase(String name);
Page<User> findByStatus(String status, Pageable pageable);

3. 테이블 객체로 페이지 조회하기

페이징 기능 구조

  • JpaRepository → PagingAndSortingRepository 상속
  • 페이징 & 정렬 기능 제공

 

페이징 처리 절차

  1. PageRequest.of(...) 로 Pageable 생성
  2. JpaRepository 메서드에 Pageable 전달
  3. Page<T> 또는 Slice<T> 응답
  4. 응답 데이터로 로직 처리

 

페이징 요청/응답 클래스 - Pageable

 

요청 : org.springframework.data.domain.Pageable

  • 페이징을 제공하는 중요한 인터페이스이다.
// Pageable 만드는법
PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들
// Pageable 메서드
pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환

 

응답: org.springframework.data.domain.Page

  • 페이징의 findAll() 의 기본적인 반환 메서드로 여러 반환 타입 중 하나이다.  
// Paging 응답
{
    "content": [
        {"id": 1, "username": "User 0", "address": "Korea", "age": 0},
        ...
        {"id": 5, "username": "User 4", "address": "Korea", "age": 4}
    ],
    "pageable": {
        "sort": {
            "sorted": false, // 정렬 상태
            "unsorted": true,
            "empty": true
        },
        "pageSize": 5, // 페이지 크기
        "pageNumber": 0, // 페이지 번호 (0번 부터 시작)
        "offset": 0, // 해당 페이지의 첫번째 요소의 전체 순번 (다음 페이지에서는 5)
        "paged": true,
        "unpaged": false
    },
    "totalPages": 20, // 페이지로 제공되는 총 페이지 수
    "totalElements": 100, // 모든 페이지에 존재하는 총 원소 수
    "last": false,  // 마지막 페이지 여부
    "number": 0,
    "sort": {
        "sorted": false,    // 정렬 사용 여부
        "unsorted": true,
        "empty": true
    },
    "size": 5,       // Contents 사이즈
    "numberOfElements": 5,  // Contents 의 원소 수
    "first": true,   // 첫페이지 여부
    "empty": false   // 공백 여부
}

 

 

 

페이지 반환 타입

  • Page<T>
    • 전체 개수(totalElements) 포함
    • 게시판 형태에 적합
    • 카운트 쿼리 발생
  • Slice<T>
    • 다음 페이지 여부만 포함 (hasNext)
    • 더보기 형태에 적합
    • 카운트 쿼리 없음 (limit + 1 방식)
  • List<T>
    • 전체 목록 보기
    • 단순 쿼리, count 미포함

 

 

정렬 기능 정리

 

컬럼 값으로 정렬하기

  • Sort 클래스를 사용해서 정렬 조건 생성 가능
  • 여러 컬럼 다중 정렬 가능
Sort sort1 = Sort.by("name").descending();     // name 내림차순
Sort sort2 = Sort.by("password").ascending();  // password 오름차순
Sort sortAll = sort1.and(sort2);                // 다중 정렬 결합
Pageable pageable = PageRequest.of(0, 10, sortAll); // pageable 생성 시 적용

 

컬럼이 아닌 값으로 정렬하기 (Alias 기준)

  • @Query에서 조회 시 Alias(별칭)를 지정하여 정렬 가능
@Query("SELECT u.user_name, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
List<User> users = findByUsername("user", Sort.by("user_password"));

 

SQL 함수 기준 정렬하기

  • JpaSort.unsafe() 메서드로 SQL 함수를 정렬 조건으로 지정 가능
@Query("SELECT u FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
List<User> users = findByUsername("user", JpaSort.unsafe("LENGTH(password)"));

4. 테이블 객체로 수동 쿼리 생성하기

JPQL (Java Persistence Query Language)

  • Entity 객체 기준으로 작성하는 객체 지향 쿼리 언어
  • SQL과 문법은 유사하지만, 테이블이 아닌 엔티티 필드명을 기준으로 작성
  • 실행 방법: EntityManager.createQuery() 또는 @Query 어노테이션

 

EntityManager를 통한 수동 JPQL 작성

  • setParameter("키", 값) 형태로 변수 바인딩
  • 런타임 오류 위험: 문자열 기반 → 오타/오류 발생 시 컴파일러가 잡지 못함
String qlString = "select u from User u where u.username = :username";

User user = em.createQuery(qlString, User.class)
              .setParameter("username", "teasun")
              .getSingleResult();

 

 

@Query를 이용한 수동 JPQL 선언

  • Entity명, Entity 필드명으로 작성해야 함
  • 변수 바인딩 2가지 방식:
    • ?1, ?2 등 위치 기반
    • :변수명 등 이름 기반
public interface UserRepository extends JpaRepository<User, Long> {
  
  // 방법 1: ?숫자 (위치 기반)
  @Query("SELECT u FROM User u WHERE u.username = ?1")
  List<User> findByUsername(String username);

  // 방법 2: :이름 (이름 기반)
  @Query("SELECT u FROM User u WHERE u.username = :username")
  List<User> findByUsername(@Param("username") String username);
}

 

 

문자열 쿼리 사용의 단점

  • 오타 발생 가능성 높음
  • 공통 문자열 변경 시 일괄 변경 어려움
  • 컴파일 시점 오류 감지 불가 (→ 런타임 오류 발생 위험)
  • 디버깅이 어렵고, 유지보수 비용 증가

→ 해결책:

  • 상수 클래스를 만들어 공통 키워드 관리
  • QueryDSL 등 타입 안전 쿼리 라이브러리 사용 권장