kiwi

JPA소개와 작동 매커니즘

by 키위먹고싶다

jpa

SQL 중심적인 개발의 문제점 

 

객체지향 언어와 관계형DB를 사용한다면 객체를 관계형 DB에 저장하고 관리한다는 것이다. 

 

그런데 DB는 SQL만 알아 듣기 때문에 객체를 SQL로 바꾸다보면 객체지향적인 설계보다 SQL코드가 대부분인 SQL중심적인 개발의 문제점이 있다.  또한 애초에 객체와 관계형 데이터베이스 자체는 패러다임이 맞지 않는다. 

 

객체를 저장하는 방법은 File이나 여러가지가 있지만 현실적인 대안은 관계형 데이터베이스이므로 객체를 sql로 변경해야 한다. 개발자는 거의 sqlMapper의 역할을 하고 있는 것이다. 

 

객체와 RDB는 차이점이 있다. 

  • 상속 : 자바에는 상속이라는 개념이 있지만 RDB는 없다. Table 슈퍼타입 서브타입 관계를 사용할 수 는 있다. 
  • 연관관계 : 객체는 참조(래펴런스, get() ... )를 사용해서 연관 객체를 가져올 수 있지만 RDB는 PK나 FK로 조인을 통해 연관데이터를 가져올 수 있다.  
  • 데이터 타입
  • 데이터 식별 방법

 

아이템 객체가 있고 자식 객체인 앨범, 영화, 책 객체를 생각해보자. 객체를 생각해보자. 만약 앨범을 저장하려면 아이템을 INSERT하는 문장과 앨범을 INSERT하는 문장을 반복해야 한다. 조회할때는 또 어떤가. 각각 테이블에 따른 조인SQL을 작성하고 각각의 객체를 생성해야 한다. 그래서 DB에 저장할 객체는 상속 관계를 사용하지 않는다. 

 

자바 컬렉션에 저장한다고 생각해보자.

'list.add(album)'; 

 

조회는?

Album album = list.get(albumId); //자식타입 조회

Item item = list.get(albumId); //다형성으로 부모타입으로 조회 

 

자바 컬렉션에서 데이터를 조회할때나 RDB에서 조회할때 비슷한게 많다. 

 

결론 : 객체지향적인 모델링을 하면 할 수록 매핑 작업만 계속 늘어난다. 객체를 자바 컬렉션에 저장하는 것처럼 DB에 저장할 수 없을까? 

 

 


 

JPA소개

 

JPA

- Java Persistence API

- 자바 진영의 ORM기술 표준

- JPA는 인터패이스의 모음이다. (까보면 다 인터페이스밖에 없음 구현체 80퍼센트 정도가 하이버네이트임)

 

 

ORM

- Object relational mapping(객체 관계 매핑) 의 약자로, 객체는 객체대로 설계하고 관계형 데이터베이스는 관계형 데이터베이스대로 설계하고 ORM프레임워크가 중간에서 매핑한다. 대중적인 언어에는 대부분 ORM기술이 존재한다. 

 

 

동작 과정

 

JPA는 자바 애플리케이션과 JDBC사이에서 동작한다. 

- 자바가 객체를 생성해서 JPA보낸다.

- JPA가 객체를 분석하고 쿼리를 생성한다. 

- JPA는 생성된 쿼리를 JDBC API를 사용해서 DB와 통신한다.  

- 패러다임의 불일치를 해결할 수 있다. !!

 

JPA장점

 

- 생산성 측면(JPA와 CRUD)

  •  저장 : jpa.persist(member)
  •  조회 : Member member = jpa.find(memberId)
  •  수정 : member.setName("변경할 이름")
  •  삭제 : jpa.remove(member)

마치 자바 컬렉션에 객체를 꺼내고 변경하고 조회하는 것과 비슷함. 수정 부분을 얘기하자면 자바 List에 값을 수정하고 그 값을 다시 저장하지 않는것처럼 JPA도 마찬가지로 수정하고 그 값을 다시 저장할 필요가 없음. 

 

- 유지보수 측면

기존에는 필드가 변경되면 모든 sql을 수정해야 했다. (insert쿼리, select쿼리, update쿼리 등등...) 예를 들어

(String phNum -> String tel) 자바 필드가 변경되면 쿼리문도 다 phNum -> tel로 변경해야 한다. 그러나 JPA를 사용하면 필드만 변경하면 되고 쿼리문을 수정할 필요가 없다. SQL은 JPA가 처리해주기 때문이다. 

 

- 패러다임의 불일치 해결

