@EntityListener와 @PostLoad의 이슈
목차
- findBy에서 왜
UPDATE
쿼리가 나가지? - 프로시저와 연관이 있을까?
- 범인은
@EntityListeners
일부 중요한 비즈니스 변수명은 임의의 값으로 변경했다는 점을 알아두자.
1. findBy
에서 왜 UPDATE 쿼리가 나가지?
아래와 같은 비즈니스 로직이 있었다.
@Override
@MyProjectWriteTransactional // 커스텀 트랜잭션 어노테이션
public String changeMoney(ChangeMoneyCommand command) {
// entity를 flush 하는 로직
// procedure를 콜 하는 로직
return MemberRatingQuery.getMemberRatingMandatory(command.memberId()).getMemberNo();
}
중간에는 Procedure
를 콜 하는 로직이 전부이고, 이 외에 JPA의 Entity와 연관이 있는 로직은 마지막의 return
값으로 던져주는 로직이 전부다.
그런데 이상하다. 해당 로직을 호출하면 UPDATE
쿼리가 나가는 것이었다.
Hibernate: update bztat set RANKID=?,BETDRAW=?,BETLOSE=?,.... (생략) where MEMBERID=?
말이 안된다. JPA
에서 제공하는 findBy
는 말 그대로 조회일 뿐, 데이터를 UPDATE하는 기능은 없다.
이슈를 분석해보자.
2. 프로시저와 연관이 있을까?
return
을 하기 전에 중간에 있는 로직에서 프로시저를 콜 하고 있다.
프로시저의 로직은 대략적으로 아래와 같다.
@PersistenceContext(unitName = "myEntityManager")
private EntityManager myEntityManager;
@Override
public int changeMoney(String code, String memberId, .. other parameters) {
StoredProcedureQuery query = myEntityManager.createStoredProcedureQuery("BZTAT_UPDATE_MONEY");
query.registerStoredProcedureParameter("IN_CODE", String.class, ParameterMode.IN);
// other registerStoredProcedureParameters
query.setParameter("IN_CODE", code);
// other set parameters
query.execute();
return (Integer) query.getOutputParameterValue("OUT_VAL");
}
프로시저의 OUT VALUE가 복수개인 관계로, @Procedrue
로 호출하는 대신 StoredProcedureQuery
를 사용했다.
하지만 프로시저를 콜 하면 데이터의 DML은 즉시 이루어진다.
다시 말해, JPA
의 영속성 컨텍스트와는 별개로 다이렉트로 DB에서 조작이 이루어진다는 뜻이 된다.
// entity를 flush 하는 로직
// procedure를 콜 하는 로직
그래서 비즈니스 로직에 Procedure
를 콜 하기 전, 관련 entity
를 flush
하는 로직을 진행했다.
바로 DB에 붙어서 조작이 이루어지니, 혹시 이전에 entity
와 관련된 변경 로직이 (지금 당장은 없어도) 추후에 생길 경우, 아무도 모를 이슈가 발생할 수 있으니 말이다.
그러므로 프로시저는 이슈가 없다는게 밝혀졌다.
그럼 원인은 뭘까?
3. 범인은 @EntityListeners
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "bztat")
@EntityListeners(NullValuePostLoadListener.class)
@EqualsAndHashCode
public class MemberRating {
@Column(name = "MEMBERSEQ")
@Builder.Default
private Long memberSequence = 0L;
@Id
@Column(name = "MEMBERID")
@Builder.Default
private String memberId = "";
@Column(name = "MEMBERNO")
@Builder.Default
private String memberNo = "";
// 이하 생략...
findBy
로 조회하는 Entity
는 위와같이 구성되어있다.
원인은 @EntityListeners(NullValuePostLoadListener.class)
였다.
@EntityListeners
의 역할은 공식 문서에 다음과 같이 적혀있다.
Specifies the callback listener classes to be used for an entity or mapped superclass.
This annotation may be applied to an entity class or mapped superclass.
단순히 JPA
의 리스너 클래스를 연결해주는 역할이다.
그런데 여기서 연결된 리스너 클래스는 아래와 같이 구현되어있다. (참고로 자체 개발된 커스텀 리스너 클래스다.)
@Slf4j
public class NullValuePostLoadListener {
@PostLoad
public void setDefaultValuesAfterLoad(Object entity) {
setDefaultValues(entity);
}
private void setDefaultValues(Object entity) {
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.canAccess(entity)) {
field.setAccessible(true);
}
try {
if (field.get(entity) == null) {
if (field.getType() == Integer.class) {
field.set(entity, 0);
보다시피 @PostLoad
로 구현되어있는데, 이 어노테이션을 따라가보면 아래와 같이 공식 문서에 설명되어있다.
Executed after an entity has been loaded into the current persistence context or an entity has been refreshed.
다시 말해 영속성 컨텍스트에서 조회된 직후, 또는 refresh를 호출한 후 (= 또는 2차 캐시에 저장되어있는 경우) 이벤트가 발생한다.
위에 선언된 로직을 보면 알겠지만, NullValuePostLoadListener
는 Reflection
을 통해 조회한 객체의 필드가 null
인 경우 정해진 초기화 값으로 셋팅을 해주고 있다.
이러다보니 findBy
가 종료되고 값이 반환되며 @MyProjectWriteTransactional
이 종료된 직후,더티 체킹으로 인해 UPDATE
쿼리가 나가고 있는 상황이었다.
막상 분석해보니 별건 아니였지만, 덕분에 당연하게 생각했던 어노테이션을 다시 한 번 복기할 수 있는 기회가 됐다.