본문 바로가기

JPA/영속성 관리

#04 영속성 컨테스트의 특정 - 엔티티 등록 / 수정 / 삭제

Index

  • 엔티티 등록
  • 엔티티 수정
  • 엔티티 삭제

엔티티 등록

EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction transaction = em.getTransaction(); //트랜잭션 기능 획득

// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin();   // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
// 여기까지는 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit();  // [트랜잭션] 커밋



 

EntityManager는 트랜잭션을 커밋( transaction.commit() )하기 직전 까지는 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 그리고 커밋 시점에 데이터베이스에 보내는데 이부분을 트랜잭션을 지원하는 쓰기지연(transactional write-behind) 이라 한다.

 

예전에 자바배치를 사용할 때 JDBC를 이용해서 5천~1만건의 SQL이 쌓이면 COMMIT을 했던적이 있는데. 당연히 불필요한 연결이 줄어들기 때문에 속도도 훨씬 빨랐던 기억이 난다. 참 재미있는건 하나씩 공부하면서 느껴지는 부분이 

점점 로직이 SQL에서 애플리케이션으로 넘어 오는 것이다. 내가 개발을 하면서 자바 개발자인데... 자바코드보다 

SQL을 만지는 시간이......... 압도적이 였던것도 모두 이부분에 해당된다. ㅎㅎ ORM을 모르고 JPA를 공부하지 않았다면 그냥 우물안에서 자기만족에 취해서 살았을 것 같다.  공부공부 공부가 답임. ㄱㄱ  다시 내용으로 ~ ㅋㅋ 

 

쓰기지연 A , B

이미지 출처 : https://velog.io/@syleemk/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC

 

 

  1.  memberA를 1차 캐시에 저장 / INSERT SQL을 만들고 쓰기지연 SQL 저장소에 보관한다.
  2.  memberB를 1차 캐시에 저장 / INSERT SQL을 만들고 쓰기지연 SQL 저장소에 보관한다.
  3. commit() 을 수행하면 영속성 언텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 DB에 동기화 하는 작업을 한다. 이렇게 동기화 후 데이터베이스 트랜잭션을 커밋한다.

쓰기 지연이 가능한 이유 알아보기 

우리는 트랜잭션의 commit과 DB에 SQL을 보내는걸 같다라고 착각하면 안된다. 우리가 SQL을 보내도 commit하지 않는다면 SQL은 실제 DB에 wirte되지 않기 때문이다. 좀더 자세히 살펴보자 Oracle에서 보면 SGA(System Global Area)라는 공유 메모리 영역이 있다. 우리가 SQL을 작성하고 DB에 보내면 SGA의 Data Buffer Cache에 담아두게된다. 그리고 아직 commit을 하지 않았기 때문에 실제 DB file에는 write 하지않고 기다리는 것이다. 어차피 우리가 하나씩 보내나 한번에 보내나 commit기준으로  DB file에 write하기 때문에 우리는 조금더 성능에 집중을 해야되고 필요없는 연결은 줄이는게 좋다.  따라서 JPA에서는 commit을 하기 전에는 DB에 보내지 않고 쓰기지연 저장소에 보관했다가 한번만 연결하는 방식으로 설계되어 있다. 

 

1. SQL작성 DB에 보냄 

2. SGA의 Data Buffer Cache에 저장함

3. commit 하는순간 DB file에 write한다.

 

참고 ( 이번 포스팅과 크게 상관은 없는 하지만 중요하고 재미있는 Oracle 이야기... )

여기서 조금 복잡한걸 생각해 보면 좋을 것 같다.  우리는 INSERT를 생각하고 있는데 만약에 UPDATE하는 부분을 SGA의 Data Buffer Cache에 저장하면 ... SGA는 모든 세션에서 동시에 접속이 가능하기 때문에 동시성 문제가 발생할 것이다. 그럼 오라클은 어떤 방식으로 이를 해결하고 있을까?? 인프런 강의에서 답변받은 내용을 바탕으로 하면 

오라클은 MVCC(Multi version Concurrency control)라는 메커니즘이 있다.  UPDATE 시 Data Buffer Cache에 변경된 내용이 저장되고 동시에 (commit되지 않았다면) 기존 Data block은 Undo(rollback) Segment로 이동하게 되고 이때 다른 사용자가 해당 block에 접근하면 오라클은 아직 commit되지 않았기 때문에 Undo Segment의 Block을 읽어서 전달하게 된다. 내가 사용하고 있는 DB의 내부 메커니즘을 알아보니 너무 재미있고 즐겁다 ㅎㅎ  어쨋든 DB공부를 열심히 해야됨! 

 

