본문 바로가기

프로젝트/멋사 개인 프로젝트 (mutsa-SNS)

[20] mutsa-SNS-2 3일차 - (1) soft delete 관련 리펙토링 (@Transactional 이슈)

post가 delete될 때, 그와 관련되어 있는 comment와 like도 soft delete처리를 추가해 주었다.

 

이 과정에서 생겼던 Transactional 관련 이슈와

그로 인해 jpql method를 줄이고 jpa를 사용하기 위해 리펙토링 한 과정을 적어보았다...

 

1. @Transactional

jpa의 경우 더티체킹을 지원한다. 이는 상태 변경에 대한 검사라 생각하면 된다.

 

JPA로 엔티티를 조회하게 되면, 엔티티 조회 상태 그대로 스냅샷을 만들게 되고, 엔티티를 수정하여 트랜젝션을 커밋 할 때, flush가 일어나게 되는데, 그 때 초기 조회 상태의 스냅샷과 비교하여 변화가 발생하면, update query를 날리게 된다. 이로 인해 따로 save를 해주지 않아도 update query가 실행되는 것이다.

 

다만, 이러한 더티체킹은 영속성 컨텍스트에 등록된 엔티티에만 적용이 된다.

다른 설정을 따로 하지 않고 편하게 더티체킹이 일어나게 하도록 @Transactional을 사용하는 것으로 추정 된다.

해당 annotation을 사용하게 되면, 해당 범위 내 method를 호출 할 때 트렌잭션이 시작할 때 영속성 컨텍스트가 생성된다. 해당 method의 로직이 끗나면 트렌잭션이 커밋되면서 위의 문단의 과정이 실행되는 것이다.

 

더티체킹이 일어나는 시점은

  • 트랜젝션이 커밋될때
  • flush가 일어날 때
  • JPQL이 사용할 때

일어난다. 사실 트랜젝션이 커밋될때는 flush가 일어나므로 밑의 조건과 동일하다 볼 수 있다.

 

이 때, JPQL에 대해 좀 더 조사해 보았는데,

JPQL은 영속성 컨텍스트의 데이터 변화에 기여하지 않는다. 즉 영속성 컨텍스트를 거치지 않고, db에 직접 query하여 결과를 가져온다고 한다.

따라서 JPQL이 실행하기 전에 flush가 자동으로 실행되며 영속성 컨텍스트와 db의 일관성을 확보한다.

(flush 란 영속성 컨텍스트의 변경 내용을 db에 저장하는 것이다.)

 

다만, JPQL 쿼리가 나가면서 수정이 일어나더라도, 영속성 컨텍스트의 데이터는 변경시키지 않았기 때문에, 수정 되기 전의 상태가 저장되어 있다. 따라서

  • flush 와 save를 통해 영속성 컨텍스트의 내용을 db에 반영하고, 영속성 컨텍스트의 초기화
  • @Modifying(clearAutomatically = true)를 이용하여 query 실행 후, 영속성 컨텍스트를 초기화

같은 방법을 통해 db와 영속성 컨텍스트 간의 동기화를 하여, 후에 같은 트랜젝션 안에 있는 실행할 JPA query 등에 영향이 없도록 하는 것이 좋아 보인다.

 

이러한 JPQL과 JPA의 구동 차이 때문에, 최대한 JPA method만 사용하도록 하였다.

간단한 query는 자바 안에서 처리하는 것이 좋다는 멘토님의 조언도 위의 이유 때문으로 추정된다.

 

다만, 여러 JPA query와 JPQL query를 혼용하여 사용하며 @Transactional을 붙여주지 않을 때 error가 발생하였는데,

왜 error가 발생하였으며 이 때 왜 JwtTokenFilter Error가 갑자기 발생했는지 원인을 밝혀내진 못했다.

 

2023-01-05 16:05:04.184 ERROR 8700 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

위의 error가 뜨고 나서,

java.lang.NullPointerException: null

Controller에서 Authentication을 불러오는 데서 발생하고, JwtTokenFilter에서 발생했다고 떴다.

 

따라서 일단 db에 내용이 변경되는 로직이 있는 service method에는 모두 @Transactional annotation을 붙여 주었다.

 

<Reference>

1. JPQL 관련

https://cobbybb.tistory.com/11

 

JPQL과 영속성 컨텍스트의 관계

flush 영속성 컨텍스트의 변경 내용을 DB에 반영하는 이벤트 JPQL JPQL : Jpa의 Entity 객체들을 이용해서 Query할수있는 객체 지향 쿼리 예제 /** * User.java */ @Entity @Getter @Setter @NoArgsConstructor public class User

cobbybb.tistory.com

https://wwlee94.github.io/category/blog/spring-transactional-precautions/

 

[Spring] @Transactional 사용 시 주의점과 JPQL 주의점

wwlee94.github.io

https://joont92.github.io/jpa/JPQL/

 

[jpa] JPQL

JPA에서 현재까지 사용했던 검색은 아래와 같다. 식별자로 조회 EntityManager.find() 객체 그래프 탐색 e.g. a.getB().getC() 하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다. 그

