본문 바로가기
개발 이론/JPA

Delete와 Insert의 트랜잭션 문제

by dal_been 2024. 6. 2.
728x90

일중에 S3이미지를 삭제 + 데이터 삭제 후에 다시 S3이미지 넣고 데이터 삽입하는 로직이 있었다. 즉, 수정하는 로직.

근데 S3이미지까지는 잘 삭제가 되는데 데이터가 삭제가 안되고 삽입만 되는 것이다... 

그래서 보자마자,, 아 트랜젝션 같은데,,, 싹 냄새가,,, 그래서 집에 와서 한번 테스트해보았다.

 


코드

 

게시물과  댓글 엔티티가 존재한다. 여기서 정말 말도안되지만.... 게시글이 수정할때 댓글도 같이 수정되어 댓글이 모두 삭제후 -> 새롭게 Insert한다고 가정한다.

 

@Entity
class Post(
    var content: String,
) : BaseEntity() {

    @OneToMany(mappedBy = "post", cascade = [CascadeType.ALL])
    val comments: MutableList<Comment> = mutableListOf()

}

@Entity
class Comment(
    @ManyToOne val post: Post,
    var comment: String,
) : BaseEntity()

 

 

그리고 서비스 코드와 컨트롤러 코드 (레포지토리 코드는 너무 간단하여 생략)

 

@RestController("/update")
class UpdateController(
    private val updateService: UpdateService
) {

    @PutMapping("{id}")
    fun update(
        @PathVariable id: Long,
        @RequestBody updateDto: UpdateDto
    ): UpdateResponseDto {
        return updateService.update(id, updateDto)
    }
}

@Service
class UpdateService(
    private val postRepository: PostRepository,
    private val commentRepository: CommentRepository,
) {

    @Transactional
    fun update(
        id: Long,
        updateDto: UpdateDto
    ):UpdateResponseDto {
        val post = postRepository.findById(id).orElseThrow()

        return post.apply {
            content = updateDto.content
            val comments = commentRepository.findByPostId(id)
            commentRepository.deleteAll(comments)
            updateDto.commentList.mapNotNull {
                Comment(
                    comment = it,
                    post = post,
                )
            }.run {
                commentRepository.saveAll(this)
            }
        }.toResponse()
    }
}

 

서비스 코드를 간단하게 설명하자면

일단 id값으로 Post를 찾고 UpdateDto로 들어온 content값을 수정후

comments를 찾아서 다 삭제한후 UpdateDto에 있는 comments를 새롭게 넣어준다.

이후 UpdateResponseDto로 응답해준다

 

기본적인 데이터는 이렇게 세팅되어있다.

 

INSERT INTO POST(ID, CONTENT) VALUES (1, '게시글1');
INSERT INTO POST(ID, CONTENT) VALUES (2, '게시글2');

INSERT INTO COMMENT(ID, POST_ID ,COMMENT) VALUES (34,1,'게시글1 코멘트1');
INSERT INTO COMMENT(ID, POST_ID ,COMMENT) VALUES (35,1,'게시글1 코멘트2');
INSERT INTO COMMENT(ID, POST_ID ,COMMENT) VALUES (36,1,'게시글1 코멘트3');

 

로그
2024-06-02T19:19:50.339+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.example.kotlinspring.delete.service.UpdateService.update]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-06-02T19:19:50.340+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@390843be]
2024-06-02T19:19:50.340+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.kotlinspring.delete.service.UpdateService.update]
2024-06-02T19:19:50.341+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(926775500<open>)] for JPA transaction
2024-06-02T19:19:50.341+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2024-06-02T19:19:50.341+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.updated_at from post p1_0 where p1_0.id=?
2024-06-02T19:19:50.361+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
2024-06-02T19:19:50.362+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByPostId]: This method is not transactional.
Hibernate: update post set content=?,created_at=?,updated_at=? where id=?

 

일단 Post의 내용을 바꾸는 로그부터 보자면

 

Creating Transaction으로 트랜젝션이 만들어지고 Participating을 통해 조회와 Update에 대한 쿼리가 나간다.

이후

 

2024-06-02T19:19:50.405+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2024-06-02T19:19:50.405+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]
2024-06-02T19:19:50.406+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]
2024-06-02T19:19:50.406+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(926775500<open>)] for JPA transaction
2024-06-02T19:19:50.406+09:00 DEBUG 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2024-06-02T19:19:50.406+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll]
Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
2024-06-02T19:19:50.410+09:00 TRACE 11849 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll]

 

다시 comment에 대한 로직을 수행하기 위해 Participating을통해 트랜젝션에 참여한다.

여기서 주의깊게 보면 delete쿼리는 없고, insert쿼리만 있다..

 

그래서 H2 데이터를 보면

 

역시나 데이터를 삭제되지 않았다..

 

 

delete쿼리가 나가지 않는 이유

 

첫번째 쓰기지연저장소 때문이다.

 

영속성컨텍스트에서 트랜젝션을 같이 사용할때, select쿼리는 바로 처리한다. 다만 delete, insert의 경우 잠깐 저장해두었다가

