본문 바로가기
프로젝트/개인 프로젝트(2023.11.13-2023.12.18)

[2] Spring Batch writer에서 merge발생?? - JpaPagingItemReader,JpaItemWriter파보기

by dal_been 2023. 12. 29.
728x90

찾았다. 그대의 답을... 이전 [1] 블로그에서 이어쓰려고 했는데 다 지우기에는 나의 사고과정에 대한 기록이 다 사라져서... 

따로 블로그를 쓴다.

 

오늘 트랜젝션 + 영속성 컨텍스트 +OSIV를 공부하다가 깨달은 내용이다.

 

내가 지금 설명할 부분은 JpaItemWriter, JpaPagingItemReader부분이다. 해당 라이브러리를 살짝 파볼 것이다.

 

아 그러기전에 결론부터 내리자면

 

JpaPagingItemReader 에서 entitymager가 생성되고 해당 entitymanger에서 트랜젝션이 시작되고 commit된다

이후 Processor를 거치고(reader의 entitymanger살아있음)

JpaItemWriter에 왔을때 새로운 entitymanger를 생성하고 update, insert를 실행한다

이후 reader는 writer가 끝나면 reader의 entitymanager를 닫는다.

 

 

위의 과정으로부터 내린  JpaPagingItemReader, JpaItemWriter 결론

1. reader에서 트랜젝션이 커밋되었지만 reader의 영속성 컨텍스트는 살아있다

2. 변경감지는 되지 않는다와 되지 않을 수도 있다

3.새로운 entitymanger로 인해서 다른 영속성 컨텍스트를 갖는다

 

 


JpaPagingItemReader 분석

 

분석전 기억해야하는 내용

- 엔티티 매니저를 생성할때 하나의 영속성 컨텍스트가 만들어진다

- 플러시를 한다고 해서 영속성 컨텍스트가 비워지는게 아니라 쓰기지연저장소에 있는 update,insert쿼리들이 실행되는 것이다

- 트랜젝션 커밋은 플러시와 비슷하다. 영속성 컨텍스트를 비우는게 아니다

- em.close,em.clear은 영속성컨텍스트를 비우면서 쓰기지연 저장소도 비워진다.

  -> 즉 쓰기지연저장소에 update,insert쿼리가 있다면 db에 반영되지 않는다

 

  • doOpen 메서드 
    @Override
	protected void doOpen() throws Exception {
		super.doOpen();
        
		entityManager = entityManagerFactory.createEntityManager(jpaPropertyMap);
		if (entityManager == null) {
			throw new DataAccessResourceFailureException("Unable to obtain an EntityManager");
		}
		// set entityManager to queryProvider, so it participates
		// in JpaPagingItemReader's managed transaction
		if (queryProvider != null) {
			queryProvider.setEntityManager(entityManager);
		}

	}

 

- reader를 시작할때 엔티티매니저팩토리를 이용해 엔티티매니저를 만든다.

 

  • doReadpage 메서드
	@Override
	@SuppressWarnings("unchecked")
	protected void doReadPage() {

		EntityTransaction tx = null;

		(1)
		if (transacted) {
			tx = entityManager.getTransaction();
			tx.begin();

			entityManager.flush();
			entityManager.clear();
		} // end if

		(2)
		Query query = createQuery().setFirstResult(getPage() * getPageSize()).setMaxResults(getPageSize());

		if (parameterValues != null) {
			for (Map.Entry<String, Object> me : parameterValues.entrySet()) {
				query.setParameter(me.getKey(), me.getValue());
			}
		}

		if (results == null) {
			results = new CopyOnWriteArrayList<>();
		}
		else {
			results.clear();
		}

        (3)
		if (!transacted) {
			List<T> queryResult = query.getResultList();
			for (T entity : queryResult) {
				entityManager.detach(entity);
				results.add(entity);
			} // end if
		}
		else {
			results.addAll(query.getResultList());
			tx.commit();
		} // end if
	}

 

(1) 트랜젝션이 없다면 doOpen에서 만든 엔티티 매니저로 트랜젝션을 생성하여 시작한다.

     - 하면서 엔티티매니저의 영속성 컨텍스트를 flush,clear로 비워준다

(2) reader의 조회 쿼리를 실행

(3) 여기서 트랜젝션이 시작되었기때문에 else부분으로 가서 조회한것들을 results에 담아주고 트랜젝션은 commit된다

 

