[Spring Boot] JPA Flush 특징, 문제 해결
JPA 특징 중 쓰기지연과 Flush에 관련한 글이다.
JPA
JPA는 엔티티를 영속성 컨텍스트에서 관리하며, JPA의 모든 데이터 변경은 트랜잭션 안에서 실행된다.
tx.begin()
tx.commit()
tx.rollback()
쓰기지연
쓰기 지연이란 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 SQL을 모아뒀다가, 트랜잭션을 커밋할 때 모아둔 쿼리를 반영하는 과정이다.
이때 실제로 반영하는 작업을 flush
라고 한다.
쓰기 지연의 이점
- 성능상 이점 (50개의 insert를 50번씩 하는 것보다 한 번에 50개를 다 하는 게 낫다)
- 데이터베이스 테이블 row에 lock 걸리는 시간 최소화
Flush
플러시 하는 방법
- 트랜잭션 커밋 시 flush 자동 호출
- em.flush() 직접 호출
- JPQL 쿼리 실행으로 플러시 자동 호출
주의점
- 영속성 컨텍스트를 비우는 것이 아님 (clear는 따로 있다)
- 영속성 컨텍스트의 변경 내용을 DB에 동기화, 반영하는 것
@Query
JPA, Spring Data JPA에서 제공하는 interface 외의 쿼리를 실행할 수 있도록 직접 작성하는 방법
커스텀 Repository에 @Query를 붙여서 사용한다. (기본적으로 JPQL로 작성됨)
@Modifying
@Query 어노테이션으로 변경, 삭제 쿼리를 사용할 때 필요하다. (INSERT, UPDATE, DELETE)
주로 벌크 연산(다건의 UPDATE, DELETE를 하나의 쿼리로 실행하는 것) 시에 사용된다.
- @Query에 벌크 연산 쿼리를 작성하고 @Modifying을 붙이지 않으면 InvalidDataAccessApiUsageException이 발생한다.
- JPA Entity LifeCycle을 무시하고 실행된다.
clearAutomatically
쿼리 실행 직후 연속성 컨텍스트를 clear할것인지 정하는 속성으로, default는 false이다.
벌크 연산은 영속성 컨텍스트를 무시하고 쿼리를 실행하기 때문에, 실제로 DB에 반영된 값과 1차 캐시 값이 달라질 수 있다.clearAutomatically=true 설정으로 영속성 컨텍스트를 초기화하여, 이후 조회 쿼리 시에는 1차 캐시에 남아있는 엔티티가 없기 때문에 다시 DB에서 조회하여 싱크를 맞출 수 있다.
flushAutomatically
해당 쿼리를 실행하기 전, 영속성 컨텍스트의 변경 사항을 DB에 flush 할 것 인지를 결정하는 속성이며 default는 false이다.
Hibernate FlushModeType
- default가 false이지만, Spring Data JPA 구현체인 Hibernate의 FlushModeType 설정으로 인해 자동으로 쿼리 실행 전에 flush가 나간다고 한다.
Flush 실행 시 SQL 순서
쓰기 지연 저장소에 모아진 SQL이 Flush 될 때도 순서가 정해져 있다. 이는 외래 키 제약 조건을 위배하지 않기 위해 설계된 순서이다.
위의 정리된 내용을 바탕으로, 프로젝트에서 게임 데이터 1개를 지우면서 관련된 데이터들이 제대로 지워지지 않는 문제를 flush설정으로 해결할 수 있었다.
문제
한판의 게임에 대한 관련 데이터가 게임 기록, 팀 기록, 참가자 기록, 회원 기록 이렇게 4가지가 있고 deleteGameRecord
메서드를 실행하는 트랜잭션 내부에서 관련 데이터를 모두 삭제하게 구현하였다.
순서
- 1. 참가자 10명 기록 삭제
- 1.1 참가자 기록과 관련된 회원 기록 삭제, 회원 기록에 남은 게임 데이터수가 0이면 회원 삭제
- 1.2 참가자 기록 삭제 ( 10명 한 번에 삭제하는 벌크 연산 수행)
- 2. 팀 기록 삭제
- 3. 게임 기록 삭제
이때 1.1 -> 1.2로 넘어가는 부분에서 항상 10번째 멤버의 삭제, 변경 연산이 반영되지 않는 문제가 있었다.
원래 원했던 거 | 수행결과 |
원래대로라면 마지막 멤버가 삭제되는 delete from member
쿼리가 나간 뒤에 delete from participant, team, game
이 수행되어야 하는데 마지막 멤버 삭제가 로그만 남고 쿼리가 사라지는 문제가 있었다.
쓰기 지연에서 생기는 문제 같아서 memberService.removeStatistics() 수행 후에 결과가 반영되도록 명시적으로 em.flush()를 호출해줘서 해결했었는데 왜 이렇게 하면 해결이 된 건지 이유를 확실하게 모르겠어서 너무 찝찝했다. 위의 flush SQL 순서 , clearAutomatically 속성을 좀 더 생각해보니 원인을 알 것 같아서 적어본다!
결론
결론적으로 쓰기 지연 저장소에 담겨있는 SQL들이 flush 될 때 순서가 있어서, member를 지우는 쿼리보다 participant를 벌크 연산하는 쿼리가 먼저 나갔고 그러고 나서 clearAutomatically
설정으로 영속성 컨텍스트를 초기화하면서 아직 반영되지 않았던 member 업데이트(삭제) 쿼리가 지워져서 수행되지 않았던 것 같다. AbstractFlushingEventListener 클래스를 디버깅해보면서 이해할 수 있었다.
위에 보면 flush SQL 순서가 3. delete collection -> 5. delete in the order이다.
1. em.flush()를 명시적으로 호출해주거나
2. hibernate AUTO 설정과 또 별개로 이 쿼리가 수행되기 전에 미리 flush를 진행하도록 flushAutomatically=true를 설정해주면 된다.
순서가 멤버 업데이트, 삭제 -> em.flush -> participant 삭제 벌크 연산 -> em.clear 이렇게 진행되도록 하였다.
아직 잘 모르겠는 점
- hibernate auto 설정은 query수행 + 이전 결과에 대해서 flush이고 flushAutomatically는 query 수행 전이라서 이게 되는 건가?
- flush 안 하고 참가자 삭제 전에 member 엔티티를 조회하는 jpa 메서드(memberRepository.findAll
) 같은걸 수행해도 삭제가 반영이 됐다. member 말고 다른 테이블에 대한(participant.findAll()
)걸 중간에 넣었을 땐 똑같이 삭제가 안됐다. 뭔가 아직 확실하게 이해한 게 아닌 것 같다. ㅠ
- 이 부분에 대해서 고민하고 있으니까 이런 걸 굳이 flush로 설정해주지 않아도 participant와 Member 테이블에 연관관계를 맺어주고 외래 키 제약조건으로 처리하면 되지 않냐는 질문을 받았었는데 그것도 맞는것 같기도 하다.. 일단 participant 테이블과 member 테이블은 좀 별개라서 따로 관계로 맺지 않고 싶어서 (?) 지금은 이게 최선이라고 생각했다.. 다른 프로젝트 하면서 delete나 벌크 연산을 하는 경우가 온다면 다시 이런 문제가 또 나타나는지 고민을 해봐야겠다.. 지나가다 누가 의견을 좀 댓글에 달아주셨으면 좋겠다 ㅠ_ㅠ
출처
- 강의
- 인프런 김영한 님 자바 ORM 표준 JPA 프로그래밍
- 공식 문서
- https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/event/internal/AbstractFlushingEventListener.html
- https://docs.spring.io/spring-data/data-jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html
- https://docs.jboss.org/hibernate/orm/4.1/manual/en-US/html_single/#objectstate-flushing
- 블로그 - 도움이 많이 됐다 >_<