상속, 연관관계, 객체 그래프 탐색, 비교... 

 

결론 : ORM은 객체와 RDB 두 기둥위에 있는 기술!!!

 


 

JPA구동 방식

JPA는 Persistence클래스에서 persistence.xml에서 설정정보를 읽어서 EntityManagerFactory클래스를 만든다.  

 

 

 

pox.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>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <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>

</project>

Maven을 사용해서 pox.xml에 JPA하이버네이트와 H2데이터베이스 라이브러리 설정을 추가한다.

 

 

 

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>

JPA의 설정파일인 persistence.xml을 /META-INF아래에 추가한다.

여기서 중요한것은 persistence-unit의 name이다. 여기서는 'hello'로 지정했다. 

 

참고로 javax.persistence로 시작하는것은 JPA표준 속성이며

hibernate로 시작하는것은 하이버네이트 전용 속성이다. 

 

 

* 데이터베이스 방언

필수속성인 hibernate.dialect를 보면 H2Dialect이다. 하이버네이트는 40가지 이상의 데이터베이스를 지원하는데 그 이유는  JPA는 특정 데이터베이스에 종속하지 않기 때문이다. 예를 들어 mySql에서 사용하는 VARCHAR와 oracle의 VARCHAR2같이 가변문자를 표현하는 방법이나 LIMIT이나 ROWNUM과 같은 페이징과 같은 SQL표준을 지키지 않는 특졍 데이터베이스들만의 고유한 기능을 방언이라고 하는데 하이버네이트는 여러 데이터베이스 방언을 지원한다고 보면 된다. 

 

 

 

그리고 H2데이터 베이스에 id와 name컬럼을 가진 member테이블을 만든다.

 

 

Member.java

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

객체를 생성하고 테이블을 매핑하기 위해 @Entity를 붙혀서 JPA가 관리할 객체임을 알려주고

@Id를 붙혀서 데이터베이스의 PK를 매핑시킨다. 

 

 

 

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(2L);
            member.setName("HelloB");

            em.persist(member);

            tx.commit();

        } catch (Exception e) {

            tx.rollback();

        } finally {

            em.close();

        }

        emf.close();
    }

}

 

메인 메서드 실행시키면 첫줄에서 Persistence클래스가 persistence.xml에서 내가 설정한 persistence-uint name인 'hello'를 읽어서 EntityManagerFacotry를 만들고 객체 emf를 얻는다. 

 

그리고 emf에서 EntityManager인 em객체를 얻는다. 

트랜잭션을 맞추기 위해 트랜잭션을 시작하고 member객체를 생성해서 EntityManager 인스턴스를 통해 persist하면 h2DB에 id가 2이고 name이 'HelloB'가 추가된다. 근데 commit하기 전까지는 h2에 저장되지 않고 커밋을 실행하면 그때 DB에 저장된다. 만약 예외가 터지면 트랜잭션을 롤백한다. 

 

콘솔창을 보자.

 

하얀글씨를 자세히 보면 insert쿼리문이 보이는데 저것이 보이는 이유는 persistence.xml파일에서 'hibernate.show_sql'속성을 추가했기 때문이다. 

 

 

디비를 확인하자!!

우리가 추가한 member들이 잘 들어가있다. 

 

이번에는 조금 다르게 저장된 멤버의 이름을 바꿔보겠다.

 

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        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");

            tx.commit();

        } catch (Exception e) {

            tx.rollback();

        } finally {

            em.close();

        }

        emf.close();
    }

}

em.find해서 엔터티 클래스와 PK속성을 넣고 멤버를 찾는다. 그리고 그 객체의 값을 변경하였다.

실행하면 다음과 같은 결과가 나온다.

 

 

그냥 값을 찾아서 setName()만 했을뿐인데 select쿼리와 update쿼리가 실행된다. 처음 em.persist를 통해 저장한것도 아닌데 어떻게 update를 할까?

 

그이유는 우리가 자바 컬렉션을 다루듯이 설계되었기 때문이다. 

어떻게 객체 값만 바꿨는데 이렇게 될까? JPA를 통해서 앤터티를 가져오면 JPA가 변경이 되었는지 안되었는지 트랜잭션을 커밋하는 시점에 변경사항을 다 체크하고 만약 변경사항이 존재하면 update쿼리를 만든다. 그리고 커밋되는 시점에 update쿼리가 나간다.  

 

*주의