엔티티 수정

아래 코드의 영속 엔티티를 조회 / 수정 / commit 하는 코드를 살펴보자.

EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction transaction = em.getTransaction(); //트랜잭션 기능 획득
transaction.begin(); // [트랜잭션] 시작

//영속 엔티티 조회
Member memberA = em.find(MemberA.class, "memberA");

//영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);

transaction.commit(); // [트랜잭션] 커밋

1. em.find() 로 영속상태의 Entity ( "memberA" )를 조회한다.

2. 조회한 memberA의 username, age를 변경한다.

3. 트랜잭션을 commit() 한다.

4. 변경된 부분이 DB에 적용된다. 

 

이렇게 우리가 영속상태의 Entity를 변경하면 EntityManager는 이 부분을 감지하고 transaction.commit() 시점에 DB에 UPDATE SQL을 보내고 데이터베이스 트랜잭션을 commit 하게된다. 이렇게 우리는 변경에 대한 UPDATE문을 작성하지 않고 영속상태의 Entity의 값을 변경 하기만 하면된다.

 

이번에는 조금더 내부적인 동작을 살펴보자 아래 그림은 transaction.commit()을 수행할 때 EntityManager 내부적으로 동작하는 부분을 나타내고 있다.

아래 그림을 보면 "스냅샷" 이 보인다. 이 부분은 Entity를 영속화 할 때 원본을 저장해 둔다고 생각하면 된다. (그래야 변경된 Entity가 있는지 확인 가능하니까...) 아래 그림을 순서대로 분석해보자.

변경 감지

1. transaction.commit() 을 호출하면 EntityManager 내부에서 flush() 가 호출된다.

2. Entity와 스냅샷을 비교해서 변경된 Entity를 찾는다. 쉽게말해 원본을 비교해서 다른게 있는지 확인한다.

3. 변경된 Entity가 있으면 UPDAT SQL을 만들어서 쓰기지연 SQL 저장소에 보관한다. (여러개의 Entity가 변경 됐다면 변경된 Entity만큼 보관)

4. 쓰기 지연 저장소의 SQL을 DB에 보낸다.

5. DB 트랜잭션을 커밋한다.

 

 

이렇게 변경된 Entity를 감지하고 UPDATE SQL을 만들때 기본적으로는 Entity의 모든 맴버를 대상으로 UPDATE문을 작성해서 보낸다. 만약 10개의 컬럼중에 2개만 변경이 되더라도 굳이 10개를 모두 변경하는게 기본 정책이고 @DynamicUpdate 을 사용하면 변경된 컬럼 2개만 작성해서 UPDATE문을 작성한다. 여기서 우리는 의문을 가져야 한다. 2개의 컬럼만 변경 했는데 왜!!! 10개를 모두 변경하는게 기본 정책인지...  그리고 컬럼 개수가 많지 않다면 (책에서는 대략 30개 이상)모두 변경하는게 속도 또한 더 빠르다고한다. 이 부분을 이해 하려면 또 DB의 내부적인 동작을 이해해야 된다.  이번에도 Oracle의 SGA를 살펴보자. 

 

아래 이미지를 보면 Shared Pool 이 보인다.  이 영역은 SGA(System Global Area) 로서 공유메모리 영역 안에 존재한다. 

오라클 SGA의 Shared Pool

SGA > Shared Pool > Library Cache 안에 모든 SQL 을 보관하고 같은 SQL이 들어오면 Library Cache에 존재 하는지 확인하고 존재한다면 Soft Parsing으로 이미 저장되어 있는 실행계획을 사용하기 때문에 속도에 이점이 있다.

( sql의 실행계획을 만드는게 굉장히 비싼 비용 이라고한다. 그래서 오라클은 앞서 설명한 것 과 같은 정책으로 Hard Parsing을 줄이고 있다. )

 

이미지 출처 : 인프런에서 수강중인강의, 강의명 : 오라클 성능분석과 인스턴스 튜닝 핵심가이드 ( Shared Pool 개요와 SQL 공유 방안)

 

 

엔티티 삭제

Member memberA = en.find(Member.class, "memberA"); // 엔티티 조회
em.remove(memberA);  // 엔티티 삭제

 

em.remove() 에 삭제대상 엔티티를 넘겨주면 DELETE SQL을 만들어서 DB에서 삭제된다.

( 내부적으로는 플러시를 호출 > DELETE SQL 작성 > 쓰기 지연 저장소 >  DB 전송 > DB 트랜잭션 커밋)

하지만 실무에서 실제 DELETE SQL을 작성하는 일은 매우 드물어서 아마.. 사용빈도는 매우 적을 것 같다.