# JPA Repository

스프링 부트에서 `JpaRepository`는 `Spring Data JPA` 모듈의 핵심 인터페이스로, 데이터베이스와의 상호작용을 추상화하여 CRUD(Create, Read, Update, Delete) 작업과 쿼리 작성을 간소화합니다. `JpaRepository`는 JPA(Java Persistence API)를 기반으로 동작하며, 엔티티(Entity)와 데이터베이스 테이블 간의 매핑을 통해 객체 지향적으로 데이터를 다룰 수 있게 해줍니다. 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 메서드 이름 규칙(Query Method)이나 `@Query` 어노테이션을 사용해 데이터베이스 작업을 수행할 수 있습니다.

## JpaRepository의 특징
- **상속 구조**: `JpaRepository`는 `CrudRepository`와 `PagingAndSortingRepository`를 확장하며, 기본 CRUD 메서드와 페이징/정렬 기능을 모두 제공합니다.
- **자동 구현**: 인터페이스를 정의하기만 하면 스프링이 런타임에 구현체를 생성합니다.
- **쿼리 생성**: 메서드 이름 규칙을 통해 자동으로 쿼리를 생성하거나, `@Query`로 커스텀 쿼리 정의 가능.
- **트랜잭션 관리**: 기본적으로 읽기 메서드는 읽기 전용 트랜잭션, 쓰기 메서드는 트랜잭션 내에서 실행됩니다.

## JpaRepository 기본 사용법  

### JpaRepository 인터페이스 생성  
JpaRepository를 사용하려면, **Repository 인터페이스를 생성하고 JpaRepository를 상속**하면 됩니다.  

```java
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}
```
✔ `JpaRepository<User, Long>` → **User 엔티티를 관리하며, 기본 키(ID)의 타입은 Long**  
✔ `save()`, `findById()`, `deleteById()` 등의 기본 메서드를 자동으로 제공  


## JpaRepository의 기본 제공 메서드  

JpaRepository를 상속하면 기본적으로 제공되는 메서드는 다음과 같습니다.  

| 메서드 | 설명 |
|--------|------|
| `save(T entity)` | 엔티티를 저장 또는 업데이트 |
| `findById(ID id)` | ID로 엔티티 조회 (Optional 반환) |
| `findAll()` | 모든 엔티티 조회 |
| `findAll(Sort sort)` | 정렬 조건을 적용하여 모든 엔티티 조회 |
| `deleteById(ID id)` | ID로 엔티티 삭제 |
| `delete(T entity)` | 엔티티 삭제 |
| `count()` | 엔티티 개수 조회 |
| `existsById(ID id)` | 특정 ID의 엔티티 존재 여부 확인 |

* `save()`는 **ID가 있으면 업데이트, 없으면 삽입**  
* `findById()`는 **Optional<T> 반환 → `orElseThrow()`와 함께 사용 가능**  


## 메서드 이름을 이용한 자동 쿼리 생성  

`Spring Data JPA`는 메서드 이름을 분석해 쿼리를 자동 생성합니다. 아래 표는 주요 키워드와 그 설명을 정리한 것입니다. 메서드 이름은 일반적으로 `find`, `exists`, `count`, `delete` 등의 동사로 시작하며, 조건과 옵션을 조합합니다.

### 메서드 이름 규칙  

| **키워드** | **설명** |
|----------------------|----------------|
| `find` | 조건에 맞는 엔티티를 조회 (단일 또는 리스트 반환)  `findByName(String name)` |   
| `findDistinct` | 중복 제거 후 조회                                                                          `findDistinctByName(String name)` |
| `exists` | 조건에 맞는 엔티티가 존재하는지 확인 (boolean 반환)                                         `existsByName(String name)` |
| `count` | 조건에 맞는 엔티티 수를 반환                                                               `countByAgeGreaterThan(int age)` |
| `delete` | 조건에 맞는 엔티티 삭제                                                                     `deleteByName(String name)` |
| `By` | 조건을 시작하는 구분자                                                                     `findByName(String name)` |
| `And` | 여러 조건을 AND로 결합                                                                     `findByNameAndAge(String name, int age)`|
| `Or` | 여러 조건을 OR로 결합                                                                      `findByNameOrAge(String name, int age)` |
| `Is`, `Equals` | 특정 필드가 값과 동일한 경우                                                               `findByNameIs(String name)` |
| `Not` | 특정 필드가 값과 다른 경우                                                                 `findByNameNot(String name)` |
| `LessThan` | 특정 필드가 값보다 작은 경우                                                               `findByAgeLessThan(int age)` |
| `LessThanEqual` | 특정 필드가 값보다 작거나 같은 경우                                                        `findByAgeLessThanEqual(int age)` |
| `GreaterThan` | 특정 필드가 값보다 큰 경우                                                                 `findByAgeGreaterThan(int age)` |
| `GreaterThanEqual` | 특정 필드가 값보다 크거나 같은 경우                                                        `findByAgeGreaterThanEqual(int age)` |
| `Between` | 특정 필드가 두 값 사이에 있는 경우                                                         `findByAgeBetween(int start, int end)` |
| `Like` | 특정 필드가 패턴과 일치하는 경우 (SQL의 LIKE)                                              `findByNameLike(String pattern)` |
| `NotLike` | 특정 필드가 패턴과 일치하지 않는 경우                                                      `findByNameNotLike(String pattern)` |
| `StartingWith` | 특정 필드가 값으로 시작하는 경우                                                           `findByNameStartingWith(String prefix)` |
| `EndingWith` | 특정 필드가 값으로 끝나는 경우                                                             `findByNameEndingWith(String suffix)` |
| `Containing` | 특정 필드가 값을 포함하는 경우                                                             `findByNameContaining(String part)` |
| `In` | 특정 필드가 값 목록에 포함된 경우                                                          `findByAgeIn(List<Integer> ages)` |
| `NotIn` | 특정 필드가 값 목록에 포함되지 않은 경우                                                   `findByAgeNotIn(List<Integer> ages)` |
| `True` | 특정 boolean 필드가 `true`인 경우                                                          `findByActiveTrue()` |
| `False` | 특정 boolean 필드가 `false`인 경우                                                         `findByActiveFalse()` |
| `OrderBy` | 결과를 특정 필드로 정렬                                                                    `findByNameOrderByAgeAsc(String name)` |
| `Asc` | 오름차순 정렬                                                                              `findByNameOrderByAgeAsc(String name)` |
| `Desc` | 내림차순 정렬                                                                              `findByNameOrderByAgeDesc(String name)` |
| `First`, `Top` | 결과 중 처음 N개만 반환 (숫자 생략 시 1개)                                                 `findFirstByName(String name)` |
| `FirstN`, `TopN` | 결과 중 처음 N개 반환                                                                      `findTop10ByName(String name)` |
| `Page` | 페이징된 결과 반환 (`Page<T>` 타입)                                                        `findByName(String name, Pageable pageable)` |
| `Slice` | 페이징된 결과의 일부 반환 (`Slice<T>` 타입, 전체 카운트 제외)                               `findByName(String name, Pageable pageable)` |