EntityManagerFactory는 웹 애플리케이션이 작동될때 DB당 1개만 생성이 되고 애플리케이션 전체에서 공유하지만 EntityManager는 고객의 요청이 올때마다 생성되고 close()하면 사라진다. 그러므로 EntityManager는 쓰레드간의 공유가 될 수 없으므로 사용하고 버려야 한다. 또한 JPA의 모든 데이터 변경은 트랜잭션 안에서 실행된다.  

 

JPQL

EntityManager.find()처럼 가장 단순한 조회 방법을 사용할때도 있지만 결과가 여러건인 데이터를 조회할때는 문제가 생긴다. 

 

try {
    List<Member> result = em.createQuery("select m from Member as m", Member.class)
            .getResultList();

    for (Member member : result) {
        System.out.println("member.name = " + member.getName());
    }

    tx.commit();

} catch (Exception e) {

    tx.rollback();

} finally {

    em.close();

}

JPA를 사용하면 앤티티 객체를 중심으로 개발되는데 이때 검색 쿼리에서 문제가 발생한다.

검색할때 테이블이 아닌 객체를 대상으로 하기 때문에 페이징과 같은 조건을 줄때 이것을 객체에서 가져오기엔 한계가 있다. 결국 검색 조건이 포함된 SQL이 필요한데 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다. 

 

SQL문법과 유사하지만 JPQL을 앤티티 객체를 대상으로 쿼리한다는 점과 SQL은 데이터베이스 테이블을 대상으로 쿼리한다는 점에 차이가 있다. 특히 JPQL은 SQL을 추상화 하기 때무에 특정 데이터베이스에 의존하지 않는다. 

 

 


 

영속성 관리 : JPA내부 구조

 

JPA를 이해하는데 가장 중요한 용어인 영속성 컨텍스트가 있다. 

 

영속성 컨텍스트

앤티티를 영구 저장하는 환경이라는 뜻이다.

EntityManager.persist(entity) ==> 사실 이 문장은 디비에 저장한다는게 아니라 영속성 컨텍스트에 저장한다는 뜻이다. 

 

영속성 컨텍스트는 논리적인 개념이므로 눈에 보이지 않는다. 앤티티매니저를 통해 영속성 컨텍스트에 접근한다. 

 

엔터티에는 생명주기가 있다.

  • 비영속 : 객체를 생성한 상태 (new Member())
  • 영속 : 객체를 저장한 상태 (em.persist(member)), 영속성 컨텍스트 안에 값을 조회할 때
  • 준영속, 삭제 : 객체를 삭제한 상태(em.detach(member), em.remove(member))

 

영속성 컨텍스트의 이점

 

엔티티 조회 - 1차 캐시

영속 컨텍스트에 1차 캐시가 있는데 @Id값인 PK가 들어있고 Entity인 앤터티 값 이 들어 있다. 

 

만약 member를 em.persist(member)하게 되면 1차캐시에 저장되고 em.find(Member.class, "member1")해서 조회 하면 DB에서 데이터를 조회하는게 아니라 1차캐시에 먼저 조회한다. 만약 1차캐시에 없고 DB에만 있는 값을 조회한다면 JPA가 영속성 컨텍스트 1차캐시에 존재하지 않는다는 것을 알고 DB에서 조회하고 그 값을 1차캐시에 저장시켜주고 값을 반환해준다. 그리고 다시 조회하면 1차캐시에서 값을 조회한다. 

 

 

JpaMain.java

콘솔창을 보면 insert문만 보이고 select문이 보이지 않는다. 그 이유는 영속 컨텍스트 1차캐시 덕분이다.

persist하는 순간 1차캐시에 값이 저장되고, 그 값을 조회할때는 DB가 아니라 1차캐시에서 조회하기 때문에 select문이 필요 없는 것이다. 그래서 트랜잭션을 커밋하면 insert문만 보이게 되는것이다. 

 

 

JpaMain.java

em.find를 2번 조회했는데 select문이 한번만 작성됐다. 그 이유는 DB에서 조회한 값을 1차로 조회하기 위해(1차캐시에 없을 때) 쿼리가 작성되고 두번 조회할때는 1차캐시에서 조회하기 때문이다. 

 

 

 

영속 엔티티의 동일성 보장

 

JpaMain.java

같은 트랜잭션안에서 같은 객체를 비교하면 동일성을 보장해준다. 

 

 

엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연

 

Member.java

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    public Member() {
    }

    public Member(Long id, String name) {
        this.id = id;
        this.name = name;
    }

생성자를 추가했는데 이때 기본생성자가 꼭 필요하다. 왜냐하면 JPA는 내부적으로 리플렉션을 써서 동적으로 객체를 생성해야 하기 때문이다. (꼭 public일 필요없음.)

 

 

 