--> 주의! 앞서 이야기했다싶이 트랜젝션이 commit되었다고 영속성 컨텍스트가 비워지는게 아니다

 

  • doClose 메서드
	@Override
	protected void doClose() throws Exception {
		entityManager.close();
		super.doClose();
	}

  

doClose메서드는 모든게 다 수행되었을때 즉 writer까지 모든 데이터가 수정되었을때 엔티티 매니저가 close된다.

 

 

JpaPagingItemReader에 대한 기억해야하는 부분

- 트랜젝션이 커밋된다.

- 엔티티매니저는 살아있다(즉, 영속성 컨텍스트가 살아있다)

- 다만 모든 데이터가 수정되었을때 엔티티 매니저는 flush가 아니라 close된다(쓰기지연저장소가 반영이 안된다)

 

 

JpaItemWriter 분석

 

  • write 메서드
	@Override
	public void write(Chunk<? extends T> items) {
		EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory);
		if (entityManager == null) {
			throw new DataAccessResourceFailureException("Unable to obtain a transactional EntityManager");
		}
		doWrite(entityManager, items);
		entityManager.flush();
	}

 

엔티티 매니저를 새로 생성한다.

이후 doWrite메서드 호출한후 앤티티매니저를 플러쉬한다(db 반영)

 

  • doWrite 메서드
	protected void doWrite(EntityManager entityManager, Chunk<? extends T> items) {

		if (logger.isDebugEnabled()) {
			logger.debug("Writing to JPA with " + items.size() + " items.");
		}

		if (!items.isEmpty()) {
			long addedToContextCount = 0;
			for (T item : items) {
				if (!entityManager.contains(item)) {
					if (usePersist) {
						entityManager.persist(item);
					}
					else {
						entityManager.merge(item);
					}
					addedToContextCount++;
				}
			}
			if (logger.isDebugEnabled()) {
				logger.debug(addedToContextCount + " entities " + (usePersist ? " persisted." : "merged."));
				logger.debug((items.size() - addedToContextCount) + " entities found in persistence context.");
			}
		}

	}

 

writer에 넘겨지는 데이터들이 있고 엔티티 매니저가 해당 데이터들을 포함하고 있지 않다면

merge또는 persist를 실행한다. 다만 usePersist기본값이 false라서 기본적으로 merge가 된다.

이후 다시 write메서드에 돌아가서 엔티티매니저가 플러쉬 된다.

 

여기서 궁금중! 왜 기본적으로 merge가 될까??

writer에서 엔티티매니저가 새로 생성되기 때문이다. 즉 새로운 영속성 컨텍스트가 나타난다.

그렇다면 새로운 영속성 컨텍스트에 앞서 reader에서 조회한 데이터들이 있을까??

아니다. 없다. 그래서 entityManager.contains를 통해 없다는 것을 확인할 수 있다.

그러다보니 update와 같은 데이터 수정의 경우 영속성 컨텍스트에 데이터가 없어 변경감지가 안되기때문에 merge를 통해 데이터를 다시 db에서 조회한후 비교하여 update쿼리를 날리는 것이다.

 

그래? 그럼 insert쿼리는 persist가능해?? 그렇다. 

insert할 데이터들은 1차캐시하고 쓰기지연 저장소에 insert쿼리를 저장하며 되기때문에 persist로 가능하다

(writer에서 userPersist true로 설정해주면 됨)

 


이정도면 어느정도 메서드에 대해서 분석이 됬을 것이다.

그럼 내가 내린 결론에 대한 근거를 말해보겠다

 

첫번째  reader에서 트랜젝션이 커밋되었지만 reader의 영속성 컨텍스트는 살아있다

 

https://haebing.tistory.com/112

 

앞서 작성했던 블로그를 보면 processor,writer에서 lazy로딩이 가능했다.

그런데 트랜젝션이 분명 reader에서 커밋되었는데..?

 

음 사실 여기서는 두가지 생각이 있다.

첫번째 트랜젝션이 유지되고 있다.

-> 트랜젝션이 유지되고 있고 영속성 컨텍스트가 같기에 lazy로딩이 가능하다

 

두번째 트랜젝션 없이 읽기(Nontransactional reads)가능하다

-> 영속성 컨텍스트는 트랜젝션 밖에서 조회나 프록시를 초기화하는 지연로딩 조회 기능이 가능하다

