beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

JPA 3장 요약 #5

Open joont92 opened 6 years ago

joont92 commented 6 years ago

엔티티란?

간단하게 DB 테이블에 매핑되는 자바 클래스를 얘기한다.
이 클래스의 인스턴스가 결국 RDB의 레코드 하나로 매핑될 수 있다.

엔티티 매니저

이름 그대로 엔티티를 관리하는 관리자이다.
엔티티 매니저를 통해 엔티티와 관련된 모든 작업(삽입, 수정, 삭제 등)을 수행할 수 있다.

이 엔티티 매니저는 엔티티 매니저 팩토리를 통해 생성할 수 있다.
간단한 코드는 아래와 같다.

EntityManagerFactory emf = Persistence.createEntityMangerFactory("test");

EntityManger em = emf.createEntityManager();

em.persist(entity); // 등록
em.find(entity); // 조회
em.remove(entity); // 삭제

아래와 같이 database에 대한 하나의 커넥션이라고 보면 된다.

application - entity manger - db

하지만 보다시피 어플리케이션이 직접적으로 맺는것은 아니고, 이 엔티티매니저를 통해 맺는? 그런 구조라고 보면 된다.

이 엔티티매니저를 얻어오기 위해 엔티티 매니저 팩토리라는 것을 생성했고, 여기에 test라는 이름을 주고 있는데, 이건 뭘까?

<persistence-unit name="test">
    <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:~/test"/>
        <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
    </properties>
</persistence-unit>

META-INF에 등록된 persistence.xml의 내용이다.
보다시피 하나의 데이터베이스에 대한 정보이다.

즉, 엔티티 매니저 팩토리를 통해 해당 데이터베이스에 연결(?)하고,
이 엔티티 매니저 팩토리를 통해 엔티티 매니저를 매번 생성하는 구조이다.

J2SE에서는 엔티티 매니저 팩토리 생성 시 커넥션 풀을 생성하고, J2EE의 경우 컨테이너가 제공하는 데이터 소스를 사용한다.
진짜 팩토리이다!

보다시피 팩토리이므로 어플리케이션 실행 시 한번만 생성해서 공유하도록 하면 된다.
하지만 여기서 생성된 엔티티 매니저는 데이터베이스에 대한 직접적인 하나의 커넥션이므로, 절대 공유해서는 안된다.
(엔티티 매니저를 생성했다고 바로 커넥션을 얻는 것은 아니고, 정말 필요할 시점에 커넥션을 획득한다)

영속성 컨텍스트

JPA에서 가장 중요한 개념이다.
용어를 정의하면 엔티티를 영구 저장하는 환경이다.
엔티티 매니저가 생성될 때 하나 생성되고, 엔티티 매니저에 요청하는 모든 작업이 이 영속성 컨텍스트를 기반으로 이루어진다.

엔티티와 관련된 모든 작업 내용을 여기에 저장하고,
트랜잭션이 종료될 때 이 환경에 저장된 내용을 기반으로 실제 SQL을 날린다고 보면 된다.
즉, 그냥 위에서도 언급했지만 간단하게 어플리케이션과 데이터베이스 사이에 있는 1차 캐시정도로 보면 좋을 듯 하다.

아래는 em.persist를 수행했을 시의 소스를 간단히 본 것인데, Map에 저장하고 있음을 볼 수 있다.
즉 모든 행위들에 대해 이런식으로 저장하고, 트랜잭션이 끝나는 시점에 이런 정보들을 종합하여 최종 SQL을 날린다고 보면 된다.(이 행위를 flush라고 한다)

private Map<EntityKey, Object> entitiesByKey;

// ... 
@Override
public void addEntity(EntityKey key, Object entity) {
    entitiesByKey.put( key, entity ); // 여기!
    getBatchFetchQueue().removeBatchLoadableEntityKey( key );
}

key, object의 형태로 저장함을 볼 수 있다. (key는 hashCode와 persister(?) 등으로 조합된 클래스이다)
엔티티 키를 만드는 행위를 간단히 보면 아래와 같다.

final EntityKey key = source.generateEntityKey( id, persister );

엔티티의 식별자값(primary key)을 통해 만들고 있다. 즉, 엔티티 매니저에 의해 관리되러면 식별자값은 필수이다!!

