간략 JPA 소개
예전에는 sql을 한땀한땀 작성을 해야했다. 그런데 이제는 sql을 직접 만들 필요가 없어졌다. 바로 그것이 JPA
JPA는 자동화의 끝판왕이다. 그래서 JPA는 트랙터 같은 놈이다. 문제는 운전을 하려면 사용법을 알아야한다.
그런데 실무에서는 사용하기 어렵다.
- 객체와 테이블을 올바르게 매핑하고 설계하는 방법을 몰라서 ⇒ 삽질이 길어진다.
- JPA 내부 동작 방식 이해를 몰라서 ⇒ 원하는 방식대로 동작하지 않는다.
SQL 개발의 문제점
기존 개발방식은 SQL에 의존한 개발이 주를 이룬다.
필드를 하나 추가해도 상당히 많은 양의 코드를 변경해야한다. 혹은 놓치는 코드가 생긴다.
⇒ SQL 의존적인 개발을 피하기 어렵다
패러다임의 불일치
객체지향 → 추상화, 캡슐화, 은닉화 등등을 위해 존재함.
SQL → 데이터를 잘 저장하기 위해 존재함.
서로 다른 목적을 가지고 존재하다 보니 개발자는 직접 매퍼가 되어 SQL을 매번 새롭게 만들어야 하는 문제가 발생한다.
이러다 보니 개발자는 SQL매퍼가 된다.
그렇다고 객체답게 설계를 할수록 오히려 매핑 작업만 계속 늘어난다.
⇒ 진정한 의미의 계층 분할이 어렵다.
이렇다 보니 객체를 자바 컬렉션에 저장 하듯이 DB에 저장할 수는 없을까?
JPA(Java Persistence API)
ORM (Object-relational mapping)
객체 관계 매핑이라는 의미로 객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터 베이스대로 설계하고 이걸 매핑해주는 기술이다.
결국 객체지향 설계와 관계지향 설계의 패러다임을 연결시켜주는 기술이다.
생산성
저장 : jpa.resist(member)
조회 : Member member = jpa.find(memberId)
수정 : member.setName(”변경할 이름”)
삭제 : jpa.remove(member)
유지보수
DB에 컬럼이 추가되면 클래스에 해당 컬럼만 추가해주면 더이상 수정할게 없다.
JPA의 성능 최적화 기능
- 1차 캐시와 동일성(identity) 보장
String memberId = "100"; Member m1 = jpa.find(Member.class, memberId); // SQL Member m1 = jpa.find(Member.class, memberId); // Cache // m1 == m2
- 약간의 조회 성능 향상
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
transaction.begin(); // 트랜잭션 시작 em.persist(memberA); em.persist(memberB); em.persist(memberC); // 여기까지는 INSERT SQL을 데이터베이스에 보내지 않는다. // 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다. transaction.commit(); // 트랜잭션 커밋
- 버퍼링 기능을 말한다.
- 지연 로딩즉시 로딩은 JOIN SQL로 한번에 연관된 객체까지 미리 로딩하는것이를 통해서 필요한 방식에 따라 튜닝을 할 수 있다.
Member member = memberDAO.find(memberId); Team team = member.getTeam(); // 즉시로딩인 경우 이때 전부 가져옴 String teamName = team.getName(); // 지연로딩인 경우 필요할때 가져온다.
- 지연 로딩은 객제가 실제 사용될 때 로딩하는것.
JPA 시작하기
h2 설치하기 : https://www.h2database.com/html/main.html
<?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>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// EntityManagerFactory는 하나만 생성해서 애플리케이션 전체에서 공유
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// 쓰레드간 공유하면 안된다. 매번 요청마다 새로 만들어서 사용하고 버려야한다.
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 모든 데이터의 변경은 트랜잭션 안에서 사용해야한다.
tx.begin();
try{
//수정하는 경우:Member findMember = em.find(Member.class, 1L);
// findMember.setName("HelloJPA"); // 컬랙션을 다루듯이 함수만으로 저장된다.
Member member = new Member();
member.setId(2L);
member.setName("HB");
em.persist(member);
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
emf.close();
위와 같이 연결에 대해 트랜잭션을 생성하고 에러가 생기는 경우 롤백을 해주는 방식의 코드가 정석적인 코드이다. 그러나 스프링을 사용하게 되면 이러한 부수적인 작업을 알아서 처리해준다.
재밌는것은 JPA를 통해 객체를 들고와서 setName으로 프로퍼티를 바꿔주면 트랜잭션 커밋 시점에 변경된 데이터가 있는 경우 쿼리를 날려 업데이트 시켜준다.
실제로 쿼리가 실행되는 경우 어떻게 쿼리가 실행되었는지 확인가능하다.(persistence.xml에 옵션을 주어야한다.)
JPA 정리
- EntityManagerFactory 를 선언해야한다. 애플리케이션 전체에서 공유
- EntityManager를 선언해야한다. 매 요청마다 사용하고 버림
- EntityTransaction을 선언해야한다.(tx.begin, tx.commit, tx.rollback) 매 요청마다 트랜잭션이 반드시 필요하다.
- 이렇게 부수적인 모든것을 스프링에서 알아서 해준다.
영속성 관리
영속성 컨텍스트 : 엔티티를 영구 저장하는 환경
EntityManager.persist(entity)실행하는 경우 사실 DB에 저장하는게 아니라 엔티티컨텍스트라는 곳에 저장하는 것이다
// 비영속 상태
Member member = new Member();
member.setId(2L);
member.setName("HB");
// 영속 상태에 member를 저장한다
em.persist(member);
// 실제 쿼리가 날아가는 시점
tx.commit();
영속성 컨텍스트의 이점
- 1차 캐시
- find함수를 실행하면 바로 DB를 보는게 아니라 영속 컨텍스트 내부의 1차 캐시를 먼저 확인한다.(근데 1차 캐시는 하나의 트랜잭션에서만 유지되기 때문에 성능상 큰 이점을 가져가지는 않는다.)
Member findMember1 = em.find(Member.class, 101);
Member findMember2 = em.find(Member.class, 101); // 실제 쿼리 select 쿼리가 하나만 날아감. 왜냐하면 2번째 함수는 1차 쿼리에서 데이터를 가져오기 때문 tx.commit();
- 동일성 보장
- 똑같은 래퍼런스를 참조하듯이 영속 엔티티의 동일성을 보장한다.
Member findMember1 = em.find(Member.class, 101);
Member findMember2 = em.find(Member.class, 101);
System.out.println(findMamber1 == findMember2);// true
- 트랜잭션을 지원하는 쓰기 지연
- persist()함수를 사용하게 되면 해당 insert명령의 데이터가 쓰기지연 SQL저장소에 쌓이게 되고 commit()함수가 실행되는 시점에 DB로 한번에 INSERT문을 날린다.
- (*만약 jdbc.batch 옵션을 주면 여러개의 SQL을 한번에 날려줄 수 있다. 잘 사용하면 성능상의 이점을 가져갈 수 있다.)
transaction.begin();
em.persist(memberA);
em.persist(memberB); // 여기까지 데이터를 영속성 엔티티에 쌓아놓는다. // 커밋하는 순간 데이터 베이스에 Insert Sql을 보낸다.
transaction.commit();
- 변경 감지
- 커밋시에 flush함수가 호출되고 우선 엔티티와 스냅샷을 비교한다. 스냅샷은 값을 읽어온 최초시점의 데이터를 말한다. 이때 데이터가 바뀐것을 확인하면 UPDATE SQL을 생성한다.영속성 컨텍스트의 변경내용을 데이터베이스에 반영em.flush() : 직접 호출(쿼리를 먼저 날리고 싶은경우 사용하면 된다.)주의
- 영속성 컨텍스트를 비우지 않는다.
- 영속성 컨텍스트의 변경내용을 데어터베이스에 동기화
- 트랜잭션이라는 작업단위가 중요 → 커밋 직전에만 동기화 하면됨
Member member = new Member("aa", 1);
em.persist(member); em.flush(); // 먼저 INSERT를 날리고 싶으면 flush를 호출하면 DB에 쿼리를 날린다. // 1차 캐쉬는 유지되고 단지 SQL을 날리도록 하는것 // code tx.commit();- 트랜잭션 커밋 - 플러쉬 자동호출
- 플러쉬 발생 → 변경감지(스냅샷과 비교) → 수정된 엔티티 쓰기 지연 SQL 저장소에 등록 → 쓰기 지연 SQL저장소의 쿼리를 데이터베이스에 전송
- 플러쉬
Member member = em.find(Member.class, 1);
member.setName("ZZZ"); // 바로 쿼리가 날아간다. // em.persist(member); 이걸 안해도 된다.
tx.commit();
- 지연 로딩
준영속 상태
영속 상태의 엔티티가 영속성 컨텍스트와 분리된 상태 → 영속성 컨텍스트가 지원하는 기능을 사용하지 못한다.
em.detach(): (상태전환)JPA에서 더이상 관리를 하지 않게 된다. 따라서 영속성 컨텍스트에 대한 모든 데이터가 사라진다.
em.clear() : (초기화)영속성 컨텍스트를 통째로 지워버린다.
em.close() : (종료) 영속성 컨텍스트 종료