JPA 가 필요한 이유
- SQL 중심적인 개발의 문제점을 해결해준다.
- CRUD와 같은 반복적인 코드를 지원해준다.
- 개발자는 객체지향적으로 개발을 할 수 있다.
- 필드값이 추가된다거나 변경되는 상황이 발생했을때, 쿼리문을 전부 바꿔주는 작업을 줄여준다.
- 패러다임의 불일치를 해결해준다.
- 1차 개시와 동일성 보장 --> 신뢰도 보장
- 트랜잭션을 지원하는 쓰기 지연 지원
- 지연 로딩 지원 --> 성능 보장
객체 vs 관계형 데이터베이스
- 상속 관점 ( 객체 상속 관계 vs Table 슈퍼타입 서브타입 관계)
- 연관관계 관점 ( 단방향 vs 양방향)
- 데이터 타입 관점
- 데이터 식별 방법 ( 같은 참조값 기준 같은 값 vs 다른 값)
패러다임의 불일치를 해결해 줄 수 있는 기술 "JPA"
- 위에서 말한 객체 와 관계형 데이터베이스 사이에서 발생하는 패러다임의 불일치를 해결해 줄 수 있는 무기이다.
- Java Persistence API의 약자로 자바 진영의 ORM 기술 표준이다.
- JAVA 애플리케이션 --> JPA --> JDBC API --> DB --> 결과 반환 순으로 동작한다.
JPA는 어떻게 만들어 졌을까?
- EJB로 부터 하이버네이트로 발전되면서 JPA가 만들어졌다.
- JPA 표준 인터페이스는 대표적으로 3가지 구현체를 가진다.
- Hibernate
- EclipseLink
- DataNucleus
지연로딩 vs 즉시로딩
- 지연로딩
- 객체가 실제 사용될 때 로딩
- 장점: 사용될 때 로딩하기 때문에 로딩시간이 짧다.
- 단점: 매번 사용된다면 쿼리문이 2번 발생함으로 즉시로딩보다 비효율적이다.
- 즉시로딩
- JOIN SQL로 한번에 연관된 객체까지 미리 조회
- 장점: 자주 사용될 경우 Join문으로 쿼리 한번에 호출함으로 지연로딩보다 성능이 좋을 수 있다.
- 단점: 객체를 사용하지 않을 때도 로딩하기 때문에 성능이슈가 발생할 수 있다.
- 하지만 N+1문제가 발생할 수 있다.
- 실무에서는 즉시로딩보다 지연로딩을 선호한다.
실습
- pom.xml
?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jpa-basic</groupId>
<artifactId>ex1-hello-jpa</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- JPA 하이버네이트 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.3.10.Final</version>
</dependency>
<!-- H2 데이터베이스 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
</dependency>
</dependencies>
- 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>
데이터베이스 방언
- 방언 : SQL 표준을 지키지 않는 특정 데이터베이스만의 고유한 기능
- hibernate.dialect 속성에 지정
- H2 : org.hibernate.dialect.H2Dialect
- Oracle 10g : org.hibernate.dialect.Oracle10gDialect
- MySQL : org.hibernate.dialect.MySQL5InnoDBDialect
- 하이버네이트는 40가지 이상의 데이터베이스 방언 지원
entityManagerFactory, entityManager
- JPA 구동 방식 : Persistence --> EntityManagerFactory --> EntityManager
- entityManagerFactory
- 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
- 만드는 비용이 상당히 큼
- 한 개만 만들어서 어플리케이션 전체에서 공유하도록 설계
- 여러 스레드가 동시에 접근해도 안전, 서로 다른 스레드 간 공유 가능
- entitManaer
- 여러 스레드가 동시에 접근하면 동시성 문제 발생
- 스레드간 절대 공유하면 안된다
- 데이터베이스 연결이 필요한 시점까지 커넥션을 얻지 않는다
- JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
JPQL
- JPQL : Java Persistence Query Language
- 왜 필요한가? : a.getB().getC() 이런식으로 찾는 것은 한계가 있다
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다
- 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다
- 그래서 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다
- SQL과 문법이 유사하고, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN등을 지원한다
- JPQL은 엔티티 객체를 대상으로 쿼리를 질의하고
- SQL은 데이터베이스 테이블을 대상으로 쿼리를 질의한다.
영속성 컨텍스트
- 영속성 컨텍스트란? : 엔티티를 영구 저장하는 환경을 뜻한다.
- 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.
- 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 엔티티 메니저를 생성해서 영속성 컨텍스트에 접근, 관리할 수 있다.
엔티티의 생명주기
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed): 영속성 컨텍스트에 저장된 상태
- 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태
- 영속성 컨텍스트의 관리를 받는다
em.persist(member);
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed): 삭제된 상태
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제
em.remove(member);
영속성 컨텍스트의 장점
- 동일성 보장
- 쓰기 지연 지원
- 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 INSERT SQL을 모아둔다
- 트랜잭션을 커밋하는 시점에서 모아둔 쿼리를 한번에 DB에 보냄
- 변경 감지 지원
- JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터를 변경
- 개발자는 변경 후 다시 저장하는 작업을 할 필요가 없다
- 변경 감지의 흐름은 아래와 같다.
-
- 트랙잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
-
- 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
-
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
-
- 쓰기 지연 저장소의 SQL을 플러시한다.
-
- 데이터베이스 트랜잭션을 커밋한다.
flush (플러시)
- 플러시란? : 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
- 플러시하는 방법
-
- em.flush()
-
- 트랙잭션 커밋시 자동 호출
-
- JPQL 쿼리 실행시 자동 호출
- 플러시가 진행되는 흐름
-
- 변경 감지가 동작해서 스냅샷과 비교해서 수정된 엔티티를 찾는다.
-
- 수정된 엔티티에 대해서 수정 쿼리를 만들거 SQL 저장소에 등록한다.
-
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
엔티티 매핑
- 객체와 테이블 매핑
- 필드와 컬럼 매핑
- 기본 키 매핑
- 연관관계 매핑
데이터베이스 스키마 자동 생성
필드와 컬럼 매핑
- @Column
- @Column(name = "name") : DB 컬럼명 설정
- @Column(updatable = false) : 변경되어도 DB에 반영도지 않는다.
- @Column(nullable = false) : NOT NULL 제약조건
- @Column(columnDefinition = "varchar(100) default 'EMPTY'") : 직접 컬럼 정보를 작성
- @Enumerated
- EnumType.ORDINAL : enum 순서를 데이터베이스에 저장
- EnumType.String : enum 이름을 데이터베이스에 저장
- 거의 필수적으로 EnumType.String을 사용한다
- @Temporal
- java.util.Date, java.util.Calender
- TemporalType enum class는 아래와 같이 세가지 타입이 존재
- DATE: DB의 date타입과 매핑 (2021-07-07)
- TIME: DB의 time타입과 매핑 (10:33:32)
- TIMESTAMP : DB의 timestamp와 매핑(2020-07-07 10:33:32)
- Java 8이상인 경우 LocalDate(date), LocalDateTime(timestamp)로 생략이 가능하다.
- @Lob
- DB에서 varchar를 넘어서는 큰 내용을 넣고 싶은 경우에 사용
- DB의 BLOB, CLOB과 매핑
- BLOB : byte[], java.sql.BLOB
- CLOB : String, char[], java.sql.CLOB
- @Transient
- 특정 필드를 컬럼에 매핑하지 않음
- DB에 관계없이 메모리에서만 사용하고자 할때 사용
- 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
- DB에 저장, 조회되지 않는다.
연관관계 매핑
- 객체 연관관계 vs 테이블 연관관계
- 객체는 참조로 연관관계를 맺습니다
- 테이블은 외래 키로 연관관계를 맺습니다
- 객체의 연관관계는 단방향입니다
- 테이블의 연관관계는 양방향입니다
- 다대일 단방향 매핑
- 가장 많이 사용하는 연관 관계
- 다대일의 반대는 일대다
- 다대일 양방향 매핑
- 외뢰 키가 있는 쪽이 연관관계 주인
- 양쪽을 서로 참조
- 일대다 단방향 매핑
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
- @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
- 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 UPDATE SQL 실행
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!!
- @JoinColumn
- name : 매핑할 외래 키 이름
- referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
- foreignKey(DDL) : 외래 키 제약조건을 직접 지정
연관 관계의 주인
- 연관 관계의 주인이란? :두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.
- 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다.
- 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.
- 주인은 mappedBy 속성을 사용하지 않는다.
주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
- 연관관계의 주인은 외래 키가 있는 곳으로 설정해야 한다.
- 따라서 @ManyToOne은 항상 연관관계의 주인이 된다. @OneToMany는 주인이 되지 않는다.
- 그러므로 @ManyToOne에는 mappedBy 속성이 없습니다. mappedBy속성은 @OneToMany에서 가진다.
편의 메소드
- 편의 메소드? : 양방향 연관에서 어느 한쪽만 동기화 되지 않도록 편의 메소드를 구현해 두는 것이 좋다. (맴버에 팀을 등록하면 팀에도 맴버를 등록해줘야된다)
- 보통 주인쪽에 구현해둔다.
- 예시는 아래와 같다.
public class Member{
private Team team;
public void setTeam(Team team) {
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
- 위와 같이 Member에 setTeam이라는 편의 메소드를 구현해 둠으로써
- 연관된 객체들 끼리의 동기화를 보장해 준다.
프록시
- 프록시란? : JPA는 객체지향언어와 관계형 데이터베이스의 패러다임 불일치를 해결하는 기술이다.
- 객체와 다르게 RDB의 경우 참조하는 외래키를 이용해서 테이블간의 조인 쿼리를 작성해서 탐색해야 한다.
- 그렇기 때문에 연관된 객체를 쓰일지 안 쓰일지도 모르는 체 전부다 가져와야하는 문제가 있다.
- 이러한 문제를 프록시라는 기술로 해결한다.
- 프록시는 실제 연관된 객체를 즉시 join을 통해 DB로부터 가져오는 것이 아니라 앞서 프록시 객체를 두어서 실제 사용되는 시점에 DB를 조회해서 가져오는 기술이다.
- 프록시 객체는 실제 객체에 대한 참조를 보관하고 있다가 객체가 사용돌 때 프록시 객체가 실제 객체의 메소드를 호출하는 식으로 사용된다.
- 프록시 객체가 실제 객체에 대한 참조가 없을 때에는 영속성 컨택스트에 실제 객체를 조회하고 영속성 컨택스트에도 없다면 DB조회를 해서 실제 객체를 생성해준다.
- 프록시 객체의 초기화 : 프록시객체가 실제 엔티티 객체를 생성하는 작업을 프록시 객체의 초기화라고 한다.
프록시의 특징
- 처음 사용할 때 한 번만 초기화 된다.
- 프록시 객체는 원본 엔티티를 상속받는 객체이므로 타입 체크를 잘해야한다.
- 영속성 컨텍스트에 찾는 객체가 있으면 DB를 조회할 필요가 없기 때문에 getReference()로 프록시를 가져오려고 해도 실제 엔티티가 조회된다.
- 초기화는 영속성 컨텍스트의 도움을 받아야한다. 만약 준영속상태의 프록시를 초기화하면 예외가 발생한다.
- PersistenceUnitUtil.isLoaded(object entitry) 메소드로 프록시 객체의 초기화 여부를 알 수 있다.
즉시로딩
지연로딩
- @ManyToOne(fetch = FetchType.LAZY) 이렇게 속성을 정의하면 된다.
- member만 필요할 때는 member만 조회하고 team은 프록시객체로 가지고 있다가 추후에 member.getTeam()으로 팀이 요구될 때 실제 객체를 영속성컨텍스트에서 찾아온다.
- 만약 영속성컨텍스트에 이미 team객체가 있으면 프록시객체가 아닌 실제 객체를 사용한다.
즉시로딩 vs 지연로딩
- 지연로딩이 즉시로딩보다 장점이 많은 것은 사실이다.
- 하지만 즉시로딩 또한 어떤점에서는 최적화에 있어서 유리하다.
- 그렇기 때문에 개발단계에서 모든 연관관계에 지연로딩을 사용하고, 출시버전배포 이전에 필요한 부분에 즉시로딩을 적용하면 최적화된다.
CASCADE
- 영속성 전이가 필요한 이유? : 만약 Team이라는 엔티티가 있고, Member 20개가 속해 있다고 생각해 보자. 이럴 경우 어떠한 수정이 발생하면 20개의 쿼리를 날려야 한다. 그렇기때문에 자동적으로 영속성 켄텍스트에 관리될 수 있도록 전이시키는 작업이 필요하다.
- 종류
- ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH 가 있다.
- 여러 속성을 지정할 수도 있다. --> cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
고아 객체
- JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이 있다.
- 부모엔티티의 컬렉션에서 자식엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 하는 것이다.
- 특정 엔티티가 다른 엔티티를 개인 소유할 때만 사용해야한다.
Spring Data Jpa
Spring Data JPA란?
- Spring Data JPA는 Spring에서 제공하는 모듈이다.
- JPA를 한 단계 추상화시킨
Repository
라는 인터페이스를 제공함으로써 이루어진다.
- Spring Data JPA의
Repository
의 구현에서 JPA를 사용하고 있다.
- CRUD 처리를 위한 공통 인터페이스 제공한다.
- 공통 인터페이스 종류는 대략 아래와 같다.
- count, delete, deleteAll, deleteAll, deleteById, existsById, findById, save.
- 공통 메소드가 아닐 경우에도 스프링 데이터 JPA가 메소드 이름을 분석해서 JPQL을 실행할 수 있다.
JPA vs Data JPA
JPA만으로 Repository를 구현한다면?
@Repository
public class TeamJpaRepository {
@PersistenceContext
private EntityManager em;
public Team save(Team team) {
em.persist(team);
return team;
}
public void delete(Team team) {
em.remove(team);
}
public List<Team> findAll() {
return em.createQuery("select t from Team t", Team.class)
.getResultList();
}
public Optional<Team> findById(Long id) {
Team team = em.find(Team.class, id);
return Optional.ofNullable(team);
}
public long count() {
return em.createQuery("select count(t) from Team t", Long.class)
.getSingleResult();
}
}
- 위와 같이 JPA만으로 Repository를 구현할 수 있다.
- 이 방식도 나쁘진 않다.
- 하지만 Data JPA의 힘을 빌리면...
Reference