
JPA(Java Persistence API)는 JAVA ORM(Object-Relational Mapping 객체 관계 매핑) 기술에 대한 표준 명세로 인터페이스의 모음임.
따라서 실제로 동작하는 것이 아니기 때문에 구현체가 필요한데, JPA 표준을 구현한 구현체는 아래와 같이 Hibernate, EclipseLink, DataNucleus가 있으며 대표적으로 Hibernate를 사용함.
JPA는 객체와 관계형 데이터베이스 간의 데이터를 자동으로 변환해 주는 역할을 함.
JPA는 자바 객체와 데이터베이스의 테이블 간의 매핑을 지원하며, 데이터베이스 접근을 객체 지향적으로 처리할 수 있게 해줌.
JPA는 자바 애플리케이션에서 데이터베이스 연동 시 SQL을 직접 작성하지 않고도 데이터를 쉽게 관리할 수 있도록 도와줌.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
// Getter와 Setter 생략
}
@Id: 해당 필드는 기본 키(Primary Key)를 나타냄.@GeneratedValue: 기본 키 값이 자동으로 생성되도록 설정함.public class UserRepository {
@PersistenceContext
private EntityManager entityManager;
public User findUser(Long id) {
return entityManager.find(User.class, id); // 특정 사용자 조회
}
public void saveUser(User user) {
entityManager.persist(user); // 새로운 사용자 저장
}
}
persist(Object entity): 새로운 엔터티를 영속성 컨텍스트에 저장하고, 데이터베이스에 해당 데이터를 삽입함. 즉, 새로운 레코드를 추가하는 메소드임.User user = new User();
user.setName("John");
entityManager.persist(user); // user 엔터티가 영속성 컨텍스트에 저장됨.
find(Class<T> entityClass, Object primaryKey): 기본 키로 엔터티 객체를 조회함. 영속성 컨텍스트에 엔터티가 있으면 캐시된 엔터티를 반환하고, 없으면 데이터베이스에서 조회함.User user = entityManager.find(User.class, 1L); // ID가 1인 User 엔터티를 조회
merge(Object entity): 변경된 엔터티를 영속성 컨텍스트에 병합하고 저장함. 분리된 엔터티(Detached 상태)도 병합할 수 있음.user.setName("Jane");
entityManager.merge(user); // 분리되거나 변경된 엔터티를 영속성 컨텍스트에 병합하고 저장함.
remove(Object entity): 엔터티를 영속성 컨텍스트에서 삭제하고, 데이터베이스에서 해당 엔터티를 제거함.entityManager.remove(user); // 영속성 컨텍스트에서 엔터티 제거 후, 데이터베이스에서 삭제
flush(): 영속성 컨텍스트의 변경 사항을 데이터베이스에 반영함(동기화). 자동으로 데이터베이스에 반영되지 않도록 하기 위해 수동으로 명시할 때 사용됨.entityManager.flush(); // 현재 영속성 컨텍스트의 변경 사항을 데이터베이스에 반영
flush는 영속성 컨텍스트의 내용을 데이터베이스에 반영(동기화)하는 과정이며, 이는 데이터베이스와 영속성 컨텍스트 사이의 스냅샷을 일치시키는 작업임 이는 에러 발생 시 롤백이 가능한 단계까지만 반영하는 작업임을 설명. 반면, commit은 진행 중인 트랜잭션을 완료하고 해당 트랜잭션내의 모든 변경 사항들을 데이터베이스에 영구적으로 반영하는 과정임. 해당 단계 이후에는 롤백이 불가능함. commit은 영속성 컨텍스트를 비우지만 flush는 영속성 컨텍스트를 비우지 않아 1차캐시 사용가능함.
요약: flush해서 db에 삽입 혹은 변경이 이루어졌더라도 이후 해당 트랜잭션을 commit하지 않는다면 데이터베이스에 영구적인 저장이 이루어지지 않음.
EntityManager는 Thread-Safe하지 않기 때문에 동시성 문제가 발생할 수 있음.
그래서 EntityManager는 스레드간에 공유를 절대로 해서는 안됨.
일반적으로 EntityManager를 @PersistenceContext로 Spring이 관리해주는 방식으로 사용함.
@PersistenceContext를 사용해서 EntityManager를 주입받으면 Spring에서 EntityManager를 Proxy로 감싼 EntityManager를 생성해서 주입해주기에 Thread-Safe를 보장함.
User user1 = entityManager.find(User.class, 1L); // 데이터베이스에서 조회됨
User user2 = entityManager.find(User.class, 1L); // 1차 캐시에서 조회됨 (쿼리 발생하지 않음)
EntityManager는 동일한 트랜잭션 내에서 트랜잭션을 commit하기 전까지 데이터베이스에 Entity를 저장하지 않고 영속성 컨텍스트 내부의 SQL저장소에 쿼리를 저장해둠.
commit을 하게 되면 SQL저장소에 저장해두었던 쿼리를 데이터베이스에 보내고 EntityManager는 영속성 컨텍스트를 flush함.
이 후 트랜잭션을 commit함.
EntityTransaction transaction = entityManager.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
entityManager.persist(user1);
entityManager.persist(user2);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
user.setName("KIM"); // 엔터티 필드 값 변경 (변경 감지)
transaction.commit(); // 변경 사항이 데이터베이스에 자동으로 반영됨
연관관계에 있는 엔티티를 조회시 한번에 가져오지 않고 필요시에 가져오는 것임.
연관관계에 있는 객체는 프록시 상태로 초기화되지 않은 상태로 존재함.
필요시에 가져오기 때문에 불필요한 쿼리를 실행하지 않을 수 있음.
각 연관관계의 Default 로딩값
OneToMany : Lazy
ManyToMany : Lazy
ManyToOne : Eager
OneToOne : Eager
엔터티 객체는 생명주기(Lifecycle)에 따라 상태가 변함. JPA는 엔터티의 상태를 관리하며, 엔터티는 다음 네 가지 상태 중 하나에 속함.
엔터티 객체가 영속성 컨텍스트에 저장되지 않았으며, 데이터베이스와 연관이 없는 상태.
// 객체를 생성만 하고 저장하지 않은 상태 (비영속)
User user1 = new User();
user1.setName("KIM");
엔터티가 영속성 컨텍스트에 저장된 상태.
// 객체를 영속성 컨텍스트에 저장한 상태 (영속)
entityManager.persist(user1);
entityManager.find(User.class, 1);
엔터티가 한때 영속성 컨텍스트에 저장되었지만, 현재는 영속성 컨텍스트에서 분리된 상태.
비영속 상태에 가까움.
영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.(지연로딩, 1차 캐시, 쓰기 지연, 변경 감지)
비영속 상태는 식별자 값이 없지만, 준영속 상태는 이미 한번 영속 상태였으므로 식별자 값(ID)을 가지고 있다.
//회원 엔티티를 영속성 컨텍스트에서 분리 (준영속)
entityManager.detach(user1);
//영속성 컨텍스트를 완전히 초기화
entityManager.clear();
//영속성 컨텍스트를 종료
entityManager.close();
entityManager.detach(entity): 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됨.
entityManager.clear(): 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
entityManager.close(): 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다.
엔터티가 영속성 컨텍스트와 데이터베이스에서 삭제된 상태.
entityManager.remove(user1);
JPQL(Java Persistence Query Language)은 JPA(Java Persistence API)에서 사용하는 객체 지향적 쿼리 언어임. JPQL은 데이터베이스 테이블을 대상으로 하는 SQL과 달리, 엔터티 객체를 대상으로 쿼리를 작성함. 즉, JPQL은 SQL과 유사한 문법을 사용하지만, 실제로는 객체를 기반으로 동작하며, JPA에서 엔터티 매핑을 통해 데이터베이스 테이블과 연동됨.
String jpql = "SELECT u FROM User u WHERE u.age > :age";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("age", 20);
List<User> users = query.getResultList();
createQuery(): JPQL 쿼리를 생성하고, TypedQuery를 반환함.-- SQL에서는
SELECT * FROM User WHERE age > 20;
-- JPQL에서는
SELECT u FROM User u WHERE u.age > 20;
-- 예를 들어, User 엔터티와 Address 엔터티가 연관 관계가 있을 때,
SELECT u FROM User u JOIN u.address a WHERE a.city = 'Seoul';
JPQL은 SQL과 매우 유사한 문법을 가지고 있음.
SELECT 쿼리는 엔터티 객체 또는 특정 필드를 조회하기 위해 사용됨.
SELECT u FROM User u
WHERE 절은 조건에 맞는 데이터를 조회할 때 사용됨.
SELECT u FROM User u WHERE u.age > 20
JOIN은 연관된 엔터티를 조회할 때 사용됨. JPQL에서는 객체 간의 연관 관계를 기반으로 조인을 수행함.
SELECT u FROM User u JOIN u.address a WHERE a.city = 'Seoul'
ORDER BY는 조회한 결과를 정렬할 때 사용됨.
SELECT u FROM User u ORDER BY u.name ASC
GROUP BY는 특정 필드를 기준으로 그룹화하여 조회할 때 사용됨.
SELECT u.city, COUNT(u) FROM User u GROUP BY u.city
HAVING은 GROUP BY와 함께 사용하여, 그룹화된 결과에 조건을 부여할 때 사용됨.
SELECT u.city, COUNT(u) FROM User u GROUP BY u.city HAVING COUNT(u) > 10
각 도시에서 10명 이상의 사용자가 있는 도시만 조회함.
LIMIT은 JPQL에서는 제공하지 않지만, setMaxResults() 메소드를 사용하여 결과 수를 제한할 수 있음.
TypedQuery<User> query = entityManager.createQuery("SELECT u FROM User u", User.class);
query.setMaxResults(10); // 최대 10개의 결과만 반환
JPQL은 서브쿼리도 지원하며, 주로 SELECT, WHERE 절에서 사용됨.
SELECT u FROM User u WHERE u.age > (SELECT AVG(u2.age) FROM User u2)
JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션들을 한번의 SQL 쿼리로 함께 조회할 수 있음.
연관된 엔티티에 대해 추가적인 쿼리를 실행할 필요 없이 효율적인 로드를 할 수 있음.
즉, 패치 조인은 성능 최적화에 주로 사용되며, N+1 문제를 해결하는 데 효과적임.
일반 Join과 Fetch Join의 차이는 데이터를 조회할 때 어떻게 가져오느냐에 있음.
일반 Join은 데이터베이스 테이블을 조인하여 연관된 데이터만 가져옴. 하지만 가져온 데이터를 사용하려고 할 때 별도의 쿼리를 수행해야 함.
예를 들어, 멤버(Member)와 팀(Team)이 있는데, 팀에 소속되 있는 모든 멤버를 조회하고 싶다면, 일반 Join에서는 멤버와 팀 테이블을 조인하여 팀 정보를 가져옴.
SELECT t FROM Team t JOIN t.members
Join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
조회의 주체가 되는 엔티티와 그 관련 엔티티들 까지 함께 조회한다.
이를 객체 그래프 로드라고 하는데, 즉시 원하는 정보를 사용할 수 있도록 데이터를 로드해온다.
이 방식을 통해 한 번의 쿼리로 필요한 정보를 모두 가져와서 성능을 향상시킬 수 있다.
위 예시에서 Fetch Join을 사용하면 팀과 해당 팀에 소속된 모든 멤버 데이터를 한번에 가져와 별도의 쿼리 없이 바로 멤버 정보에 접근할 수 있다.
SELECT t FROM Team t JOIN FETCH t.members
실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT
SELECT m FROM Member m JOIN FETCH m.team WHERE m.team.id = :teamId
m.team.id: 리스트 엔터티가 아닌 단일 엔터티를 참조하는 경우 연관 테이블 컬럼에 접근 할 수 있음.
연관 엔티티의 데이터를 자주 사용하는 경우
지연 로딩의 경우 연관 엔티티를 자주 가져오는 경우에 성능 저하가 발생할 수 있음.
이 때, 연관 엔티티의 데이터를 함께 가져오면 지연 로딩에 따른 추가 쿼리를 줄이고 성능을 향상시킬 수 있음.
즉, A만 필요할 때는 FetchType.Lazy 지연 로딩으로 A 만 불러 사용하고,
A와 B를 같이 부르고 싶을 경우에는 FetchType.Lazy + Fetch Join 조합을 사용하여 성능을 향상 시킬 수 있음.
N+1 문제가 발생하는 경우
N+1 문제는 연관 엔티티가 실제 사용될 때마다 별도의 쿼리가 발생하여 성능 이슈를 초래하는 문제임.
이 경우, 추가적인 쿼리가 불필요하게 발생하여 성능 저하가 발생함.
Fetch Join은 연관 엔티티와 함께 즉시 로딩되므로, N+1 문제를 제거할 수 있음.
Fetch Join을 사용할 때 일대다(OnetoMany) 또는 다대다(ManytoMany) 관계의 엔티티가 많이 포함되면, 불필요한 중복 데이터를 가져오는 일이 발생할 수 있음.
이 경우, DISTINCT 키워드를 사용하여 중복 데이터를 제거할 수 있음.
JPQL의 DISTINCT
SELECT DISTINCT o FROM Order o JOIN FETCH o.products WHERE o.id = :orderId2개 이상의 컬렉션을 한 번에 Fetch Join 불가
컬렉션을 Fetch Join하면 페이징 API를 사용할 수 없음
페이징 기능을 사용하여 데이터를 검색할 때, 일반적으로 데이터베이스에서 필요한 범위의 데이터만 가져온 후 이를 화면에 표시함.
그러나 Fetch Join을 사용할 경우 서로 관련된 데이터를 함께 로드하게 되며, 이 때문에 페이징 처리가 제대로 이루어지지 않을 수 있음.
이 문제는 OneToMany와 ManyToMany 연관관계에서 데이터가 중복되거나 데이터의 크기가 곱이 되면서 가장 뚜렷하게 나타남.
Fetch Join 대상 Entity에는 별칭 불가
JPQL에서는 데이터 조작을 위한 INSERT 문은 존재하지 않음. 대신, persist() 메소드를 사용하여 새로운 엔터티를 저장함. 그러나 UPDATE와 DELETE는 JPQL에서 지원함.
UPDATE 쿼리는 엔터티 객체의 데이터를 수정하는 데 사용됨.
UPDATE User u SET u.age = 30 WHERE u.name = 'John'
DELETE 쿼리는 엔터티 객체를 삭제하는 데 사용됨.
DELETE FROM User u WHERE u.age < 20
String jpql = "SELECT u FROM User u WHERE u.age > :age";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("age", 20);
List<User> users = query.getResultList();
String jpql = "SELECT u FROM User u JOIN u.address a WHERE a.city = :city";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("city", "Seoul");
List<User> users = query.getResultList();
String jpql = "SELECT u.city, COUNT(u) FROM User u GROUP BY u.city";
TypedQuery<Object[]> query = entityManager.createQuery(jpql, Object[].class);
List<Object[]> results = query.getResultList();
객체 지향적 쿼리: JPQL은 엔터티 객체를 기반으로 하여 쿼리를 작성하므로, 객체 지향적 패러다임을 유지하면서 데이터베이스 작업을 할 수 있음.
데이터베이스 독립성: JPQL은 특정 DBMS에 종속되지 않으며, JPA가 JPQL을 각 데이터베이스에 맞는 SQL로 변환해주므로, 데이터베이스 독립적인 코드를 작성할 수 있음.
자동 변환 및 최적화: JPA는 JPQL을 적절한 네이티브 SQL로 변환하고, 성능을 최적화함.
JPA는 JAVA 애플리케이션과 JDBC 사이에서 동작한다. JAVA 애플리케이션에서 JPA를 사용하면 내부에서 JDBC API를 통해 SQL을 DB에 전달하고 결과를 반환받음.
@Entity(name = "Users")JPQL은 테이블이 아닌 엔티티를 대상으로 쿼리를 수행하는데, 이때 이 쿼리에 엔티티의 이름이 사용됨.
SELECT u FROM Users u WHERE u.name = "KIM"
@Entity(name = "Users")
@Data
public class User {
@Id
private String id;
}
| 속성 | 기능 | 기본값 |
|---|---|---|
| strategy | 식별자 생성 전략을 선택 | GenerationType.AUTO |
| generator | 사용할 식별자 생성 전략을 명시적으로 지정된 생성기 이름을 사용하여 설정할 때 사용 |
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
엔티티가 영속 상태가 되기 위해서는 식별자가 필수임.
그런데 IDENTITY 전략을 사용하면 식별자를 데이터베이스에서 지정하기 전까지는 알 수 없기 때문에, em.persist()를 하는 즉시 INSERT SQL이 데이터베이스에 전달됨.
따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않음.
@SequenceGenerator(
name = "USER_SEQ_GENERATOR",
sequenceName = "USER_SEQ",
initialValue = 1,
allocationSize = 1
)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "USER_SEQ_GENERATOR")
@Column(name = "user_id")
private Long id;
}
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 식별자 생성기 이름 | 필수 |
| sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
| initialValue | DDL 생성 시에만 사용된다. 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. | 1 |
| allocationSize | 시퀀스를 한 번 호출할 때 증가하는 수 | 50 |
| catalog, schema | 식별자 생성기의 catalog, schema 이름 |
SEQUENCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 SEQUENCE를 사용해서 식별자를 조회함.
그리고 조회한 식별자를 엔티티에 할당한 후 해당 엔티티를 영속성 컨텍스트에 저장함.
이후 트랜잭션 commit 시점에 flush가 발생하면 엔티티를 데이터베이스에 저장함.
IDENTITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후에 식별자를 조회하여 엔티티의 식별자에 할당한 후 영속성 컨텍스트에 저장함.
@TableGenerator(name = "USER_SEQ_GENERATOR", table = "SEQUENCE_TABLE", valueColumnName = "NEXT_VAL",pkColumnName = "SEQUENCE_NAME", pkColumnValue = "USER_SEQ", initialValue = 1, allocationSize = 1)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "USER_SEQ_GENERATOR")
@Column(name = "user_id")
private Long id;
}
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 식별자 생성기 이름 | 필수 |
| table | 키생성 테이블명 | hibernate_sequence |
| pkColumnName | 시퀀스 컬럼명 | sequence_name |
| valueColumnName | 시퀀스 값 컬럼명 | next_val |
| pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
| initialValue | 초기 값. 마지막으로 생성된 값이 기준 | 0 |
| allocationSize | 시퀀스를 한 번 호출할 때 증가하는 수 | 50 |
| catalog, schema | 식별자 생성기의 catalog, schema 이름 | |
| uniqueConstraints (DDL) | 유니크 제약 조건을 지정 |
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 매핑할 테이블 이름 지정 | 엔티티 이름 |
| catalog | catalog 기능이 있는 DB에서 지정한 catalog를 매핑하여 사용 | |
| schema | 스키마 기능이 있는 DB에서 schema 를 매핑해서 사용 | |
| uniqueConstraints (DDL) | DDL 생성시 유니크 제약 조건을 만듦, 이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들때만 사용('spring.jpa.hibernate.ddl-auto=create' 등) |
@Table(name = "user_table",
uniqueConstraints = {
@UniqueConstraint(name = "UniqueUsername", columnNames = {"username"}),
@UniqueConstraint(name = "UniqueNickname", columnNames = {"nickname"})
}
)
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 필드와 매핑할 테이블의 컬럼 이름을 지정 | 객체의 필드 이름 |
| insertable | 엔티티 저장 시 이 필드도 같이 저장. false로 설정하면 이 필드는 데이터베이스에 저장하지 않음. false 옵션은 읽기 전용일 때 사용 | true |
| updatable | 엔티티 수정 시 이 필드도 같이 수정. false로 설정하면 데이터베이스에 수정하지 않음. | true |
| table | 하나의 엔티티를 두 개 이상의 테이블에 매필할 때 사용함(@SecondaryTable 사용). 지정한 필드를 다른 테이블에 매핑할 수 있음 | 현재 클래스가 매핑된 테이블 |
| nullable (DDL) | DDL 생성 시 null 값의 허용 여부를 설정. false로 설정하면 not null 제약조건 | true |
| unique (DDL) | @Table의 uniqueConstraints와 같으나 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용 | false |
| columnDefinition (DDL) | 데이터베이스 컬럼 정보를 직접 줄 수 있음 | 자바 필드의 타입과, 데이터베이스 방언 설정 정보를 사용해적절히 생성 |
| length (DDL) | 문자 길이 제약조건, String 타입에만 사용 | 255 |
| precision, scale (DDL) | BigDecimal 타입(혹은 BigInteger)에서 사용. precision은 소수점을 포함한 전체 자리수, scale은 소수의 자리수임 | precision = 0, scale = 0 |
@Entity(name = "Users")
@Data
public class User {
@Id
@Column(name = "user_id")
private String id;
@Column(columnDefinition = "varchar (100) default 'EMPTY'")
private String profileContent
}
@Column의 unique 속성을 true로 사용할 경우 유니크 제약조건의 이름을 랜덤으로 지정하여 알아보기 힘듬. 따라서 @Column의 unique 속성보다는 @Table의 uniqueConstraints 속성을 사용하여, 제약조건의 이름을 걸어주는 방법을 사용하는 것이 가독성이 좋음.
| 속성 | 기능 | 기본값 |
|---|---|---|
| value | TemporalType.DATE : 날짜, 데이터베이스 date 타입과 매핑 (예 : 2011-11-11), TemporalType.TIME : 시간, 데이터베이스 time 타입과 매핑 (예 : 11:11:11), TemporalType.TIMESTAMP : 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예 : 2011-11-11 11:11:11) | TemporalType 필수 지정 |
@Entity
public class User{
@Id
private Long id;
@Temporal(value = TemporalType.DATE)
private LocalDateTime createdDate;
}
자바 엔티티 필드의 Enum 타입을 데이터베이스에 매핑할 때 어떻게 매핑할 것인지 정해주는 어노테이션임.
| 속성 | 기능 |
|---|---|
| EnumType.ORDINAL | enum 순서(숫자) 값을 DB에 저장 |
| EnumType.STRING | enum 이름 값을 DB에 저장 |
enum Gender {
MALE,
FEMALE
}
@Enumerated(EnumType.ORDINAL)
private Gender gender // MALE로 세팅하면 1, FEMALE 은 2
@Enumerated(EnumType.STRING)
private Gender gender; // "MALE", "FEMALE" 문자열 자체가 저장
@Embeddable
public class Address {
private city;
private street;
private zipcode;
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address;
}
@Entity
public class Order {
@Embeddedid
private OrderNumber number;
...
}
@Embeddable
public class OrderNumber implements Serializable {
@Column(name="order_number")
private String number;
...
}
@Embeddable
public class Address {
private String city;
private String street;
@Embedded
private Zipcode zipcode;
}
@Embeddable
public class Zipcode {
private String zip;
private String plusFour;
}
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "HOME_CITY")),
@AttributeOverride(name = "street", column = @Column(name = "HOME_STREET")),
@AttributeOverride(name = "zipcode.zip", column = @Column(name = "HOME_ZIP")),
@AttributeOverride(name = "zipcode.plusFour", column = @Column(name = "HOME_PLUS_FOUR")),
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
@AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
@AttributeOverride(name = "zipcode.zip", column = @Column(name = "COMPANY_ZIP")),
@AttributeOverride(name = "zipcode.plusFour", column = @Column(name = "COMPANY_PLUS_FOUR")),
})
private Address companyAddress;
}
| 속성 | 기능 | 기본값 |
|---|---|---|
| name | 매핑할 외래 키 이름 | 필드명 +_+ 참조하는 테이블의 기본 키 컬럼명 |
| referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
| foreignKey(DDL) | 외래 키가 참조하는 대상 테이블의 컬럼명 | |
| unique nullable insertable. updatable columnDefinition table | @Column의 속성과 같다. |
| 속성 | 기능 | 기본값 |
|---|---|---|
| optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
| fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY |
| cascade | 속성 전이 기능을 사용한다. | |
| targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. |
| 속성 | 기능 | 기본값 |
|---|---|---|
| mappedBy | 연관관계의 주인 필드를 선택한다. | |
| fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne=FetchType.EAGER @OneToMany=fetchType.LAZY |
| cascade | 속성 전이 기능을 사용한다. | |
| targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다. |
JPA는 객체 관계 모델링에서 매우 중요한 다중 관계를 지원함. 객체와 데이터베이스 테이블 간의 관계를 매핑할 때 1:1, 1:N, N:M과 같은 관계를 처리할 수 있음.
한 엔터티가 다른 엔터티와 일대일(1:1)로 매핑될 때 사용됨. 일대다(1:N), 다대일(N:1) 관계에서는 다(N) 쪽이 항상 외래 키를 가지고 있지만, 일대일(1:1) 관계에서는 주 테이블이나 대상이 되는 테이블 양쪽 모두 외래 키를 가질 수 있음. 때문에 일대일 관계를 적용할 때는 주 테이블과 대상이 되는 테이블, 어느 쪽에 외래 키를 둘지 선택해야 함. 일반적으로는 주 테이블에 외래키를 두는 방식을 채택함
JPA에서는 외래 키를 갖는 쪽이 연관 관계의 주인이 되고, 연관 관계의 주인이 데이터베이스 연관 관계와 매핑되어 외래 키를 관리(등록, 수정, 삭제)할 수 있기 때문에 해당 설정이 중요함.
데이터베이스는 컬렉션을 담을 수 없기 때문에 일대다, 다대일의 경우 일이 되는 쪽에서 외래 키를 가지는 것이 불가능함.
따라서 해당 경우에는 항상 다가 되는 쪽에서 외래 키를 가지게 되는 것임.
이 경우는 많이 사용되는 객체인 주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾는 방식.
주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인할 수 있다는 장점이 있지만, 만약 값이 없는 경우 외래 키에 Null을 허용해야 하는데, 이것은 DB 측면이나 비즈니스 로직의 검증 부분에서 봤을 때 좋지 않은 부분이 됨.
Member - 주 테이블
Locker - 대상 테이블
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String lockername;
}
반대 방향에도 추가적으로 @OneToOne을 설정해줌. 단, 양방향 연관관계의 대상이 되는 테이블이므로 다대일 양방향 연관관계 매핑과 마찬가지로 mappedBy 속성을 추가해주어야함.
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String lockername;
@OneToOne(mappedBy = "locker")
private Member member;
}
JPA에서 일대다 단방향 관계은 대상 테이블로 연관관계를 매핑을 지원하지만, 대상 테이블에 외래키가 있는 일대일 단방향 관계는 매핑을 지원하지 않음.
이 때는 단방향 관계를 아래와 같이 Locker ➡️ Member로 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 함.
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String lockername;
@OneToOne(mappedBy = "locker")
private Member member;
}
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String lockername;
@OneToOne
private Member member;
}
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
@Entity
public class Locker{
@Id
@GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String lockername;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
일대일 매핑에서 대상 테이블에 외래키를 두고 싶으면 양방향으로 매핑하고, 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 Locker 테이블의 외래키를 관리하도록 설정하면 됨.
JPA 구현체인 Hibernate에서는 @OneToOne 양방향 매핑 시 지연 로딩으로 설정하여도 지연 로딩(LAZY)이 동작하지 않고, 즉시 로딩(EAGER)이 동작하는 문제가 있음.
정확하게는 테이블을 조회할 때 외래 키를 가지고 있는 테이블(연관 관계의 주인)에서는 외래 키를 가지지 않은 쪽에 대한 지연 로딩은 동작하지만, mappedBy 속성으로 연결된 외래 키를 가지지 않은 쪽에서 테이블을 조회할 경우 외래 키를 가지고 있는 테이블(연관 관계의 주인)에 대해서는 지연 로딩이 동작하지 않고 N+1 쿼리가 발생하게 됨.
해당 이슈는 JPA의 구현체인 Hibernate에서 프록시 기능의 한계로 인해 발생함.
Member 테이블이 외래 키인 LOKCER_ID를 가지고 있는 연관 관계의 주인이고, Locker 테이블은 외래 키가 없는 경우를 예시로 생각했을 때, Locker 테이블의 입장에서는 Member 테이블에 대한 외래 키가 없기 때문에 Locker Entity 입장에서는 Locker에 연결되어 있는 Member가 null인지 아닌지를 조회해보기 전까지는 알 수 없음.
이처럼 Locker에 연결된 Member 객체가 null 인지 여부를 알 수 없기 때문에 Proxy 객체를 만들 수 없는 것이며, 때문에 무조건 연결된 Member가 있는지 여부를 확인하기 위한 쿼리가 실행됨.
(Locker Entity를 조회했을 때 연결된 Member Entity에 대해서 무조건 즉시 로딩이 적용되며, 따라서 N+1 조회가 발생하게 되는 것)
해결 방법이라고 했지만 완벽하게 이 문제를 해결할 수 있는 방법은 존재하지 않음. 따라서 아래와 같은 해결책을 고려한 후 현재 상황에 알맞게 적용해야 함.
구조 변경하기
구조를 유지한채 해결하기
한 엔터티가 다른 엔터티와 1:N 관계를 가질 때 사용됨.
@Entity
public class User {
@OneToMany(mappedBy = "user")
private List<Order> orders;
}
두 엔터티 간에 N:M관계가 있을 때 사용됨. 중간에 연결 테이블을 두어 매핑함.
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
}
실무에서 ManyToMany 관계를 사용하면 안 되는 이유는 주로 복잡한 데이터 모델링 및 성능 이슈 때문임. ManyToMany 관계는 두 엔터티 간에 서로 다대다의 관계를 나타내기 위한 편리한 방법이지만, 실무에서는 이 방식이 가지는 몇 가지 문제 때문에 중간 테이블을 사용하는 방식으로 구현하는 것이 더 선호됨.
1. 조인 테이블로 인한 성능 저하
ManyToMany 관계는 중간 테이블(조인 테이블)을 자동으로 생성하여 두 테이블 간의 매핑을 처리함. 하지만, 데이터가 많아질수록 조인 테이블을 사용하는 쿼리가 매우 복잡해지고 느려질 수 있음.
2. 지연 로딩(Lazy Loading) 문제
ManyToMany 관계에서 지연 로딩을 사용하는 경우, 관련된 엔터티들을 모두 로드할 때 다중 쿼리가 발생할 수 있음. 특히, N+1 문제가 발생할 수 있어, 많은 데이터를 처리할 때 성능 저하를 초래할 수 있음.
1. 중간 테이블에 추가 속성을 저장할 수 없음
ManyToMany 관계는 두 엔터티 간의 관계를 자동으로 처리하는 조인 테이블을 사용하지만, 중간 테이블에 추가적인 정보를 저장할 수 없음. 예를 들어, 두 엔터티 간의 관계에 추가적인 속성이 필요할 때, 예를 들어 연관된 날짜, 상태 정보 등을 저장해야 하는 경우 ManyToMany 관계는 이를 지원하지 않음.
2. 관계에 대한 세밀한 제어 부족
실무에서는 두 엔터티 간의 관계를 더 세밀하게 제어해야 하는 경우가 많음. 그러나 ManyToMany는 기본적으로 자동화된 매핑에 의존하므로, 관계에 대한 세밀한 제어가 어려움.
실무에서는 ManyToMany 관계를 피하고, 대신 중간 엔터티를 사용하여 OneToMany와 ManyToOne 관계로 풀어내는 것이 더 일반적임. 이렇게 하면 중간 테이블에 추가적인 정보를 저장할 수 있고, 관계에 대한 세밀한 제어가 가능해짐.
중간 엔터티 사용 예시
예를 들어, 학생(Student)과 수업(Course) 간의 관계를 ManyToMany 대신 중간 엔터티를 사용하여 풀어내면 다음과 같음.
엔터티 설계
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<Enrollment> enrollments;
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@OneToMany(mappedBy = "course")
private List<Enrollment> enrollments;
}
@Entity
public class Enrollment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private LocalDate enrollmentDate; // 추가 속성
}
확장성: 중간 엔터티에 추가적인 속성을 쉽게 저장할 수 있음. 예를 들어, 수강 등록 날짜, 상태 등의 추가 정보를 저장할 수 있음.
유연성: 관계에 대한 세밀한 제어가 가능하며, Cascade 옵션 및 삭제 정책을 명확하게 정의할 수 있음.
성능 최적화: 쿼리 성능을 더 세밀하게 최적화할 수 있으며, N+1 문제 등을 더 쉽게 관리할 수 있음.
JPA는 객체 지향적으로 데이터를 처리할 수 있기 때문에, 데이터베이스의 테이블과 객체를 매핑하여 객체 중심의 개발을 지원함.
객체지향 설계를 따르면서 데이터베이스와의 매핑 코드를 줄일 수 있음.
JPA는 데이터베이스에 종속되지 않음. JPA는 표준 API이므로, 특정 데이터베이스에 의존하지 않고 다양한 데이터베이스와 연동 가능함.
예를 들어, MySQL, PostgreSQL, Oracle 등 다양한 DBMS에서 JPA를 사용할 수 있음.
JPA는 SQL을 자동으로 생성해주므로, 직접 SQL을 작성하지 않고도 CRUD 작업을 수행할 수 있음.
복잡한 쿼리를 작성할 필요 없이, 객체 지향적으로 데이터를 처리할 수 있음.
JPA는 기본적으로 지연 로딩을 지원함. 이는 엔터티의 관련 데이터가 실제로 필요할 때에만 로딩됨을 의미함.
필요하지 않은 데이터를 미리 로딩하지 않으므로 성능 최적화에 유리함.
JPA는 트랜잭션 처리를 지원함. 트랜잭션 단위로 엔터티의 상태를 관리하며, 이를 통해 데이터의 일관성과 안정성을 유지할 수 있음.
개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해 실행함.
JPA를 이용함으로써 개발자는 항상 객체 지향적으로 코드를 표현할 수 있게되어 더는 SQL에 종속적인 개발을 하지 않아도 됨.
JPA를 통해 객체 중심으로 개발을 할 수 있게 되다보니 생산성 향상은 물론 유지 보수또한 정말 편리해짐.
JPA는 간단한 CRUD 작업에 적합하지만, 복잡한 쿼리를 처리하는 데에는 한계가 있을 수 있음.
복잡한 쿼리가 필요한 경우 JPQL이나 네이티브 SQL을 사용해야 함.
JPA를 사용하기 위해서는 학습해야 할 것들이 많음.
JPA를 사용해도 SQL을 알아야 Hibernate가 수행한 쿼리 확인 및 성능 모니터링이 가능함.
대규모 데이터를 처리하거나 잘못된 매핑이나 지연 로딩 설정이 문제를 일으킬 경우 성능 문제가 발생할 수 있음.
성능 최적화와 관련된 부분은 주의 깊게 관리해야 함.
따라서 JPA를 사용할 때는 성능에 대한 이해가 필요함.
Spring Data JPA는 Spring Framework에서 제공하는 JPA(Java Persistence API)의 확장된 기능을 지원하는 ORM(Object-Relational Mapping) 기술임. Spring Data JPA는 JPA의 사용을 보다 간단하고 효율적으로 만들어주며, 데이터 접근 계층에서 자주 사용하는 CRUD 기능, 쿼리 생성, 페이징, 정렬 등을 자동화하여 개발자의 생산성을 크게 향상시킴.
Spring Data JPA는 JPA의 기본 기능을 기반으로 추가적인 데이터 접근 기능을 제공하며, JPA 구현체인 Hibernate와 같은 ORM 프레임워크와 함께 사용됨. 특히 Spring Boot와 함께 사용할 경우, 자동 설정과 함께 간편하게 데이터베이스와 상호작용할 수 있음.
Spring Data JPA는 Repository 인터페이스를 제공하여 데이터베이스와 상호작용함. 이 인터페이스를 통해 CRUD 작업을 쉽게 수행할 수 있으며, 개발자는 기본적인 CRUD 작업에 대한 구현 코드를 작성할 필요가 없음. JpaRepository 인터페이스를 상속받아 사용하며, 기본적인 CRUD 메소드를 자동으로 제공함.
public interface UserRepository extends JpaRepository<User, Long> {
}
JpaRepository: JpaRepository<User, Long>는 User 엔터티를 다루며, 기본 키(PK) 타입이 Long임. Spring Data JPA에서 제공하는 기본 인터페이스로, 다양한 데이터 접근 기능을 제공함.
UserRepository는 별도의 구현 없이 CRUD 기능을 사용할 수 있음.
Spring Data JPA는 JpaRepository를 통해 자동으로 CRUD 메소드를 제공함. save(), findAll(), findById(), deleteById(), count(), existsById()와 같은 메소드를 기본적으로 사용할 수 있음.
save(): 새로운 엔터티를 저장하거나 기존 엔터티를 업데이트.
findById(): 기본 키로 엔터티를 조회.
findAll(): 모든 엔터티를 조회.
deleteById(): 기본 키로 엔터티를 삭제.
count(): 전체 엔터티 개수 반환.
existsById(): 특정 엔터티가 존재하는지 확인.
@Autowired
private UserRepository userRepository;
public void example() {
// 새로운 사용자 저장
User user = new User();
user.setName("John");
userRepository.save(user);
// 모든 사용자 조회
List<User> users = userRepository.findAll();
// ID로 사용자 조회
User userById = userRepository.findById(1L);
// 사용자 삭제
userRepository.deleteById(1L);
// 전체 사용자수
userRepository.count();
// ID로 해당 사용자 존재여부
userRepository.existsById(1L);
}
Spring Data JPA는 메소드 이름만으로 쿼리를 자동으로 생성하는 쿼리 메소드 기능을 제공함. 메소드 이름을 규칙에 따라 작성하면, 해당 메소드에 맞는 SQL 쿼리가 자동으로 생성됨.
예)
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name); // 이름으로 사용자 조회
List<User> findByAgeGreaterThan(int age); // 나이가 특정 값 이상인 사용자 조회
List<User> findByNameAndAge(String name, int age); // 이름과 나이가 모두 일치하는 사용자 조회
}
findByName: 이름으로 사용자 조회.
findByAgeGreaterThan: 나이가 특정 값 이상인 사용자 조회.
findByNameAndAge: 이름과 나이가 모두 일치하는 사용자 조회.
| Keyword | Sample | JPQL snippet |
|---|---|---|
| Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
| Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
| LessThan | findByAgeLessThan | … where x.age < ?1 |
| LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
| After | findByStartDateAfter | … where x.startDate > ?1 |
| Before | findByStartDateBefore | … where x.startDate < ?1 |
| IsNull, Null | findByAge(Is)Null | … where x.age is null |
| IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age is not null |
| Like | findByFirstnameLike | … where x.firstname like ?1 |
| NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
| Not | findByLastnameNot | … where x.lastname <> ?1 |
| In | findByAgeIn(Collection ages) | … where x.age in ?1 |
| NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 |
| True | findByActiveTrue() | … where x.active = true |
| False | findByActiveFalse() | … where x.active = false |
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
Spring Data JPA는 JPQL(Java Persistence Query Language)과 nativeQuery를 지원하여, 복잡한 쿼리를 직접 작성할 수 있음.
// JPQL 쿼리
@Query("SELECT u FROM User u WHERE u.age > :age")
List<User> findUsersOlderThan(@Param("age") int age);
// 네이티브 SQL 쿼리
@Query(value = "SELECT * FROM users WHERE age > :age", nativeQuery = true)
List<User> findUsersByNativeQuery(@Param("age") int age);
@Query: 어노테이션을 사용하여 복잡한 JPQL 또는 네이티브 쿼리를 직접 정의할 수 있음. 기본 CRUD 외에 복잡한 쿼리를 작성해야 할 때 사용됨.
nativeQuery = true: 네이티브 SQL 쿼리임을 명시함.
Pageable 인터페이스는 Spring Data JPA에서 제공하는 페이징 처리와 정렬을 위한 기능으로, 대규모 데이터를 효율적으로 처리할 수 있도록 지원하는 페이징 요청 객체임. Pageable 인터페이스는 페이징 및 정렬 정보를 메소드 호출 시 인자로 전달하여, 데이터를 페이지 단위로 나누고, 특정 기준에 따라 정렬된 결과를 조회할 수 있게 해줌.
Pageable은 페이징 처리를 위한 정보를 담고 있는 객체로, 주로 페이징 요청을 나타냄.
페이징 처리 시, 클라이언트는 데이터를 페이지 단위로 요청하고, Pageable 인터페이스는 해당 페이지의 페이지 번호, 페이지 크기, 정렬 방식과 같은 정보를 담음.
이를 통해 개발자는 대량의 데이터를 페이지별로 나누어 클라이언트에 응답할 수 있으며, 필요한 만큼의 데이터만 조회하여 성능을 최적화할 수 있음.
Pageable 인터페이스는 페이징 처리에 필요한 다양한 정보를 제공하는 메소드를 포함하고 있음. 대표적인 메소드는 다음과 같음:
getPageNumber(): 현재 요청의 페이지 번호를 반환함. (0부터 시작)
getPageSize(): 한 페이지에 포함될 데이터의 크기(개수)를 반환함.
getOffset(): 데이터베이스 조회 시 시작 위치를 반환함. (예: LIMIT 쿼리의 시작점)
getSort(): 정렬 기준을 반환함. (정렬 방식이 없는 경우 UNSORTED)
hasPrevious(): 이전 페이지가 있는지 여부를 반환함.
Pageable을 구현한 기본 구현체는 PageRequest 클래스임. 이는 페이징 요청 객체를 생성할 때 자주 사용됨.
PageRequest 클래스는 Pageable 인터페이스의 구현체로, 페이지 번호, 페이지 크기, 정렬 기준을 생성할 수 있음. 페이징과 정렬을 함께 처리할 수 있도록 지원함.
PageRequest.of() 메소드
페이징 요청 객체를 생성할 때는 PageRequest.of() 메소드를 사용함. 이 메소드는 다음과 같은 매개변수를 받음:
int page: 조회할 페이지 번호 (0부터 시작).
int size: 한 페이지에 포함될 데이터의 크기(몇 개의 데이터를 가져올지).
Sort sort: 데이터를 정렬할 때 사용하는 정렬 기준(optional).
PageRequest 예시
// 1. 페이지 번호가 0이고, 페이지 크기가 10인 페이징 요청 (정렬 없음)
Pageable pageable = PageRequest.of(0, 10);
// 2. 페이지 번호가 1이고, 페이지 크기가 20이며, name 필드를 기준으로 오름차순 정렬
Pageable pageable = PageRequest.of(1, 20, Sort.by("name").ascending());
// 3. 페이지 번호가 0이고, 페이지 크기가 10이며, 여러 필드를 기준으로 정렬
Pageable pageable = PageRequest.of(0, 10, Sort.by("name").descending().and(Sort.by("age").ascending()));
페이지 번호는 0부터 시작함.
Sort는 여러 필드에 대해 복합 정렬도 가능하며, 필드 이름에 따라 오름차순(ascending) 또는 내림차순(descending)으로 정렬할 수 있음.
Pageable 인터페이스는 Spring Data JPA에서 페이징된 데이터를 조회할 때 주로 사용됨. JpaRepository에서 제공하는 메소드에 Pageable을 인자로 전달하여 페이징된 데이터를 쉽게 조회할 수 있음.
기본 페이징 메소드
Page<User> findAll(Pageable pageable);
위 메소드는 페이징된 결과를 Page<User> 객체로 반환함.
// 사용자 레포지토리에서 1페이지, 10개의 데이터를 오름차순 정렬로 조회
Pageable pageable = PageRequest.of(1, 10, Sort.by("name").ascending());
Page<User> page = userRepository.findAll(pageable);
List<User> users = page.getContent(); // 조회된 사용자 리스트
long totalUsers = page.getTotalElements(); // 전체 사용자 수
int totalPages = page.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = page.hasNext(); // 다음 페이지 여부
커스텀 쿼리에서 Pageable 사용
@Query 어노테이션과 Pageable을 함께 사용하여 커스텀 JPQL 쿼리에서 페이징 처리도 가능함.
@Query("SELECT u FROM User u WHERE u.age > :age")
Page<User> findUsersByAgeGreaterThan(@Param("age") int age, Pageable pageable);
해당 쿼리는 나이가 특정 값 이상인 사용자들을 페이징하여 반환함.
Pageable pageable = PageRequest.of(0, 5, Sort.by("name").ascending());
Page<User> users = userRepository.findUsersByAgeGreaterThan(20, pageable);
Spring Data JPA에서는 Pageable을 사용하여 데이터를 페이징 처리할 때, Page와 Slice라는 두 가지 인터페이스를 사용할 수 있음. 두 인터페이스 모두 페이징 처리를 위한 결과를 담고 있지만, 차이점이 있음.
Page 인터페이스
Page는 페이징된 전체 데이터 정보를 제공함. 예를 들어, 총 페이지 수, 총 데이터 수, 현재 페이지의 데이터 등의 정보를 포함함.
주로 findAll(Pageable pageable) 메소드를 사용할 때 반환됨.
Page<User> page = userRepository.findAll(pageable);
List<User> users = page.getContent(); // 현재 페이지의 데이터
long totalElements = page.getTotalElements(); // 전체 데이터 수
int totalPages = page.getTotalPages(); // 전체 페이지 수
Slice 인터페이스
Slice는 Page보다 가벼운 버전으로, 현재 페이지의 데이터와 다음 페이지가 있는지 여부만 제공함. 전체 데이터 수나 전체 페이지 수는 제공하지 않음.
이는 성능 최적화가 필요할 때 사용되며, 전체 데이터를 다 계산할 필요가 없을 때 유용함.
Slice<User> slice = userRepository.findAll(pageable);
List<User> users = slice.getContent(); // 현재 페이지의 데이터
boolean hasNext = slice.hasNext(); // 다음 페이지가 있는지 여부
페이지 번호는 0부터 시작
Spring Data JPA에서 페이지 번호는 0부터 시작함. 클라이언트에서 1 페이지를 요청하는 경우, 서버에서 0 페이지를 요청해야함을 기억해야 함.
성능 최적화
페이징은 대규모 데이터를 처리할 때 매우 유용하지만, 잘못 사용하면 성능 이슈가 발생할 수 있음. 예를 들어, 복잡한 조인 쿼리에서 페이징 처리를 적용할 때 N+1 문제가 발생할 수 있음. 이런 경우에는 JPQL을 최적화하거나, 필요한 필드만 조회하는 프로젝션을 고려해야 함.
Slice 사용 시 전체 데이터 개수 정보가 없음
Spring Data JPA는 자동으로 CRUD 메소드를 제공하므로, 개발자가 별도의 구현 없이도 데이터 조작 작업을 할 수 있음. 이를 통해 코드의 중복을 줄이고, 개발 시간을 절약할 수 있음.
쿼리 메소드를 통해 메소드 이름만으로 쿼리를 생성할 수 있음. 단순한 조회 작업이나 조건에 맞는 데이터 조회는 메소드 이름을 통해 처리할 수 있어 코드 가독성이 높아짐.
Spring Data JPA는 복잡한 쿼리가 필요한 경우, @Query를 통해 JPQL이나 네이티브 쿼리를 사용할 수 있어 성능 최적화나 고급 기능을 구현할 수 있음.
페이징 및 정렬 기능을 자동으로 제공하여 대규모 데이터를 처리할 때 유용함. 개발자는 간단한 설정으로 페이징 처리와 정렬을 적용할 수 있음.
Spring Data JPA는 Spring Boot와의 통합이 잘 되어 있어, 별도의 설정 없이도 자동 설정으로 빠르게 데이터베이스와 연동할 수 있음. 이는 개발 생산성을 크게 향상시킴.
Spring Data JPA는 단순한 CRUD 작업에 매우 유용하지만, 매우 복잡한 쿼리를 처리할 때는 한계가 있을 수 있음. 이 경우에는 네이티브 쿼리 또는 JPQL을 직접 작성해야 함.
대규모 데이터를 처리하거나 잘못된 매핑이나 지연 로딩 설정이 문제를 일으킬 경우 성능 문제가 발생할 수 있음. 성능 최적화와 관련된 부분은 주의 깊게 관리해야 함.
Spring Data JPA는 설정이 비교적 간편한 편이지만, 복잡한 엔터티 매핑을 처리하는 대규모 시스템에서는 초기 설정이 복잡해질 수 있음. 올바른 매핑 전략을 세워야 함.