JpaMain.java

em.persist()를 두번 하고 "==========" 를 출력했는데 결과는 "========"출력이 먼저 되고 insert문이 나왔다. 

그 이유는 persist를 하면 JPA안에서 영속 컨텍스트안에 1차 캐시 에 저장되는 동시에 쓰기 지연 SQL저장소라는곳에 JPA가 앤터티를 분석해서 insert쿼리를 생성해서 저장한다. (아직 디비에 넣는게 아님)

 

그리고 트랜잭션을 커밋하는 순간 쓰기 지연 SQL저장소에 있던 쿼리가 DB에 날아간다. 그리고 실제로 커밋이 된다. 

 

<property name="hibernate.jdbc.batch_size" value="10"/>

배치사이즈라는 속성을 추가하면 10개까지 저장했다가 한번에 커밋시킬수 있다. (모아서 한번에 넣기)

 

 

엔티티 수정 - 변경 감지

 

JpaMain.java

엔티티의 값을 바꾸고 값을 저장하지 않아도 되는 이유가 있다. JPA는 데이터베이스 트랜잭션을 커밋하는 시점에 엔티티와 스냅샷을 비교한다. 1차 캐시 안에 사실 PK와 엔터티, 스냅샷이라는 것들이 있는데 내가 값을 읽어온 그 시점의(처음 영속 컨텍스트에 들어옴) 상태를 스냅샷에 저장한다. 근데 엔터티 값이 변경되었다면 커밋되는 시점에 JPA가 이 엔터티와 스냅샷을 비교해서 만약 변경사항이 있다면 update쿼리를 쓰기 지연 SQL저장소에 또 저장해서 커밋된다면 DB에 실제로 update쿼리가 적용되는 것이다. 이것을 변경감지라고 한다. 

 

 


 

플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영. 쉽게 말해 DB에 쿼리를 날려주는것이다. 

 

플러시가 발생될때는 커밋될때인데 만약 변경이 감지되면 수정된 엔티티 쓰기 지연 SQL 저장소에 등록이 되고 쓰기 지연 SQL저장소의 쿼리를 데이터베이스에 전송한다. (등록, 수정, 삭제 쿼리) ==> 플러시와 커밋은 다름. 플러시가 되면 커밋이 되는것. 

 

플러시 하는 방법

em.flush() 직접 호출, 트랜잭션 커밋, JPQL 쿼리 실행

 

 

em.flush()

원래 em.persist만 하면 커밋하기 전까지 DB쿼리를 볼 수 없지만 강제로 flush()하면 쿼리를 먼저 반영한다.

플러시는 1차캐시값을 변경하는 것이 아니라 DB에 쿼리를 반영하는 것이다.  

 

 

정리하자면 플러시는 영속성 컨텍스트를 비우는것이 아니라 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 하는 것이다. 플러시가 작동되는 이유는 트랜잭션이라는 작업 단위덕분이다. 어쨋든 트랜잭션 커밋 직전에만 변경내용을 동기화 하면 되는것이다. 

 

 


준영속 상테

 

준영속은 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다. 영속 상태의 엔터티가 영속성 컨텍스트를 분리한다. 

 

 

JpaMain.java

em.find로 조회하는 순간 영속컨텍스트에 찾는 데이터가 저장되고 이름을 변경했다. 그럼 변경사항이 생겼는데 detch를 통해 준영속 상태로 만들고 커밋하면 select쿼리만 실행되고 update는 실행되지 않는다. 그러므로 변경사항이 저장되지 않는다. 

 

 

//영속
Member member = em.find(Member.class, 150L);
member.setName("AAAAA");

em.clear();

Member member2 = em.find(Member.class, 150L);

System.out.println("============================");

tx.commit();

만약 영속성 상태를 clear()한다면 영속성 컨텍스트에서 저장된 데이터가 다 사라지는데 이때 한번더 조회하면 영속 상태를 만들기 위해 DB에 값을 또 조회하기 때문에 이때는 update는 실행되지 않지만 select가 두번 실행된다.  

'jpa' 카테고리의 다른 글

객체지향 쿼리 언어 JPQL  (0) 2022.04.02
[Transaction Isolation]과 트랜잭션 범위의 영속성 컨텍스트  (0) 2022.04.02
프록시와 연관관계 관리  (0) 2022.03.27
엔티티 매핑  (0) 2022.03.19

블로그의 정보

kiwi

키위먹고싶다

활동하기