Ian's Archive 🏃🏻

thumbnail
자바 ORM 표준 JPA 프로그래밍 (기본편)
JPA
2022.09.18.

JPA (JAVA Persistence API)

자바 ORM 기술 표준

ORM(Object-relational mapping)

프레임 워크가 중간에서 매핑

  • JPA는 APP과 JDBC 사이에서 동작
  • 패러다임의 불일치 해결

JPA는 인터페이스 모음 -> Hibernate (구현체)

JPA 설정(Persistence.xml)

JPA 설정하기 위해선 Persistence.xml필요

  • /META_INF/persistence.xml위치
copyButtonText
<?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 특정 필드를 컬럼에 매핑하지 않음(메모리상에서만 임시로 값 보관하고 싶을 때)

기본키 매핑

기본키 자동 생성 전략

  1. IDENTITY - 데이터 베이스에 위임
  2. SEQUENCE - 데이터베이스 시퀀스 오브젝트 사용 (Oracle, PostgreSQL, H2)
  3. TABLE - 키 생성용 테이블 사용
copyButtonText
@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함수와 구분
  • 필요 시 제약조건 검사 구문 추가
  • 한쪽에만 작성
copyButtonText

...

public void addMember(Member member) {
  member.setTeam(this);
  members.add(member);
}

...

정리

  • 테이블 설계 -> 연관관계 매핑은 단방향 매핑으로 완성 되어야 함
  • 양방향 매핑은 반대 방향으로 조회 기능을 위해 추가
  • JPQL에서 역방향 탐색할 일이 많음

다중성

연관관계 매핑을 위한 어노테이션

copyButtonText
@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가지

  1. 각각의 테이블 -> 조인 전략
  2. 통합 테이블 -> 단일 테이블 전략
  3. 서브타입 테이블 -> 구현 클래스 마다 테이블 전략
copyButtonText
// 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강의에 해당 내용 강의 듣고 추가 정리

즉시 로딩, 지연 로딩

copyButtonText
@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)

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들 때

연관관계 주인이 아닌 자식 객체에 설정

copyButtonText
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

  • ** 참조하는 곳이 하나**일 때 사용
  • 특정 엔티티가 개인 소유할 때 사용
    (@OneToOne, @OneToMany 일 경우)
copyButtonText
@OneToMany(CascadeType.ALL + orphanRemovel=true)

-> 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기 관리 가능 (도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용)

값 타입

JPA는 크게 엔티티타입, 값 타입으로 분류

엔티티 타입 : @Entity 정의, 데이터가 변해도 식별자로 추적 가능 값 타입 : 자바 기본 타입이나 객체, 식별자가 없어 추적 불가

값 타입 종류

생명주기를 엔티티에 의존

1. 기본 값 타입

  • 값타입 공유 x -> 복사해 사용
  • 생명주기를 엔티티에 의존
  • 불변 객체로 만들어 사용

2. 임베디드(복합 값) 타입

  • 새로운 값 타입을 정의할 수 있음
  • 재사용성, 높은 응집도
  • 해당 값 타입만 사용하는 메소드 만들 수 있음
  • 값이 null이면 매핑한 컬럼은 모두 null
  • 불변 객체로 만들어 사용해야 함
    • setter 만들지 않으면 된다
    • 값을 복사해 사용
    • 값을 공유해 사용하기 위해선 엔티티를 만들어 사용
copyButtonText
@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
copyButtonText
@ElementCollection // 컬렉션 값 타입 사용시 매핑

@CollectionTable // 테이블 설정

실무에 적용 시 상황에 따라 일대다 관계 고려

엔티티 vs 값 타입
-> 식별자 필요, 지속해서 값 추적 필요 시 엔티티

쿼리

종류

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접사용, MyBatis, SpringJdbcTemplate 함께 사용
    -> 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요

JPQL문법

  • 엔티티 이름 사용(테이블 아님)
  • 별칭 필수

반환 타입

copyButtonText
// 반환 타입이 명확할 경우
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

// 반환 타입이 명확하지 않을 경우
Query query = em.createQuery("SELECT m.username, m.age from Member m");

결과 조회

copyButtonText
query.getResultList()   // 결과가 하나 이상일 때 리스트 반환
query.getSingleResult() // 결과가 하나

파라미터 바인딩

이름 , 위치 기준

copyButtonText
// SQL
SELECT m FROM Member m where m.username=:username

query.setParameter("username", usernameParam);

프로젝션

프로젝션 대상으로 엔티티, 임베디드 타입, 스칼라 타입 가능

페이징 API

copyButtonText
setFirstResult(int startPoint) // 조회 시작 위치
setMaxResults(int maxResult)   // 조회할 데이터 수

서브 쿼리

  • SELECT, WHERE, HAVING 절에서 서브 쿼리 사용 가능
  • FROM 절 서브쿼리는 사용 불가

경로 표현식

copyButtonText
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 문제 해결책)

copyButtonText
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

  1. SQL에 DISTINCT를 추가
  2. 애플리케이션에서 같은 식별자를 가진 엔티티 중복 제거

한계

  • 패치 조인은 기본적으로 모든 연관 그래프 가지고 오는 개념
  • 패치 조인 대상에는 별칭을 줄 수 없다
  • 둘 이상의 컬렉션은 패치 조인 할 수 없다
  • 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.

다형성 쿼리

Type : 조회 대상을 특정 자식으로 한정

copyButtonText
-- [JPQL]
select i from Item i
where type(i) IN (Book, Movie)

-- [SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

TREAT : 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용

copyButtonText
-- [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 레벨로 생성 (추가로 필요한 내용 정리 예정)

copyButtonText
@Entity
@NoArgsconstructor(access = AccessLevel.PROTECTED)
public class Member {
  ...
}

Reference

Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.