-> 다만 트랜젝션 밖에서는 데이터 수정할 수 없다(만약 트랜젝션 없이 flush 하면 예외가 발생한다

 

사실 느낌상 트랜젝션이 유지되고 있다기보다는 영속성 컨텍스트를 공유하고 있다가 뭔가 맞는 느낌이다.

그 이유는 reader에서 doReadePage를 보면 트랜젝션을 시작하고 영속성 컨텍스트를 flush한다.

근데 뒤에서 이야기할 변경감지에 대해서 만약 트랜젝션 없이 processor와 writer에서 수정과 조회만 하고 쿼리를 날리지 않느다면 reader에서 flush를 통해 반영된다. 

여기서 만약 트랜젝션이 유지되고 있다면 tx.begin()후 엔티티 매니저를 flush 할 필요가 있을까? 그냥 새로운 트랜젝션 시작하기전에 flush하는게 더 낫지 않을까??

 

 

이부분에 대해서는 명확하지는 않지만 어째든 전달하고 싶은 것은 영속성 컨텍스트가 살아있다는 점이다

 

두번째 변경감지는 되지 않는다 와 되지 않을 수도 있다

 

첫번째 이유는 writer에서 새로운 엔티티 매니저를 생성한다.즉 새로운 영속성 컨텍스트를 생성하기 때문에 변경감지 즉 스냅샷이 없기에 변경전 비교할 데이터가 없다. 그렇기 때문에 update의 경우 merge를 통해 db에서 조회한후  영속성컨텍스트에 로드한후 update쿼리를 쓰기지연 저장소에 저장한다.

 

두번째 이유는 reader에서 flush, close 때문이다.

reader의 엔티티매니저의 경우 job에서 실행할 데이터의 생성,수정이 끝날때까지 유지된다. 

이 점을 빌려 reader의 코드를 조금 다시 보자면

 

protected void doReadPage() {
	EntityTransaction tx = null;

	if (transacted) {
		tx = entityManager.getTransaction();
		tx.begin();

		entityManager.flush();
		entityManager.clear();
      } // end if
}

protected void doClose() throws Exception {
	entityManager.close();
	super.doClose();
}

 

나는 초반에 doReadePage에서 왜 flush,clear를 할까? 궁금했다. 근데 생각해보니 쓰기지연저장소를 반영하기 위해서이다.

만약 변경감지를 적용하고 싶다는 생각에 processor에서 데이터 수정, 생성을 하고 writer에서 아무것도 안한게 했다고 하자(writer에서 db를 반영하는게 아니라 reader의 영속성컨텍스트에 반영된다)

그렇게 되면 reader의 엔티티매니저 영속성컨텍스트에 반영되기 때문에 해당 수정, 생성 사항을 반영하기 위해 flush, clear를 하는 것이다.

 

다만 이는 애매한 상황이 고려된다.

예를 들어 55개의 데이터를 10개식 처리한다고 생각해보자. 그럼 앞의 50개는 writer를 거쳤다가 다시 reader로 돌아가기 때문에 엔티티매니저가 flush, clear해준다. 즉 db에 반영해준다. 하지만 나머지 5개는 processor에서 update,insert를 시도하지만 마지막 데이터들에 대해서 reader는 flush가 아니라 close해버린다. 즉 db 반영이 안된 것이다(영속성컨텍스트 + 쓰기지연저장소 비워짐)

 

그래서 나도 초반에 변경감지 이용해보자 했는데 어쩔때는 반영이 되고 안되고가 발생했다. 알고보니 이 이유같다. 디버깅 찍어보면 마지막 남은 데이터들은 doReadePagae의 flush로 가는게 아니라 doClose의 close로 간다. 

 

세번째 새로운 entitymanger로 인해서 다른 영속성 컨텍스트를 갖는다

 

두번째 근거에서 살짝 얘기했는데 writer에서는 새로운 엔티티 매니저를 만든다. 즉 새로운 영속성 컨텍스트를 만든다. 

따라서 reader에서 조회하고 processor에서 변경된 데이터들은 writer영속성 컨텍스트에 반영이 되지 않기 때문에 기본적으로 merge를 취하고 있는 것이다.

 

 

 


와 ... 진짜 1주일 걸렸다... 

내 느낌상 내가 내린 결론이 맞는 것같은데.. 틀렸다면 언제든지 누군가가와서 댓글 달아서 이거아니에여 해줬으면 좋겠다.

이렇게 오래 고민해보고 라이브러리까지 보면서 고민해본적이 처음인데... 잼있는데..?

일단 나에게 힌트를 주신 JPA 프로그래밍 저자 김영한님 감사합니다. 책이 정말 도움이 되었습니다. 그냥 거의 여기서 아이디어를 찾았습니다. 감사합니다.