본문 바로가기
기획&구현

[BE] Bulk Update를 이용하여 대용량 데이터 한번에 update하기

by sungwon 2023. 2. 8.

안녕하세요. 당근플래너 팀 백엔드를 맡은 성원입니다.😊

 

당근플래너에서는 알림 버튼을 누르면 유저에게 온 모든 알림을 '읽음' 상태로 처리되도록 하는 API가 필요했습니다.

 

이때, 한 건씩 update를 하면 적은 용량의 데이터는 상관이 없지만 대용량을 한번에 처리할 때 성능상의 문제가 발생할 수 있는데요. 이 때문에 Bulk Update를 사용하여 한 번의 쿼리 만으로 다건의 데이터를 update를 하기로 했습니다.

 

 

Bulk Update

 

우선 Bulk Operation(벌크 연산)이란 단건의 UPDATE, DELETE를 제외한 다건의 UPDATE, DELETE 연산을 하나의 쿼리로 처리하는 것을 의미합니다.

참고로 Hibernate는 INSERT 문도 지원합니다.

저희는 여기서 Bulk Update 쿼리를 사용했습니다.

 

JPA는 보통 데이터를 가져와서 변경하면 변경 감지(dirty checking)을 통해 DB에 업데이트 쿼리를 수행합니다.

 

여기서 잠깐, 우리가 JPA에서 특정 entity의 값을 변경하려면 어떻게 했는지 알아보고 가볼까요?

1. em.find() 또는 select 쿼리를 날려서 영속성 컨텍스트에 entity를 저장 후 반환합니다.
2. 반환 받은 entity의 값을 변경합니다. (영속성 컨텍스트에 반영)
3. Commit 시점에 dirty checking이 일어나며 update 쿼리를 날려 DB에 반영합니다.

 

즉, 개별로 select이후 update가 이루어지기 때문에 수천건을 update하는 경우에는 굉장히 비효율적입니다.

이러한 비효율적인 상황들을 해결하기 위해 JPA를 사용해서도 수 천건의 데이터를 한 번에 update하는 Bulk Update 쿼리를 사용하여 해결할 수 있습니다.

 

 

@Modifying

 

JPA에서 Bulk Update를 하기 위해서는 @Query와 @Modifying을 사용해야 합니다.

 

다음 코드는 해당하는 회원한테 온 알림을 모두 읽음으로 update하는 코드입니다.

 

이 때 @Modifying 어노테이션을 누락시키면 다음과 같은 에러가 발생합니다.

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations

 

DML operation을 지원하지 않는다는 내용인데, JPA 기본 동작이 select - update 로 이루어져 있기 때문에 update만 하겠다고 알려주는 어노테이션이 바로 @Modifyiing입니다.

 

또한, DML 쿼리이기 때문에 반드시 @Transactional 어노테이션을 사용해야 합니다.

(저희는 Service에서 update하는 메서드에 사용했기 때문에 Repository에서는 사용하지 않았습니다.)

 

@Modifying을 사용할 때 주의할 점은 반환타입을 반드시 void, int, Integer로 해야합니다.

 

 

주의 사항

clearAutomatically

 

JPA에서는 영속성 컨텍스트에 있는 1차 캐시를 통해 entity를 캐싱하고, DB의 접근 횟수를 줄임으로써 성능개선을 합니다. 

@Id 값을 key 값으로 entity를 관리합니다.

그래서 findById 등을 통해 entity를 조회 했을 시 @Id값이 1차 캐시에 존재한다면 DB에 접근하지 않고, 캐싱된 entity를 반환합니다.

그러면, Bulk Update를 통해 변경 쿼리를 실행하고, 해당 entity를 조회하면 어던 일이 생길까요?

 

DB에는 값이 잘 변경이 되지만 findById()를 통해 해당 entity를 조회하면 변경 전의 값이 호출이 될 것입니다.

 

그 이유는 Bulk Update는 영속성 컨텍스트를 통한 entity 관리가 불가능합니다.

save 나 find 등을 통해 Repository에서 가져오게 되면 영속성 컨텍스트로 인식하지만, 직접적으로 update 쿼리를 실행하게 되면 영속성 컨텍스트로 인지할 기회가 없기 때문입니다.

 

즉, JPA의 1차 캐시는 DB에 접근 횟수를 줄이는 성능 개선을 할 수 있는 좋은 기능이지만, @Modifying과 @Query를 사용한 Bulk Operation에서는 1차 캐시를 포함한 영속성 컨텍스트를 무시하고 바로 쿼리를 실행하기 때문에 영속성 컨텍스트는 데이터 변경을 알 수 없습니다.

Bulk Operation 실행 시, 영속성 컨텍스트와 DB의 데이터 싱크가 맞지 않게 되는 것입니다.

 

이를 해결하기 위해서는 

1. 영속성 컨텍스트를 관리할 수 있는 EntityManager를 주입받습니다.
2. EntityManager의 flush()메서드를 사용해 남아있는 변동사항이 DB에 반영되도록 합니다.
3. EntityManager의 clear()메서드를 사용해 영속성 컨텍스트를 비워줍니다.

 

위와 같은 로직을 JPA에서는 @Modifying 어노테이션에 clearAutomatically = true의 옵션을 추가해줌으로써 간편하게 사용할 수 있습니다.(여기서 기본값은 false입니다.)

 

이렇게 하면 조회를 할 때 1차 캐시에 해당 entity가 존재하지 않기 때문에 DB 조회 쿼리를 실행하게 됩니다.

 

 

이상으로

 

Bulk Update를 활용한 Update하기를 알아봤습니다.

쿼리에 대한 내용은 그리 어렵지 않으니 조금만 찾아보셔도 금방 하실 수 있으실 겁니다!

 

부족한 글을 읽어주셔서 감사합니다!

 

짝짝!! 👏