
JPA (JAVA Persistence API)
자바 ORM 기술 표준
ORM(Object-relational mapping)
프레임 워크가 중간에서 매핑
- JPA는 APP과 JDBC 사이에서 동작
- 패러다임의 불일치 해결
JPA는 인터페이스 모음 -> Hibernate (구현체)
JPA 설정(Persistence.xml)
JPA 설정하기 위해선 Persistence.xml필요
- /META_INF/persistence.xml위치
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<!-- 자바 접근 정보-->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<!-- <property name="hibernate.hbm2ddl.auto" value="create" />-->
</properties>
</persistence-unit>
</persistence>
- persistence-unit name으로 이름 지정
- javax.persistence -> DB접근 정보
- hibernate -> 하이버네이트 전용 속성
- hibernate.dialect -> DB에 맞는 sql 문법 지정
ex) MySQLDialect, OracleDialect, H2Dialect
주의 사항
- 엔티티 매니저 팩토리는 애플리케이션 전체에서 공유
- 엔티티 매니저는 쓰레드간에 공유X (사용하고 버려야 한다)
- JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
JPQL
SQL을 추상화한 객체 지향 쿼리 언어
특징
- 객체를 중심으로 개발
- 검색시 테이블이 아닌 엔티티 객체를 대상으로 검색
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- 필요한 데이터를 DB에서 불러오려면 결국 검색 조건이 포함된 SQL 필요
영속성
- 객체와 관계형 데이터 베이스 매핑
- 영속성 컨텍스트
영속성 컨텍스트란?
-> 엔티티를 영구 저장하는 환경
-> DB와 App간에 중간 계층
장점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지원
- 변경 감지
- 지연 로딩
플러시란?
영속성 컨텍스트의 변경 내용을 DB에 반영 (영속성 컨텍스트를 비우지 않음)
플러시 과정
변경 감지 -> 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
-> 쿼리를 데이터베이스에 전송
영속성 컨텍스트를 플러시 하는 방법
- em.flush() : 직접 호출
- 트랜잭션 커밋 : 플러시 자동 호출
- JPQL 쿼리 : 플러시 자동 호출
준영속 상태
영속 상태의 엔티티를 영속성 컨텍스트에서 분리
- em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
- em.clear() : 초기화
- em.close() : 종료
Entity Mapping
기본 어노테이션
어노테이션 | 설명 |
---|---|
@Entity @Table | entity와 매핑할 테이블 지정 |
@Id | 데이터베이핑 PK와 매핑 |
@Column | 필드와 컬럼 매핑 |
@ManyToOne @JoinColum | 연관관계 매핑 |
Column 어노테이션
어노테이션 | 설명 |
---|---|
@Column | 컬럼 매핑 |
@Temporal | 날짜 타입 매핑 |
@Enumerated | JAVA enum 타입 매핑 (String사용) |
@Lob | BLOB(byte[]), CLOB(String, char[]) 매핑 |
@Transient | 특정 필드를 컬럼에 매핑하지 않음(메모리상에서만 임시로 값 보관하고 싶을 때) |
기본키 매핑
기본키 자동 생성 전략
- IDENTITY - 데이터 베이스에 위임
- SEQUENCE - 데이터베이스 시퀀스 오브젝트 사용 (Oracle, PostgreSQL, H2)
- TABLE - 키 생성용 테이블 사용
@Id // 직접 할당
// 자동 생성
// AUTO : 사용하는 DB에 따라 자동 지정 (기본값)
@GeneratedValue(strategy = GenerationType.AUTO)
// SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
equenceName = "MEMBER_SEQ", // 매핑할 데이터베이스 시퀀스 이름
initialValue = 1,
allocationSize = 1 // 시퀀스 한 번 호출에 증가하는 수(성능 최적화)
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator= "MEMBER_SEQ_GENERATOR")
private Long id;
...
// TABLE : 키 생성용 테이블 사용
@TableGenerator(name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ",
allocationSize = 1)
public class CLASSNAME {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
SequenceGenerator 설정 값 (Toggle)
속성 | 설명 | 기본 |
---|---|---|
name | 식별자 | 생성기 이름 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequenc |
initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL 생성 시 시작하는 수를 지정 | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
TableGenerator 설정 값 (Toggle)
속성 | 설명 | 기본 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnNa | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준 | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints(DDL) | 유니크 제약 조건 지정 |
권장하는 기본키 전략
-> Long형, 대체키, 기본 키 전략 // 비즈니스 로직 포함 x
연관 관계 매핑
고려 사항
- 다중성
- 단방향, 양방향
- 연관관계 주인
단방향, 양방향
테이블 : 외래키 하나로 연관관계를 관리 객체 : 서로 다른 단뱡향 관계 2개
한쪽만 참조하면 단방향 양쪽 서로 참조시 양방향
양방향 매핑 규칙
- 외래키가 있는 곳을 연관관계 주인으로 지정
- 연관관계 주인 값이 변경 시 외래키를 변경
- 연관관계 주인 -> 외래키 관리(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능 -> mappedBy 속성으로 주인 지정
주의할 점
- 값 추가 시 양쪽 모두 값 입력 -> 연관관계 편의 메소드 사용
- 무한루프 주의
- lombok toString 사용 x
- Controller에서 Entity 직접 반환x -> DTO로 변환해 사용
연관관계 편의 메소스
- 주인 setter + 반대쪽 객체에 값 넣어주는 부분 추가
- 새로운 함수로 작성해 setter함수와 구분
- 필요 시 제약조건 검사 구문 추가
- 한쪽에만 작성
...
public void addMember(Member member) {
member.setTeam(this);
members.add(member);
}
...
정리
- 테이블 설계 -> 연관관계 매핑은 단방향 매핑으로 완성 되어야 함
- 양방향 매핑은 반대 방향으로 조회 기능을 위해 추가
- JPQL에서 역방향 탐색할 일이 많음
다중성
연관관계 매핑을 위한 어노테이션
@ManyToOne @JoinColumn(name = "TEAM_ID") // N : 1
@OneToMany(mappedBy = "team") // 1 : N
@OneToOne // 1 : 1
@ManyToMany // N : M 사용x
- 보통 다대일 관계 사용 - 외래키가 있는 쪽이 연관관계 주인
- 일대일 관계 - 외래키에 유니크 제약조건 추가
일대일
위치 | 특징 | 장점 | 단점 |
---|---|---|---|
주(access가 많은) 테이블 | - 객체지향 개발자 선호 - JPA 매핑 편리 |
주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능 | 값이 없으면 외래 키에 null 허용 |
대상 테이블 | - 전통적인 데이터베이스 개발자 선호 | 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지 | 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨 |
@ManyToOne, @OneToMany 설정 값 (Toggle)
속성 | 설명 | 기본 값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다 | sequence_name |
상속 관계 매핑
객체는 상속을 지원하므로 모델링과 구현이 똑같지만, DB는 상속을 지원하지 않음으로 논리 모델을 물리 모델로 구현할 방법이 필요
유사한 개념으로는 슈퍼타입 서브타입 관계 있어 JPA에서는 상속 구조를 슈퍼타입 - 서브타입 관계에 매핑
DB의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법 3가지
- 각각의 테이블 -> 조인 전략
- 통합 테이블 -> 단일 테이블 전략
- 서브타입 테이블 -> 구현 클래스 마다 테이블 전략
// JOINED: 조인 전략
// SINGLE_TABLE: 단일 테이블 전략
// TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
// 부모 클래스에 어노테이션 추가 + 자식 클래스에서 부모 클래스 상속
@Inheritance(strategy=InheritanceType.XXX)
// 부모 클래스에 선언
// 하위 클래스를 구분하는 용도 (Default = DTYPE)
@DiscriminatorColumn(name=“DTYPE”)
// 자식 클래스에 선언
// 엔티티를 저장할 때 슈퍼타입 구분 컬럼에 저장할 값 지정 (Default : 클래스 명)
@DiscriminatorValue(“XXX”)
전략 | 장점 | 단점 |
---|---|---|
조인 전략 | - 테이블 정규화 - 외래 키 참조 무결성 제약조건 활용가능 - 저장공간 효율화 |
- 조회시 조인을 많이 사용, 성능 저하 - 조회 쿼리가 복잡함 - 데이터 저장시 INSERT SQL 2번 호출 |
단일 테이블 전략 | - 조인이 필요 없으므로 일반적으로 조회 성능이 빠름 - 조회 쿼리가 단순함 |
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용 - 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. - 상황에 따라서 조회 성능이 오히려 느려질 수 있다. |
구현 클래스 마다 테이블 전략 | - 서브 타입을 명확하게 구분해서 처리할 때 효과적 - not null 제약조건 사용 가능 |
- 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요) - 자식 테이블을 통합해서 쿼리하기 어려움 |
정리
- 기본적으로는 조인 전략 -> 중요, 복잡, 확장 가능성이 높은 경우
- 심플하고 확장 가능성이 낮은 경우 -> 단일 테이블 전략
@MappedSubperclass
객체의 입장에서 공통 매핑 정보가 필요할 때 사용
- 상속 받는 자식 클래스에 매핑 정보만 제공
- 조회, 검색이 불가능
프록시
연관된 객체를 처음부터 DB에서 조회하는 것이 아닌 실제 사용하는 시점에 조회 가능 (가짜 객체)
em.find() - DB를 통해서 실제 엔티티 객체를 조회하는 메서드 em.getReference() - DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회하는 메서드
특징
- 실제 클래스의 상속을 받아 만들어짐
- 프록시 객체는 실제 객체의 참조값(주소)를 가지고 있음
- 사용 시 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다
(JPA 내부에서 알아서 처리해준다.)
@트랜잭션 범위 밖에서 프록시 객체를 조회 (준영속 상태) 문제
초기화 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외) -> 해결책으로 Spring Boot에서 open-in-view 설정 true (영속성 컨텍스트(하이버네이트 세션)를 뷰 렌더링하는 시점까지 유지시키는 방법) https://kingbbode.tistory.com/27
실전 JPA강의에 해당 내용 강의 듣고 추가 정리
즉시 로딩, 지연 로딩
@ManyToOne(fetch = FetchType.LASY) // 지연 로딩
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
연관관계를 설정한 엔티티 조회 시 프록시 객체로 가져온다
즉시 로딩 사용 시 N+1 문제 발생 가능
JPA N+1 문제 해결 방법 및 실무 적용 팁 - 삽질중인 개발자
JPA N+1 발생원인과 해결방법 - Yun Blog
- 모든 연관관계에서 지연 로딩 사용
- @ManyToOne, @OneToOne, @XXXToOne 어노테이션들은 기본이 즉시 로딩(EAGER)
=> LAZY로 명시적으로 설정해서 사용 - JPQL fetch 조인, 엔티티 그래프 기능, Batch Size 사용
영속성 전이(CASCADE)
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들 때
연관관계 주인이 아닌 자식 객체에 설정
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- ** 참조하는 곳이 하나**일 때 사용
- 특정 엔티티가 개인 소유할 때 사용
(@OneToOne, @OneToMany 일 경우)
@OneToMany(CascadeType.ALL + orphanRemovel=true)
-> 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기 관리 가능 (도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용)
값 타입
JPA는 크게 엔티티타입, 값 타입으로 분류
엔티티 타입 : @Entity 정의, 데이터가 변해도 식별자로 추적 가능 값 타입 : 자바 기본 타입이나 객체, 식별자가 없어 추적 불가
값 타입 종류
생명주기를 엔티티에 의존
1. 기본 값 타입
- 값타입 공유 x -> 복사해 사용
- 생명주기를 엔티티에 의존
- 불변 객체로 만들어 사용
2. 임베디드(복합 값) 타입
- 새로운 값 타입을 정의할 수 있음
- 재사용성, 높은 응집도
- 해당 값 타입만 사용하는 메소드 만들 수 있음
- 값이 null이면 매핑한 컬럼은 모두 null
- 불변 객체로 만들어 사용해야 함
- setter 만들지 않으면 된다
- 값을 복사해 사용
- 값을 공유해 사용하기 위해선 엔티티를 만들어 사용
@Embeddable // 값 타입을 정의하는 곳에 사용
@Embedded // 값 타입을 사용하는 곳에 사용
@AttributeOverrides // 한 엔티티에서 같은 값 타입을 사용하면 어노테이션 사용해서 재정의
// 사용 예시
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
@AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
@AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
})
Address companyAddress;
3. 컬렉션 값 타입
- 컬렉션을 저장하기 위한 별도의 테이블 필요
- 일반 엔티티와 동일하게 지연로딩 사용 가능
- 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가짐
제약 사항
- 식별자 개념 없다.
- 변경 시 추적이 어려움
- 변경사항 발생 시, 값 타입 컬렉션에 있는 모든 값을 다시 저장
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키 구성 - null x, 중복 저장x
@ElementCollection // 컬렉션 값 타입 사용시 매핑
@CollectionTable // 테이블 설정
실무에 적용 시 상황에 따라 일대다 관계 고려
엔티티 vs 값 타입
-> 식별자 필요, 지속해서 값 추적 필요 시 엔티티
쿼리
종류
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접사용, MyBatis, SpringJdbcTemplate 함께 사용
-> 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
JPQL문법
- 엔티티 이름 사용(테이블 아님)
- 별칭 필수
반환 타입
// 반환 타입이 명확할 경우
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
// 반환 타입이 명확하지 않을 경우
Query query = em.createQuery("SELECT m.username, m.age from Member m");
결과 조회
query.getResultList() // 결과가 하나 이상일 때 리스트 반환
query.getSingleResult() // 결과가 하나
파라미터 바인딩
이름 , 위치 기준
// SQL
SELECT m FROM Member m where m.username=:username
query.setParameter("username", usernameParam);
프로젝션
프로젝션 대상으로 엔티티, 임베디드 타입, 스칼라 타입 가능
페이징 API
setFirstResult(int startPoint) // 조회 시작 위치
setMaxResults(int maxResult) // 조회할 데이터 수
서브 쿼리
- SELECT, WHERE, HAVING 절에서 서브 쿼리 사용 가능
- FROM 절 서브쿼리는 사용 불가
경로 표현식
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
상태 필드(state field) : 단순히 값을 저장하기 위한 필드 (ex: m.username)
연관 필드(association field) : 연관관계를 위한 필드
묵시적 내부 조인 발생
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)
주의 사항
- 컬렉션은 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 함
- 묵시적 조인 사용 x -> 명시적 조인 사용
페치 조인
SQL 조인 종류가 아닌 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회 (N+1 문제 해결책)
select m from Member m join fetch m.team
->
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
DISTINCT
- SQL에 DISTINCT를 추가
- 애플리케이션에서 같은 식별자를 가진 엔티티 중복 제거
한계
- 패치 조인은 기본적으로 모든 연관 그래프 가지고 오는 개념
- 패치 조인 대상에는 별칭을 줄 수 없다
- 둘 이상의 컬렉션은 패치 조인 할 수 없다
- 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.
다형성 쿼리
Type : 조회 대상을 특정 자식으로 한정
-- [JPQL]
select i from Item i
where type(i) IN (Book, Movie)
-- [SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)
TREAT : 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
-- [JPQL]
select i from Item i
where treat(i as Book).auther = ‘kim’
-- [SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’
Named 쿼리(정적 쿼리)
미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 어노테이션, XML 정의
- 어플리케이션 로딩 시점에 초기화 후 재사용, 쿼리 검증
Tip!!
-
JPA에서 타입 비교시 instance of(객체 비교) 사용
-
벌크 연산 수행 후 영속성 컨텍스트 초기화(em.clear()) 필수!
-
다대다 연관관계 사용 x -> 일대다, 다대일로 관계를 풀어서 사용
(보통 중간에 매핑 테이블 필요 ex - category - gategory_item - item) -
@Entity 클래스는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.
-
Entity는 기본 생성자 필요 -> protect 레벨로 생성 (추가로 필요한 내용 정리 예정)
@Entity
@NoArgsconstructor(access = AccessLevel.PROTECTED)
public class Member {
...
}