이렇게 어플리케이션과 데이터베이스 사이에 영속성 컨텍스트라는 논리적(?)인 개념을 둠으로써 얻는 이점은 굉장히 많다(뭐든 중간에 하나 두면 성능 최적화를 할 요소가 많아진다)

엔티티 생명주기(상태)

위에서 언급한 영속성 컨텍스트에 저장하는 행위에도 결국 일종의 룰이 필요할 것이다.
여기서 등장하는 것이 엔티티 상태 이다.
엔티티 매니저는 엔티티들의 상태를 통해 여러가지 작업들을 수행한다.
이를 엔티티 생명주기라고 한다.

비영속

영속성 컨텍스트와 전혀 관계없는 상태이다.
그냥 엔티티를 생성하면 비영속 상태이다.

Member member = new Member(); // 비영속 상태!

영속

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한 상태를 말한다.
간단하게 말하면 위의 Map에 저장된 상태. 즉, 엔티티 매니저에 의해 관리되는 상태이다.
영속상태로 전환하는 법은 간단하다.
em.persistem.find를 통해 엔티티를 저장하거나 조회하기만 하면 영속 상태가 된다.
영속성 컨텍스트에 저장되고, 엔티티 매니저에 의해 관리된다는 뜻!

준영속

조금 특별한 상태이다.
영속상태였다가 비영속상태로 변환된 엔티티를 준영속 상태라고 한다.
결과적으로 영속상태가 아니므로 영속성 컨텍스트에서 제공하는 모든 기능을 사용할 수 없다.
em.detach 메서드를 통해 변환할 수 있다.

em.persist(member); // 영속 상태

member = em.detach(member); // 준영속 상태

member.setName("changed name"); // update 발생하지 않음

em.detach외에도
em.clear를 통해 영속성 컨텍스트 내의 모든 엔티티를 지워버림으로써 준영속 상태로 만들 수 있고,
em.close를 통해 영속성 컨텍스트를 종료해버림으로써 준영속 상태로 만들 수 있다.

비영속 상태와 별 다를것 없지만 하나 확실한 것은, 실존하는 데이터라는게 증명이 된다는 뜻이다.
(영속성 컨텍스트에 들어갔었으면 등록되거나, 조회되어진 데이터이므로)

행위

조회

em.find를 통해 엔티티를 조회해올 수 있다.
바로 데이터베이스에서 조회해오는 것은 아니고, 영속성 컨텍스트를 거쳐서 조회한다.
처음 em.find를 통해 오브젝트를 찾으면 먼저 영속성 컨텍스트에 해당 오브젝트(key로 조회)가 있는지 찾고
있으면 db로 가지 않고 그 오브젝트를 바로 리턴하고, 없으면 db에서 조회해온 뒤 영속성 컨텍스트에 저장하고 그 오브젝트를 리턴한다.
어플리케이션 레벨에서 캐싱이 가능하단 뜻이다!

Member member1 = em.find(Member.class, "joont92");
Member member2 = em.find(Member.class, "joont92");

assertSame(member1, member2); // success

하이버네이트와 같은 ORM 프레임워크를 사용하지 않았다면
동일한 레코드임에도 불구하고 쿼리를 두번 날리는 결과가 발생했었을 것이다.

등록

em.persist를 통해 엔티티를 데이터베이스에 등록할 수 있다.
해당 메서드를 실행함과 동시에 데이터베이스에 바로 저장하는 것은 아니고, 먼저 영속성 컨텍스트에 저장한다.
근데 여기서 단순히 영속성 컨텍스트에 저장하는 작업만을 하는것은 아니고,
쓰기지연 SQL 저장소라는 곳에 insert 쿼리를 등록하는 작업까지 동시에 진행한다.
그리고 마지막에 flush가 일어나면 여기에 저장된 SQL을 데이터베이스로 발사!하는 것이다.

어떻게 쓰기 지연이 가능할까?

  1. 데이터베이스에 트랜잭션이라는 개념이 있기 때문이다.
    데이터베이스에 DML을 아무리 날려도 commit을 하지 않으면 적용되지 않는다는 특징을 이용하여 쓰기 지연을 가능하게 할 수 있다.
  2. 데이터베이스에 직접 날리지 않고 쿼리를 메모리에 저장해두는 방식으로 가능하다.

수정