commit시 한번에 처리하게 된다.

 

근데 여기서 문제점이 존재한다... 일단 쓰기지연저장소에 delete와 Insert쿼리 둘다 존재한다면,,, insert부터 나간다.

1. Inserts, in the order they were performed
2. Updates
3. Deletion of collection elements
4. Insertion of collection elements
5. Deletes, in the order they were performed

 

그러다보니 내가 미리 insert할 값의 comment id를 1부터하지 않는 이유는 insert가 먼저나가서 id 값에 대한 충돌이 일어났다.

 

 

두번째 response에대한 toResponse메서드때문이다 = 재조회

 

data class UpdateResponseDto(
    val content: String,
    val comments: List<String>,
) {
    companion object {
        fun Post.toResponse(): UpdateResponseDto {
            println("여기니?")
            val response = UpdateResponseDto(
                content = this.content,
                comments = comments.map { it.comment },
            )
            println("여기다")
            return response
        }
    }
}

 

 

여기서 보면 comments.map{it.comment}에서 댓글에 대한 쿼리가 한번더 나간다.

Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
2024-06-02T19:38:00.871+09:00 TRACE 12313 --- [KotlinSpring] [nio-8080-exec-6] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll]
여기니?
Hibernate: select c1_0.post_id,c1_0.id,c1_0.comment,c1_0.created_at,c1_0.updated_at from comment c1_0 where c1_0.post_id=?
여기다
2024-06-02T19:38:00.875+09:00 TRACE 12313 --- [KotlinSpring] [nio-8080-exec-6] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.kotlinspring.delete.service.UpdateService.update]
2024-06-02T19:38:00.875+09:00 DEBUG 12313 --- [KotlinSpring] [nio-8080-exec-6] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2024-06-02T19:38:00.875+09:00 DEBUG 12313 --- [KotlinSpring] [nio-8080-exec-6] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1363592465<open>)]

 

delete쿼리가 아직 발생하지 않은채 select쿼리가 발생하니.. 꼬였다고 해야하나(삭제할 데이터들이 영속성에 다시 들어옴)... 그러다보니 delete쿼리가 안나가는 것이다

 

단순히 서비스 코드에서 toReponse메서드를 빼면

 

Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
Hibernate: insert into comment (comment,created_at,post_id,updated_at,id) values (?,?,?,?,default)
2024-06-02T19:40:47.356+09:00 TRACE 12396 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll]
2024-06-02T19:40:47.356+09:00 TRACE 12396 --- [KotlinSpring] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.kotlinspring.delete.service.UpdateService.update]
2024-06-02T19:40:47.356+09:00 DEBUG 12396 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2024-06-02T19:40:47.356+09:00 DEBUG 12396 --- [KotlinSpring] [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(74978134<open>)]
Hibernate: delete from comment where id=?
Hibernate: delete from comment where id=?
Hibernate: delete from comment where id=?

 

앞에 이유로 Insert쿼리가 먼저 나가긴하지만 어째든 delete쿼리도 나간다.

 

해결책

 

그래서 왠만해서는 delete, update와 같이 데이터를 변경하는 사항들은 트랜젝션을 분리해주는게 나은방법이라고 생각한다.

 

@Service
class UpdateService(
    private val postRepository: PostRepository,
    private val commentRepository: CommentRepository,
) {

    @Transactional
    fun update(
        id: Long,
        updateDto: UpdateDto
    ):UpdateResponseDto {
        val post = postRepository.findById(id).orElseThrow()

        return post.apply {
            content = updateDto.content
            val comments = commentRepository.findByPostId(id)
            delete(comments)
            updateDto.commentList.mapNotNull {
                Comment(
                    comment = it,
                    post = post,
                )
            }.run {
                commentRepository.saveAll(this)
            }
        }.toResponse()
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun delete(comments: List<Comment>) {
        commentRepository.deleteAll(comments)
    }
}

 

그래서 propagation으로 분리해줬지만 예상되로 분리되지않고 같은 트랜젝션에 participating하여 delete쿼리가 나가지 않았다..

왜...?

 

프록시와 관계이다.

트랜젝션을 적용하기 위해서는 프록시를 통해 대상 객체를 호출한다.

update메서드로 트랜젝션을 호출한후 내부 메서드 delete로 호출하면 프록시르 부르는게 아니라 그냥 실제 대상 객체인스턴스에서 불러진다.

즉 프록시가 아닌 실제 객체의 메서드가 호출되면서 트랜젝션이 적용되지 않는것이다.

(동일한 클래스내에서 트랜젝션이 적용된 내부 메서드 호출시 프록시를 거치지 않음)

 

해결방법은 여러가지다

트랜젝션넴플릿을이용해서 커밋을 직접 제어하던가 아예 클래스를 분리하는 방법이 존재한다.

 

 

 


일하면서 오랜만에 만난 즐거운 문제점이었다. 근데 toResponse로 다시 select쿼리가 나가는게 좀 맘에 안들긴 하는데..

일단 여기까지 정리 완료.