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

[1] Spring Batch writer에서 merge발생??

by dal_been 2023. 12. 27.
728x90

협업프로젝트 기간인데 아직 시간이 있어 개인프로젝트 리팩토링??중에 batch 쿼리에서 신기한 것을 발견했다.

 

대충 코드를 보이자면

  @Bean
  @StepScope
  public Step reserveStep(){

    return new StepBuilder("reserveStep",jobRepository)
        .<WalkerReserveServiceInfo,WalkerReserveServiceInfo>chunk(chunkSize,platformManager)
        .reader(reserveReader())
        .processor(reserveProcessor())
        .writer(reserveWriter())
        .build();

  }
  
    @Bean
  public JpaPagingItemReader<WalkerReserveServiceInfo> reserveReader(){
    Map<String, Object> parameter=new HashMap<>();
    parameter.put("createdAt", LocalDateTime.now().minusMinutes(10));
    parameter.put("status", WALKER_CHECKING);
    return new JpaPagingItemReaderBuilder<WalkerReserveServiceInfo>()
        .name("reserveReader")
        .entityManagerFactory(entityManagerFactory)
        .pageSize(chunkSize)
        .queryString("SELECT w FROM WalkerReserveServiceInfo w "
            + "Join Fetch w.payHistory p "
            + "WHERE w.createdAt < :createdAt "
            + "AND w.status = :status")
        .parameterValues(parameter)
        .build();
  }

  @Bean
  public ItemProcessor<WalkerReserveServiceInfo,WalkerReserveServiceInfo> reserveProcessor(){
    return reserveService -> {
      reserveService.setStatus(WALKER_REFUSE);
      reserveService.getPayHistory().setPayStatus(PAY_REFUND);
      return reserveService;
    };
  }

  @Bean
  public JpaItemWriter<WalkerReserveServiceInfo> reserveWriter(){
    return new JpaItemWriterBuilder<WalkerReserveServiceInfo>()
        .entityManagerFactory(entityManagerFactory)
        .build();
  }

 

 

이런 느낌이다. 그니까 서비스의 상태와 결제 상태를 바꾸는 건데 writer에서의 쿼리를 보면

2023-12-27T19:44:43.021+09:00 DEBUG 993 --- [           main] o.s.batch.item.database.JpaItemWriter    : Writing to JPA with 2 items.
2023-12-27T19:44:43.022+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    select
        w1_0.walker_reserve_service_id,
        w1_0.walker_reserve_service_created_at,
        w1_0.customer_id,
        p1_0.pay_history_id,
        p1_0.pay_created_at,
        p1_0.user_id,
        p1_0.pay_method,
        p1_0.pay_price,
        p1_0.pay_status,
        p1_0.pay_updated_at,
        w1_0.walker_service_date,
        w1_0.walker_reserve_service_price,
        w1_0.walker_service_status,
        w1_0.walker_service_time_unit,
        w1_0.walker_reserve_service_updated_at,
        w1_0.walker_id 
    from
        walker_reserve_service w1_0 
    left join
        pay_history p1_0 
            on p1_0.pay_history_id=w1_0.pay_history_id 
    where
        w1_0.walker_reserve_service_id=?
2023-12-27T19:44:43.025+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    select
        w1_0.walker_reserve_service_id,
        w1_0.walker_reserve_service_created_at,
        w1_0.customer_id,
        p1_0.pay_history_id,
        p1_0.pay_created_at,
        p1_0.user_id,
        p1_0.pay_method,
        p1_0.pay_price,
        p1_0.pay_status,
        p1_0.pay_updated_at,
        w1_0.walker_service_date,
        w1_0.walker_reserve_service_price,
        w1_0.walker_service_status,
        w1_0.walker_service_time_unit,
        w1_0.walker_reserve_service_updated_at,
        w1_0.walker_id 
    from
        walker_reserve_service w1_0 
    left join
        pay_history p1_0 
            on p1_0.pay_history_id=w1_0.pay_history_id 
    where
        w1_0.walker_reserve_service_id=?
2023-12-27T19:44:43.030+09:00 DEBUG 993 --- [           main] o.s.batch.item.database.JpaItemWriter    : 2 entities merged.
2023-12-27T19:44:43.030+09:00 DEBUG 993 --- [           main] o.s.batch.item.database.JpaItemWriter    : 0 entities found in persistence context.
2023-12-27T19:44:43.031+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    /* update
        com.project.dogwalker.domain.reserve.PayHistory */ update pay_history 
    set
        pay_created_at=?,
        user_id=?,
        pay_method=?,
        pay_price=?,
        pay_status=?,
        pay_updated_at=? 
    where
        pay_history_id=?