수정은 딱히 메서드가 존재하지 않는다.
이는 JPA에 변경감지라는 특징이 있기 때문이다.
엔티티가 처음으로 영속상태에 들어갈 경우, Map에 저장만 하는 것이 아니라 초기 엔티티의 스냅샷이라는 것을 찍어둔다.
그리고 마지막 flush가 일어날 때 영속성 컨텍스트에 저장된 엔티티의 속성 값들과 엔티티 스냅샷의 속성 값들을 비교한다.
그리고 변경이 일어났을 경우 update 메서드를 생성하여 이를 데이터베이스에 발사한다.(변경 감지)
(이러한 로직이므로 따로 dirty check가 필요없다)

생성되는 update문의 형태
실제로 update를 발생시켜보면 알겠지만, 업데이트가 발생하는 특정 속성에 대해서만 업데이트 하는 것이 아니라
전체 오브젝트에 대해 업데이트를 실행하는 쿼리가 생성된다.
이렇게 하면 매번 사용하는 수정 쿼리가 같다는 점을 이용한 것이고, 속성 하나하나에 대해 쿼리를 다 생성해놓지 않아도 된다는 장점이 있다(진짜 장점인가)
JPA가 로딩 시점에 업데이트 쿼리를 미리 생성해둔다.

하지만 필드의 내용이 너무 많을 경우 매번 이런식으로 풀 업데이트 쿼리를 날리는 것은 비효율적이다.
이때는 아래와 같이 @DynamicUpdate 어노테이션을 사용하면 수정된 데이터에 대해서만 update를 실행하는 쿼리를 생성한다.

@Entity
@Table(name = "MEMBER")
@org.hibernate.annotations.DynamicUpdate
class Member{

}

상황에 따라 다르지만 필드가 30개가 넘어가면 위와 같이 @DynamicUpdate를 쓰는 것이 좋다고 한다.
그리고 그 이전에, 30개가 넘어가는 테이블이면 정규화가 제대로 되지 않는다는 고민이 선행되어야 할 것이다.

삭제

엔티티를 삭제하려면 먼저 삭제 대상 엔티티를 조회해야 한다.

Member member = em.find(Member.class, "joont92");
em.remove(member);

em.detach + delete 쿼리라고 보면 된다.
메서드를 실행하면 해당 엔티티는 영속성 컨텍스트에서 detach 된다(그리고 delete 쿼리를 쓰기지연 SQL 저장소에 저장?)

병합

준영속 상태인 엔티티를 다시 영속상태로 만드는 행위를 말한다.
em.merge 메서드를 사용한다.

em.merge(member);

위와 같이 작성하면 먼저,

  1. member가 영속성 컨텍스트에 있는지 검사하고, 있으면 리턴한다.
  2. 없다면 db에서 조회해와서 영속성 컨텍스트에 저장하고, 리턴한다.
  3. db에도 없다면 persist를 수행하고, 영속성 컨텍스트에 저장하고, 리턴한다.

flush

영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 행위이다.
쓰기지연 SQL에 저장된 SQL들을 발사!하고,
위에서 언급했듯이 영속성 컨텍스트에 들어있는 오브젝트와 스냅샷을 비교하여 업데이트 쿼리를 생성한 뒤, 업데이트 쿼리를 생성하여 발사!한다.

flush를 발생시키는 방법은 3가지 정도가 있다.

  1. em.flush() 메서드를 직접 호출

    거의 사용할 일이 없다

  2. 트랜잭션 커밋 시 플러시 자동 호출

    flush 하지 않고 commit 할 경우, SQL이 하나도 실행되지 않은 상태이기 떄문에 아무런 일도 일어나지 않는다.
    JPA에서는 이런 상황을 방지하기 위해 commit시 flush를 자동으로 호출한다.

  3. JPQL 실행 시 플러시 자동 호출

    JPQL은 호출시에 SQL로 변환되어 데이터베이스에서 조회해오는데, 이럴려면 레코드들이 이미 데이터베이스에 저장되어 있어야 한다.
    persist와 JQPL 호출 작업을 한 트랜잭션 내에서 하는 행위를 방지하기 위해 위와 같이 처리한 듯 하다.

플러시 모드를 변경하려면 javax.persistence.FlushModeType을 사용하면 된다.

flush를 한다고 해서 영속성 컨텍스트에서 엔티티가 지워지는 것은 아니다!!
(이걸 신경쓸 일이 있곘느냐만....)