joont92.github.io

2. 영속성

https://ultrakain.gitbooks.io/jpa/content/chapter3/chapter3.2.html

 

3.2 영속성 컨텍스트란? · jpa

 

ultrakain.gitbooks.io

https://velog.io/@seongwon97/Spring-Boot-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context

 

[Spring JPA] 영속성 컨텍스트(Persistence Context)

영속성 컨텍스트란? 엔티티를 영구 저장하는 환경이라는 뜻으로 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할을 한다.

velog.io

https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80

 

JPA 영속성 컨텍스트란?

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.em.persist

velog.io

3. 더티체킹

https://jojoldu.tistory.com/415

 

더티 체킹 (Dirty Checking)이란?

Spring Data Jpa와 같은 ORM 구현체를 사용하다보면 더티 체킹이란 단어를 종종 듣게 됩니다. 더티 체킹이란 단어를 처음 듣는분들을 몇번 만나게 되어 이번 시간엔 더티 체킹이 무엇인지 알아보겠습

jojoldu.tistory.com

https://interconnection.tistory.com/121

 

JPA 더티 체킹(Dirty Checking)이란?

JPA(Java Persistence API)를 사용하면서 더티 체킹과 트랜잭션의 관계에 대해서 알고 있지 않으면, 비즈니스 로직에서 다루는 엔티티 데이터가 꼬이는 경우가 발생합니다. 데이터가 꼬이는 경우를 방

interconnection.tistory.com

4. flush

https://gmlwjd9405.github.io/2019/08/07/what-is-flush.html

 

[JPA] 플러시(Flush)란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

2. JPQL query -> JPA 사용

1번 내용 관련 공부를 하면서, 되도록 JPA를 사용하는 것이 편리하고 문제가 덜 생기겠구나를  깨닳았다.

다소 복잡한 쿼리의 경우에만 JPQL 로 작성하고, 간단한 변경 로직이라면 JPA로 설정하는 것이 좋다는 멘토님의 조언이 어떤 뜻인지 살짝은 느낌이 오는 듯 하다.

 

따라서 기존의 deletedAt에 시간을 update하는 JPQL 쿼리를 java 내부 로직으로 변경해 주었다.

아래는 기존의 JPQL query이다.

@Query("update Like l set l.deletedAt = current_timestamp where l.deletedAt is null and l.post = :post")
@Modifying(clearAutomatically = true)
void deleteAllByPost(@Param("post") Post post);

Like와 manyToOne 관계를 맺은 post를 기준으로 검색하여, 해당 post와 관계를 갖고 있고, deletedAt 이 null인 경우, 해당 post가 delete 될 때, soft delete를 하기 위한 query이다.

단순히 JpaRepository의 deleteAllByPost를 이용하지 않고 이 query를 따로 짜준 이유는, 이미 좋아요 취소로 인해 deletedAt이 설정되어 있는 것들의 deletedAt의 시간이 갱신되는 것을 원하지 않았기 때문이다.

 

다만, 생각해보니 다소 간단힌 로직이라, java 내부 로직으로 분리할 수 있으면 분리하는것이 좋다 생각이 들었다.

따라서 JpaRepository에 아래의 method를 추가해 주고,

Optional<List<Like>> findByPost(Post post);

 

PostService에서 아래의 로직으로 deletedAt이 null인 like만 뽑아 deleteAt을 JpaRepository의 delete를 이용하여 수정하였다. (Like Entity에서 annotation으로 soft delete를 설정한 상태이다.)

@SQLDelete(sql = "UPDATE likes SET deleted_at = current_timestamp WHERE id = ?")
Optional<List<Like>> likes = likeRepository.findByPost(post);

for (int i = 0; i < likes.get().size(); i++) {
    if (likes.get().get(i).getDeletedAt() == null) {
        likeRepository.delete(likes.get().get(i));
    }
}

 

@Transactional annotation은 단지 error가 났을 때 db에 반영되는 것을 롤백하기 위한 annotation이라고만 알고 있었는데, 이번 error로 인해 어떤 기능을 하는지 원초적으로 살펴볼 기회가 된 것 같다.

JPA와 JPQL의 차이점도 알게 되었으며, 왜 JPA가 편리한지 생각 할 수 있는 계기가 된 듯 하다.

 

아직은 단순 로직만 짜기 바쁜 상태밖에 안되는 것을 깨닳았고, 정말 공부가 많이 필요하다 생각이 들었다.

 

<전체 Reference>

https://cobbybb.tistory.com/11
https://wwlee94.github.io/category/blog/spring-transactional-precautions/
https://joont92.github.io/jpa/JPQL/
https://ultrakain.gitbooks.io/jpa/content/chapter3/chapter3.2.html
https://velog.io/@seongwon97/Spring-Boot-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context
https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80
https://jojoldu.tistory.com/415
https://interconnection.tistory.com/121
https://gmlwjd9405.github.io/2019/08/07/what-is-flush.html