2023-12-27T19:44:43.033+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    /* update
        com.project.dogwalker.domain.reserve.WalkerReserveServiceInfo */ update walker_reserve_service 
    set
        walker_reserve_service_created_at=?,
        customer_id=?,
        pay_history_id=?,
        walker_service_date=?,
        walker_reserve_service_price=?,
        walker_service_status=?,
        walker_service_time_unit=?,
        walker_reserve_service_updated_at=?,
        walker_id=? 
    where
        walker_reserve_service_id=?
2023-12-27T19:44:43.034+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    /* update
        com.project.dogwalker.domain.reserve.PayHistory */ update pay_history 
    set
        pay_created_at=?,
        user_id=?,
        pay_method=?,
        pay_price=?,
        pay_status=?,
        pay_updated_at=? 
    where
        pay_history_id=?
2023-12-27T19:44:43.035+09:00 DEBUG 993 --- [           main] org.hibernate.SQL                        : 
    /* update
        com.project.dogwalker.domain.reserve.WalkerReserveServiceInfo */ update walker_reserve_service 
    set
        walker_reserve_service_created_at=?,
        customer_id=?,
        pay_history_id=?,
        walker_service_date=?,
        walker_reserve_service_price=?,
        walker_service_status=?,
        walker_service_time_unit=?,
        walker_reserve_service_updated_at=?,
        walker_id=? 
    where
        walker_reserve_service_id=?

 

 

update는 잘되지만 조회된 데이터개수만큼 조회 쿼리가 날아간다.

 

아무리 생각해도 왜인지 몰라서 어떤분의 블로그를 보다가 찾게되었다

 

 


JpaItemWriter코드를 보자

 

	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.");
			}
		}

	}

 

저기서 유심히봐야한것은 usePersist다. false면 EntityManger가 merge를 호출하게 된다.

근데 merge의 동작방식은 id필드가 채워져있다면 select를 호출하여 db 존재 유무를 판별하고, 그결과를 이용하여 insert, update를 구분하게 된다.

 

일단 이미 db에 있던 데이터를 수정하기는 것이기에 id필드가 당연히 채워져 있따. 그렇기 때문에 select -> update 쿼리가 날아가는 것이다.

 


고민과정...??

 

저 select 쿼리를 없애고 싶어서 고민을 해보았다.

 

첫번째 시도방법.. 내가 생각한 batch는 트랜젝션이 reader -> processor -> writer까지 트랜젝션이 유지된다고 생각했다.

그래서 writer에 userPersist속성을 true로 만들어주었다.

 

하지만 실패.. 그이유를 보니 detached되어있다고 나와있다. 영속성 컨텍스트에서 분리된것이다. 

즉 트랜젝션 커밋으로 영속성 분리된것이다. 위에서 말한 것처럼 userPersist가 false이다. 그렇기 때문에 persist는 안되고 다시 db에서 조회한후 merge를 하는 수 밖에 없다.

 

그렇다면 reader에서 영속성컨텍스트가 있고 이후 분리된다??

-> 왜냐하면 processor에서 엔티티의 연관된 다른 엔티티를 수정한려고 한적이 있다. 근데 detached되어있어서 에러가 나왔다. 그래서 이때도 processor에서 merge를 통해서 값을 수정 했다

-> 왜냐하면 writer에서 persist가 안되고 merge를 통해 select -> update되었기 때문이다.

 

사실  맞는 말인지는 모르겠지만 지금까지 디버깅해보고 시도해본 결과 일단 이렇다. 

 

아 그렇다면 내가 말한 reader->processor->writer에서의 트랜젝션 유지라는 것은 chunk단위를 말한 것이구나. 즉 chunk단위로 실패하면 변경된 값이 rollback된다는 것이다.

 


 

두번째 시도방법... 변경감지. jpa에는 변경감지라는 것이 존재한다. 즉 영속성컨텍스트에 있는 값들이 변경되었을떄 쓰기지연저장소에 저장되어 트랜젝션이 끝나는 시점에서 쓰기지연저장소에 있는 (insert, update) 쿼리가 날아간다.

 

그래서 writer에서 merge를 호출하지 않게 아무것도 하지 않으면 어떨까..? 생각했다.

 

  @Bean
  public ItemWriter <WalkerReserveServiceInfo> reserveWriter(){
    return reserveService ->{};
  }

 

 

