JPA

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
spring data jpa 사용을 위해 의존성 추가
ORM(Object Relational Mapping)
ORM은 Object Relational Mapping의 약자로, 객체 지향 프로그래밍 언어(예: Java)에서 사용하는 객체(엔티티)와 관계형 데이터베이스(RDB)에서 사용하는 테이블 간의 불일치를 해결하고, 이 둘을 자동으로 매핑해주는 기술입니다. 즉, 자바의 객체와 데이터베이스의 테이블을 1:1로 연결해주는 중계자 역할을 하며, 개발자가 객체를 조작하면 그에 맞춰 자동으로 데이터베이스에 반영되도록 해줍니다
@GeneratedValue는 JPA(Java Persistence API)에서 엔티티 클래스의 주요 키(Primary Key) 값을 자동으로 생성하기 위해 사용하는 어노테이션입니다. 주로 @Id와 함께 사용되어, 데이터베이스에 엔티티를 저장할 때 해당 필드의 값을 자동으로 생성하도록 지시합니다
엔티티 정의
package com.example.bookmanager.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
| 전략명 | 설명 | 주요 사용 DBMS | 언제 사용해야 하는가 |
| IDENTITY | DB의 auto-increment/identity 기능으로 PK 생성. DB가 직접 값 할당. | MySQL, MariaDB, PostgreSQL(SERIAL), SQL Server 등 | DB가 PK를 자동 증가로 관리할 때. 단순하고, 별도 시퀀스/테이블 없이 DB 기본 기능을 그대로 쓸 때 적합. 대량 배치/성능이 중요하지 않을 때. |
| SEQUENCE | DB의 시퀀스 오브젝트에서 PK 값을 가져와 할당. | Oracle, PostgreSQL, H2 등 | DB가 시퀀스를 지원할 때. 대량 데이터 삽입, JPA 배치 처리 등 고성능이 필요할 때. 시퀀스를 커스터마이즈할 때. |
| TABLE | 별도 테이블에 PK 값을 저장·증가시켜 관리. | 모든 DBMS | 시퀀스/identity 미지원 DB에서 DB 독립적 키 생성이 필요할 때. 하지만 성능 저하로 대규모 트랜잭션에는 비권장. |
| AUTO | JPA 구현체가 DB에 맞는 전략을 자동 선택. | 모든 DBMS | DB 종류가 바뀔 수 있거나, 특별한 요구사항이 없을 때. 그러나 동작이 DB/버전에 따라 달라질 수 있어 일관성이 중요할 때. |
요약
- IDENTITY: DB가 PK를 자동 증가로 관리할 때(간단, 소규모, DB에 맞춘 설계).
- SEQUENCE: 시퀀스 지원 DB에서 성능과 확장성을 원할 때(대량 데이터, 커스텀 시퀀스).
- TABLE: DB 독립성이 우선이거나, 시퀀스/identity 없는 DB에서(성능 저하 감수).
- AUTO: JPA가 알아서 결정(프로토타입, DB 변경 가능성 있을 때, 단 일관성 주의).
Repository 정의
package com.example.bookmanager.repository;
import com.example.bookmanager.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
JpaRepository<{Entity 클래스} , {PK 데이터 타입}> 을 상속 받는다.
Application.yml 설정
spring:
jpa:
show-sql: true #jpa 사용 시 sql 로그 출력
properties:
hibernate:
format_sql: true # sql 로그를 sql 포맷형식으로 출력
defer-datasource-initialization: true
h2:
console:
enabled: true
Spring Boot 2.5 이상부터는 기본적으로 data.sql과 같은 SQL 스크립트가 Hibernate(JPA)의 DDL(테이블 생성 등) 작업보다 먼저 실행된다. 이 때문에 Hibernate가 아직 초기화 되지 않았는데 data.sql이 실행되어 에러가 발생할 수 있다.
spring.jpa.defer-datasource-initialization: true를 설정하면 Hibernate의 DDL 작업(엔티티 기반 테이블 생성 등)이 모두 끝난 뒤에 data.sql, schema.sql 등 SQL 스크립트가 실행된다
JPA 주요 메소드 및 네이밍 규칙 정리
JpaRepository가 기본적으로 제공하는 주요 메소드와 Spring Data JPA 쿼리 메소드 네이밍 규칙을 아래와 같이 정리할 수 있습니다.
주요 기본 메소드
- save(S entity): 새로운 엔티티는 저장, 이미 존재하는 엔티티는 병합.
- delete(T entity): 엔티티 하나 삭제.
- findById(ID id): ID로 엔티티 하나 조회, Optional<T> 반환.
- findAll(): 모든 엔티티 조회, 정렬(Sort)·페이징(Pageable) 지원.
- count(): 전체 엔티티 개수 반환.
- existsById(ID id): 해당 ID 엔티티 존재 여부 반환.
- saveAll(Iterable<S> entities): 여러 엔티티 저장.
쿼리 메소드 네이밍 규칙 및 예시
Spring Data JPA는 메소드 이름만으로도 다양한 쿼리를 자동 생성합니다.
| 기능 | 메소드 네이밍 시작 | 반환 타입 | 예시 및 설명 |
| 조회 | findBy, readBy, queryBy, getBy | List<T>, Optional<T> | findByName(String name) |
| 카운트 | countBy | long | countByAgeGreaterThan(int age) |
| 존재여부 | existsBy | boolean | existsByEmail(String email) |
| 삭제 | deleteBy, removeBy | long, void | deleteByName(String name) |
| 중복제거 | findDistinctBy | List<T> | findDistinctByEmail(String email) |
연산자 및 키워드 조합
- And, Or: findByNameAndAge, findByNameOrEmail
- Between: findByAgeBetween(int start, int end)
- LessThan, GreaterThan: findByAgeLessThan(int age), findByAgeGreaterThan(int age)
- Like, NotLike: findByNameLike(String pattern)
- In, NotIn: findByAgeIn(List<Integer> ages)
- OrderBy: findByAgeOrderByNameDesc(int age)
- IsNull, IsNotNull: findByEmailIsNull(), findByEmailIsNotNull()
- True, False: findByActiveTrue(), findByActiveFalse()
- IgnoreCase: findByNameIgnoreCase(String name)
- Limit: findFirst3ByOrderByAgeDesc(), findTopByOrderByScoreAsc()
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
List<User> findByNameAndAge(String name, int age);
List<User> findByAgeLessThan(int age);
List<User> findByNameContaining(String part);
List<User> findByNameOrderByAgeAsc(String name);
List<User> findByAgeBetween(int start, int end);
}
data.sql로 사전 데이터 정의 하기
resources 하위에 data.sql을 생성하면 spring boot가 구동되면 자동으로 data.sql를 실행한다.
-- PK가 GenerationType.IDENTITY 인 경우
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('jun','jun@gmail.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('steve','steve@naver.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('dennis','dennis@hanmail.net',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('queen','queen@daum.com',now(),now());
insert into users (`name`, `email`,`created_at`,`updated_at`) values ('jun','jun@yahoo.com',now(),now());
JPA save() 등 ORM 코드로 데이터 삽입 (권장)
- JPA가 시퀀스와 PK 할당을 완전히 관리하므로, 시퀀스 값 불일치, 음수 PK, 중복 오류 등 예기치 못한 문제가 발생하지 않습니다.
- 테스트 데이터가 필요한 경우, 테스트 코드의 @BeforeEach 또는 별도 초기화 메서드에서 save()로 엔티티를 저장합니다.
- 예시:
- @BeforeEach void setUp() { userRepository.save(new User("jun", "[jun@gmail.com](<mailto:jun@gmail.com>)", ...)); userRepository.save(new User("steve", "[steve@naver.com](<mailto:steve@naver.com>)", ...)); }
flush : DB 반영 시점 조절. 로그상의 크게 변화 없음
delete 관련 일반 함수들은 먼저 해당 엔티티가 DB상에 있는지 각각 select쿼리로 확인 한 후 삭제 처리를 한다
delete관련 Batch 함수들은 where 속성 in 으로 확인 후 where 속성 or 절로 한번에 삭제 처리를 한다.
allInBatch 같은 경우는 select 쿼리를 거치지 않고 바로 삭제 처리를 한다.
pagenation 기능이 필요한 경우 JPA find 함수의 인자로 PageRequest.of(페이지번호, 페이지 사이즈)를 넣어주면 Page<Entitiy> 객체를 반환 받아서 활용할 수 있다.
@Test
void crud(){
Page<User> users = userRepository.findAll(PageRequest.of(1,3));
users.forEach(System.out::println);
}
@Test
void crud(){
Page<User> users = userRepository.findAll(PageRequest.of(1,3));
System.out.println("pages :" + users);
System.out.println("totalElements :" + users.getTotalElements());
System.out.println("totalPages :" + users.getTotalPages());
System.out.println("Number of elements :" + users.getNumberOfElements());
System.out.println("sort :"+users.getSort());
System.out.println("size : "+users.getSize());
users.getContent().forEach(System.out::println);
}
// 콘솔 결과
pages :Page 2 of 2 containing com.example.bookmanager.domain.User instances
totalElements :5
totalPages :2
Number of elements :2
sort :UNSORTED
size : 3
QueryByExampleExecutor란?
QueryByExampleExecutor는 Spring Data JPA에서 제공하는 인터페이스로, 엔티티의 일부 필드 값(예: name, email 등)을 예시로 삼아 동적 쿼리(Example Query)를 쉽게 생성할 수 있게 해줍니다. 즉, “이런 값이 들어있는 엔티티를 찾아줘”라는 식의 검색을 코드 몇 줄로 구현할 수 있다.
위 방식은 특히 문자열에 관련된 것만 쓸 수 있다는 한계점이 있어서 조금 복잡한 쿼리를 만들 때는
Query DSL과 같은 별도의 방식으로 구현을 하게 된다.
@Test
void crud() {
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("name")
.withMatcher("email", endsWith());
Example<User> example = Example.of(new User("j", "gmail.com"), matcher);
userRepository.findAll(example).forEach(System.out::println);
}
주요 개념
Probe: 실제로 값을 채운 엔티티 객체(예: new User("j", "gmail.com")). 이 객체의 값이 쿼리 조건이 됩니다.
ExampleMatcher: 어떤 필드는 무시하고, 어떤 필드는 어떻게 비교할지(예: endsWith, contains 등) 지정하는 도구.
Example: Probe와 ExampleMatcher를 합쳐서, 실제로 쿼리를 만들 때 사용하는 객체.
QueryByExampleExecutor: findAll(Example<S> example) 등 Example 기반 쿼리 메서드를 제공하는 인터페이스. Spring Data JPA의 Repository가 이 인터페이스를 상속받으면 QBE 기능을 쓸 수 있습니다