#### 단일 필드로 조회  
```java
List<User> findByUsername(String username);
```
✔ `SELECT * FROM user WHERE username = ?`  


#### 여러 조건으로 조회 (AND 조건)  
```java
List<User> findByUsernameAndEmail(String username, String email);
```
✔ `SELECT * FROM user WHERE username = ? AND email = ?`  


#### 여러 조건으로 조회 (OR 조건)  
```java
List<User> findByUsernameOrEmail(String username, String email);
```
✔ `SELECT * FROM user WHERE username = ? OR email = ?`  


#### 특정 값보다 큰 데이터 조회  
```java
List<User> findByAgeGreaterThan(int age);
```
✔ `SELECT * FROM user WHERE age > ?`  


#### 특정 값 포함 여부 조회  
```java
List<User> findByUsernameLike(String keyword);
```
✔ `SELECT * FROM user WHERE username LIKE ?`  


#### 정렬 추가 (`OrderBy`)  
```java
List<User> findByUsernameOrderByAgeDesc(String username);
```
✔ `SELECT * FROM user WHERE username = ? ORDER BY age DESC`  


## 페이징 및 정렬 처리 

Spring Data JPA는 **페이징(Pageable)과 정렬(Sort)** 기능을 기본으로 제공합니다.  

- **페이징 주요 용어**:
  - **Page**: 현재 페이지 번호 (0부터 시작).
  - **Size**: 한 페이지에 포함될 데이터 개수.
  - **Total Elements**: 전체 데이터 개수.
  - **Total Pages**: 전체 페이지 수.

- **장점**:
  - 데이터베이스 부하 감소.
  - 사용자 경험 개선 (한 번에 모든 데이터를 로드하지 않음).

- **정렬 주요 용어**:
  - **Sort**: 정렬 기준과 방향을 정의.
  - **Direction**: `ASC` (오름차순), `DESC` (내림차순).

- **`Pageable`**:
  - 페이징과 정렬 정보를 담는 인터페이스.
  - `PageRequest.of(page, size)`: 기본 페이징 설정.
  - `PageRequest.of(page, size, sort)`: 정렬 포함 페이징 설정.

- **`Sort`**:
  - 정렬 조건을 정의.
  - `Sort.by(Direction, "fieldName")`: 단일 필드 정렬.
  - `Sort.by(Sort.Order.asc("field1"), Sort.Order.desc("field2"))`: 다중 필드 정렬.

- **`Page<T>`**:
  - 페이징된 결과를 반환하며, 전체 페이지 수, 총 요소 수 등 메타데이터 포함.
  - 주요 메서드: `getContent()`, `getTotalElements()`, `getTotalPages()`.

- **`Slice<T>`**:
  - 전체 카운트를 포함하지 않은 페이징 결과 (다음 페이지 존재 여부만 확인 가능).
  - 주로 무한 스크롤에 사용.

```java
Page<User> findByName(String name, Pageable pageable);
```
```java
Pageable pageable = PageRequest.of(0, 10, Sort.by("age").ascending());
Page<User> users = userRepository.findByName("John", pageable);
```

### 페이징을 위한 `Pageable` 인터페이스 사용  
페이징을 적용하려면 `Pageable` 객체를 매개변수로 전달하면 됩니다.  

```java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findByAgeGreaterThan(int age, Pageable pageable);
}
```

✔ `Page<T>`를 반환하여 총 페이지 개수, 현재 페이지 정보 등을 확인 가능  


### 페이징 및 정렬을 컨트롤러에서 사용하기  
컨트롤러에서 `PageRequest.of(페이지번호, 개수, 정렬기준)`을 사용하여 페이징을 요청할 수 있습니다.  

```java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public Page<User> getUsers(@RequestParam(defaultValue = "0") int page,
                               @RequestParam(defaultValue = "10") int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("age").descending());
        return userRepository.findAll(pageable);
    }
}
```
✔ `PageRequest.of(page, size, Sort.by("age").descending())` → **나이 기준 내림차순 정렬**  
✔ 클라이언트에서 `GET /users?page=0&size=10` 요청 시 첫 번째 페이지의 10개 데이터 반환  

## 커스텀 쿼리
메서드 이름으로 표현하기 어려운 쿼리는 `@Query` 사용:
```java
@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age > ?2")
List<User> findCustomUsers(String name, int age);
```