응 안된다. 생각해보면 detached되었다는 점은 영속성컨텍스트에서 변경에 대한 감지를 못한다는 것 아닐까?? 

 


그래서 결국 생각하기엔 batch에서 spring-data-jdbc를 통해 아예 jpa사용없이 +  merge 없이 update 하자.

 

 


아 배치는 공부하면 할 수록 어려운거같다. 일단 트랜젝션이라든지 왜 writer에서는 영속성컨텍스트가 detached되는 건지..jpa를 사용해서 유용하게 하는 방법은 없을지... 

 

일단 오늘의 batch 공부는 끝... 인프런 인강이나 들으러 가자...

 

 

 

# 2023.12.28 추가 내용

spring batch에서의 트랜젝션이 이해가 안되서...아예 간단하게 프로젝트를 만들어서 해보았다

 

일단 내가 내린 결론 

1. reader -> processor -> writer 트랜젝션은 유지된다.

2. writer는 update시 merge를 한다 -> 영속성 컨텍스트가 분리되어있다

 

 

첫번째 결론 근거

 

https://jojoldu.tistory.com/347

 

위의 블로그를 마니 참고하였다. 

reader에서 엔티티를 조회하는데 lazy 로딩 되는 엔티티에 대해서 fetch join으로 검색하지 않고 processor에서 lazy로딩 되게 하였다. 

  @Bean
  public JpaPagingItemReader <Team> teamReader(){

    return new JpaPagingItemReaderBuilder <Team>()
        .name("teamReader")
        .entityManagerFactory(entityManagerFactory)
        .pageSize(chunkSize)
        .queryString("SELECT t FROM Team t ")
        .build();
  }

  @Bean
  public ItemProcessor<Team,Team> teamMemberProcessor(){
    return member ->{
      List <Member> members = member.getMembers();
      for (Member member1 : members) {
        member1.winUp();
      }
      System.out.println(member);
      return member;
    };
  }

 

그랬더니 조회가 되었다. 즉 만약 processor가 트랜젝션 밖이라면  lazy관련한 예외가 터질텐데 터지지않았다.

트랜젝션 범위 안에 있기에 lazy loading이 가능했던 것이다.

 

이 외에도 writer에서 lazy loading을 시도해보면 잘 조회된다. 즉 트랜젝션 범위 안에 있다는점이다

 

 

두번째 결론 근거

 

JpaItemWriter docs를 보면 이렇게 적혀져있다.

ItemWriter that is using a JPA EntityManagerFactory to merge any 
Entities that aren't part of the persistence context.

 

즉 일단 ItemWriter는 merge를 사용한다. 근데 영속성 컨텍스트에 없는 엔티티들을 merge한다..?

 

 

그래서 앞선 코드이지만 jpaitemwriter의 dowrite코드를 보면 이렇다

if (!items.isEmpty()) {
	long addedToContextCount = 0;
	for (T item : items) {
		if (!entityManager.contains(item)) {
			if (usePersist) {
				entityManager.persist(item);
			}
			else {
				entityManager.merge(item);
			}
			addedToContextCount++;
		}
}

 

기본적으로 userPersist는 false로 되어있기에 merge가 된다. 

 

왜 영속성 컨텍스트가 분리될까??

 

영어로 설명되어있는 걸 찾았는데 정확하지는 않다.

The problem with sharing the context is problematic due to dirty checking, 
all the objects in the cache are checked before a select and also when commit.
This can make your performance even worse

 

즉 컨텍스트를 공유하는 것은 변경감지 때문에 문제가 발생할 수 있다. 캐시에 있는 모든 객체들은 select 쿼리 실행전 커밋시에 검사되며 이는 성능 저하게 될 수 있다.

 

사실 아직도 이해가 되지 않는다. 분명 트랜젝션이 유지되는 건 맞는데 왜 영속성 컨텍스트가 detached됬다고 뜨면서 merge를 시도할까??

 

 

그래도 일단 알아낸 것은 트랜젝션은 유지가 된다는점! 하 오늘도 이거에 5시간 동안 고민했다.. 다른거 할꺼많은데 오늘은 여기까지...

 

 

 

# 2023.12.29 내용 추가

앞에 쓴 내용을 거의 뒤집어 엎은 거같은데... 어째든 답을 찾은 것같다. 내용이 많아서 따로 게시글을 만들었다

https://haebing.tistory.com/117

 

앞의 내용들을 안지운 이유는 나의 사고과정을 기록하고 싶어서...ㅎㅎㅎ