JPA 가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다. 여기에서는 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아보자.
엔티티 매니저는 엔티티를 저장, 수정, 삭제, 조회하는 등의 엔티티와 관련된 모든 일을 처리한다. 이름 그대로 엔티티를 관리하는 관리자다. 개발자 입장에서 엔티티 매니저는 가상의 DB로 생각하면된다.
엔티티 매니저 팩토리는 이름 그대로 엔티티를 만드는 공장인데 이를 만드는 비용이 매무 크므로, 한 개만 만들어서 애플리케이션 전체에 공유하도록 설계되어 있다 (엔티티 매니저를 생성하는 비용을 거의 들지 않음). 엔티티 매니저 팩토리는 여러 쓰레드가 동시에 접근해도 안전하므로 서로 다른 쓰레드간에 공유해도 되지만, 엔티티 매니저는 여러 쓰레드가 동시에 접근하면 동시선 문제가 발생하므로 쓰레드간 절대로 공유하면 안된다.
위의 그림에서 보듯 엔티티 매니저는 팬토리에서 생성되며 실제 DB 접근이 필요하기 전까지는 커넥션 풀을 사용하지 않고 대기한다.
JPA를 이해하는데 가장 중요한 개념은 영속성 컨텍스트(Persistence Context)다.
이는 엔티티를 영구저장하는 환경으로 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관 ・ 관리 한다.
persist() 메서드
는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.em.persist(member);
엔티티는 다음과 같은 4가지 상태가 존재한다.
em.persist
)em.detach
, em.close
, em.clear
)em.remove
)영속성 컨텍스트는 엔티티를 식별자 값@Id
로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 저장하는데 이를 플러시라고 한다.
엔티티 저장, 조회, 삭제 등과 같은 상황에서 엔티티와 영속성 컨텍스트가 어떠한 관계를 가지는지 실펴보자
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 한다. 영속상태의 엔티티는 모두 이곳에 저장된다.
1차 캐시의 키는 식별자값 @Id
이다. 그리고 식별자 값은 DB의 기본 키와 매핑되어있다. 따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 DB의 기본키 값이다.
em.persist(member);
em.find()
로 데이터 조회시 1차 캐시에서 식별자 값으로 엔티티를 조회한다.
em.persist(member);
//1차 캐시에서 조회
Member findOne = em.find(Member.class, "member1");
만약 em.find()
를 호출시 엔티티가 1차 캐시에 없으면 엔티티 매니저는 DB를 호출해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.
이미 영속성 컨텍스트에 존재하는 엔티티를 반복해서 호출시 저장되어 있는 같은 인스턴스를 반환한다. 따라서 아래의 코드는 참이 된다. 결과적으로 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성 을 보장한다.
Member m1 = em.find(Member.class, "member1");
Member m2 = em.find(Member.class, "member1");
m1 == m2 //true
영속성 컨텍스트는 트랜잭션이 커밋하기 전까지는 추가사항이나 변경사항을 내부에 가지고 있다. 이후 트랜잭션이 커밋될때 모든 요청사항을 DB에 보낸다. 이러한 동작 방식은 매 요청마다 DB에 접근하지 않기 때문에 성능상의 이점이 존재한다.
flush()
가 발생하여 변경사항을 DB에 전달하고 커밋한다.EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); //[트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않는다.
//커밋하는 순간 DB에 SQL을 보낸다.
trasaction.commit(); //[트랜잭션] 커밋
기존의 방식은 엔티티가 변경될 때마다 수정과 관련된 SQL이 변경되어야 한다는 문제가 있다. 예를 들어 엔티티 필드가 하나 늘거나 줄어들면 이를 반영하는 UPDATE문을 작성해야 한다. 이러한 문제는 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 한다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.
JPA에서는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. (이때, 반드시 엔티티는 영속상태) 이렇게 하면 영속성 컨테스트 내부에서 저장된 최초의 스냅샷과 비교하여 변경사항을 발견하고 자동으로 SQL을 생성하고 커밋시 flush
로 변경사항을 DB에 반영한다.
플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영한다(동기화). 플러시를 실행하면 구체적으로 다음과 같은 일이 일어난다.
1. 변경감지가 동작해서 영속성 컨텍스트의 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾고, 수정된 엔티티는 쿼리를 만들어 쓰기 지연 SQL 저장소에 저장한다.
2. 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다.
em.flush()
FlushModeType.AUTO: 커밋이나 쿼리 실행시 플러시(DEFAULT)
FlushModeType.COMMIT: 커밋할 때만 플러시
em.setFlushMode(FlushModeType.COMMIT); //직접 설정
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다. 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지다.
1. em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환한다.
2. em.clear()
: 영속성 컨텍스트를 완전히 초기화한다.
3. em.close()
: 영속성 컨텍스트를 종료한다.
엔티티가 준영속 상태가 되는 순간 1차 캐시부터 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다. merge()
메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 상태의 영속 엔티티를 반환한다.
Member mergeMember = em.merge(member);
위의 과정은 다음과 같다.
1. merge()를 실행한다.
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다. 만약 1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회하고 1차 캐시에 저장한다.
3. 조회한 영속성 엔티티에 member 엔티티 갑을 채워 넣는다 (값이 수정된다).
4. mergeMember를 반환한다.
이때, 준영속 상태인 member 엔티티와 영속 상태 mergeMember 엔티티는 서로 다른 인스턴스다.
병합은 비영속 엔티티도 영속상태로 만들 수 있다. 벙합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 DB에서 조회한다. 만약 없으면 새로운 엔티티를 생성해서 병합한다.
→ 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 없으면 생성해서 병합한다.