DummyEntity라는 엔티티가 있다고 해보겠습니다. 특별한 건 없고, modifiedAt 이라는 필드에 @PreUpdate
어노테이션이 선언되어 있습니다.
1 | // DummyEntity.javascript |
아래의 DummyServiceImpl 제가 짠 소스 코드 입니다. 문제가 있는 소스 코드입니다. 회사 코드가 유출되면 안되니 간단하게 바꾸었습니다. 엔티티를 조회한 다음, 엔티티의 특정 필드의 값을 수정하여 변경 사항을 저장한 후에 큐로 이벤트를 발행할 때, modifiedAt 에는 변경이 이루어진 시간을 세팅하여 전달하고 싶었습니다.
1 | //DummyServiceImpl.java |
크게 복잡한 소스 코드는 아니라고 생각했습니다. 아래와 같이 이해하고 구현에 임했습니다.
- repository.findOne()를 이용해서 entity를 조회합니다.
- setter 메소드를 이용해서 entity 객체의 특정 값을 변경합니다.
- repository.save()를 이용해서 entity manager에게 변경 사실을 알려줍니다.
- entity의 내용을 큐로 발행합니다.(entity 객체를 바로 보내는 것은 아니고 convert한 다음에 전달합니다.
PreUpdate의 의미를 잘 못 이해 하는 부분에서 부터 문제가 시작되었습니다. 처음 가졌던 이해는 이러했습니다.
- persistence context(영속성 컨텍스트)에서 관리되고 있는 entity, 즉, 영속 상태의 entity의 내용이 수정되면, @PreUpdate 어노테이션에 의해서 modifiedAt의 값이 new Date()로 갱신될 것이라고 생각했습니다.
네, 크게 잘못된 생각이었습니다. 결과부터 말씀드리자면, 원하는 결과를 마주하려면 DummyEntity의 modifiedAt 필드에 선언되어 있는 어노테이션이 @PreUdpate가 아닌 @PrePersist
여야 했습니다. 아주 완벽하게 잘못된 이해였습니다. 이슈를 해결하기 위해서 스터디를 하는 과정에서 상당히 많은 부분 JPA를 틀리게 이해하고 있다는 사실을 깨달았습니다. 하나하나 짚어보려 합니다. 다음은 이제까지 제가 잘 못 이해하고 있던 내용입니다.
- repository.find()를 통해서 entity를 조회하면 persistence context에서 관리가 된다. 즉, 영속 상태의 객체를 반환한다.
- setter 메소드를 통해서 entity의 값을 변경하면 context의 1차 캐시에서 관리되고 있는 값이 변경된다.
- @PreUpdate 어노테이션을 선언해놓은 메소드가 있다면 2번이 실행되기 전에 수행된다.
- 트랜잭션이 끝나면서 context에 변경된 내용이 실제 데이터베이스에 반영된다.
첫번째 부터 다시 확인해보았습니다. repository.findOne()가 반환한 객체는 persistence context에서 관리되고 있는 영속 상태의 entity일까요? 정답은 맞아요. 첫번째는 맞게 이해하고 있었습니다. entityManager.find()나 JPQL을 사용해서 조회한 entity는 persistence context가 관리하는 영속 상태라고 합니다. 물론 내부적으로 entityManager.find()를 실행하고 있는 repository.find()도 동일합니다. persistence manager가 entity를 관리했을 때는 여러 장점이 있다고 하죠. 1차 캐시, 트랜잭션을 지원하는 쓰기 지연(transactional write-behind), 변경 감지(dirty checking), 지연 로딩(lazy loading) 등등. 너무 많으니 다음에 시간이 날때 하나하나 보려고 합니다.
그렇다면 setter 메소드를 통해서 entity의 값을 변경하면 persistence context에서는 무슨 일이 일어날까요? 네, 아무일도 일어나지 않습니다. dirty checking의 잘못된 이해 때문에, entity의 내용이 변경되면 객체의 reference를 타고 타서 persistence context에서 바로 변경된 내용을 알 거라고 미루어 짐작했는데, 이 부분이 가장 큰 오산이었습니다. EntityManger가 제공하는 메소드 중에는 엔티티의 갱신 여부를 판단하는 기능을 수행하는 메소드가 존재하지 않습니다.
2번과 3번의 잘못된 이해를 바로 잡기 위해서 JPA에서 제공하는 변경 감지란 것에 대해서 조금 더 알아보았습니다. JPA는 update()라는 메소드를 제공하고 있지 않습니다. entity에 setter 메소드를 통해서 값을 설정해주기만 하면 트랜잭션이 커밋되는 시점에 entity의 최종 상태(snapshot)를 데이터베이스에 자동으로 반영해줍니다. 이를 변경 감지라고 합니다. 트랜잭션이 커밋되면 entity manager 내부에서 flush() 메소드가 호출됩니다. 이때 1차 캐시에 저장되어 있는 앤티티와 스냅샷을 비교합니다. 변경 내용이 있는 경우 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 해당 내용을 쓰게 됩니다. 그리고 쓰기 지연 저장소의 SQL을 데이터베이스에 커밋하게 됩니다. @PreUpdate가 선어되어 있는 메소드는 실제 데이터베이스에 쓰기 SQL이 커밋되기 직전에 수행됩니다.
다시 문제의 소스 코드를 보면, JPA의 특성을 전혀 이해하지 못한 채로 구현이 되어 있음을 알 수 있습니다. 트랜잭션이 커밋되기 전이기 때문에 DummyEntity의 preUpdate() 메소드는 수행되지 않았고, sendQueue(dummy)가 실행되는 시점에 dummy 엔티티의 modifiedAt에는 new Date()가 들어가지 않았습니다. 문제의 소스코드는 아래의 내용으로 수정되었습니다.
1 | // DummyServiceImpl.java |
소스 상에 큰 변화는 dummyRepository.save(dummy)를 dummyRepository.saveAndFlush(dummy)로 수정한 것 뿐입니다. saveAndFlush 메소드를 호출함으로써 persistence context의 변경 내용을 데이터베이스에 반영하도록 강제했습니다. 따라서 sendQueue(dummy)가 호출되기 전에 dummy 엔티티의 preUpdate가 실행되어 변경된 modifiedAt의 값을 전달 할 수 있었습니다.
별 생각없이 코딩을 하는 행위는 굉장히 위험하다는 걸 다시 한번 깨닫게 되었습니다. 다음부터는 잘 아는 것만 쓸게요. 물론 지금은 아는 게 없으니 잘 알 수 있도록 평소에 꾸준히 공부를 해야겠습니다.
- 추가적으로 다음 링크의 이미지( https://pumpkineaterdotorg.files.wordpress.com/2013/08/lifeent30-e1375858118520.gif )를 함께 보신다면 엔티티의 영속 상태 변경에 따라 호출되는 API와 데이터베이스 변경 내용을 한눈에 파악할 수 있을 것 같습니다.
Comments