📕 목차
❤️ 학습 목표
- 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 상속
- 페이징 & 정렬 기능 제공
페이징 처리 절차
- PageRequest.of(...) 로 Pageable 생성
- JpaRepository 메서드에 Pageable 전달
- Page<T> 또는 Slice<T> 응답
- 응답 데이터로 로직 처리
페이징 요청/응답 클래스 - 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 등 타입 안전 쿼리 라이브러리 사용 권장
'Spring > 강의' 카테고리의 다른 글
[📕 JPA 심화] 5. SpringData JPA 심화 (2) | 2025.06.26 |
---|---|
[📕 JPA 심화] 3. RawJPA 기본 (1) | 2025.06.26 |
[📕 JPA 심화] 2. 데이터베이스 다루기 (2) | 2025.06.26 |
[📕 JPA 심화] 1. 프로젝트 세팅 (0) | 2025.06.26 |
[📘 심화 Spring] 3-4. N+1 문제 (0) | 2025.06.09 |