되자!백엔드개발자

[JPA] update할 때 @Transactional과 Repository.save()의 차이 본문

개발공부/JPA

[JPA] update할 때 @Transactional과 Repository.save()의 차이

HyunJng 2023. 7. 4. 05:43

개발 공부 중 update문을 구현하다가 궁금한 점이 생겼다.

나는 기존에 JPA를 이용하여 update를 구현할 때는 아래와 같이 dirty checking을 이용하여 구현하였다.

(물론 setter을 이용하진 않지만 설명상 ㅎㅎ)

    @Transactional
    public void edit(Long id, PostEdit postEdit) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다."));
        post.setTitle(postEdit.getTitle());
    }

그런데 가끔가다 아래처럼 JpaRepository의 save()를 이용하여 구현하는 분들도 계시는데 이 둘이 어떻게 다른지 궁금해져서 알아보았다.

    public void edit(Long id, PostEdit postEdit) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다."));
        post.setTitle(postEdit.getTitle());
        postRepository.save(post);
    }

 

 

Dirty checking이 실행되는 조건


더티체킹이란 영속성 컨텍스트에 포함된 엔티티들의 변화을 감지하고 트랜잭션이 commit되면 수정사항을 DB에 자동으로 반영하는 것이다.

 

JPA는 영속성 컨텍스트에 DB에서 가져올 때의 entity의 스냅샷을 1차캐시에 저장해둔다. 이렇게 영속성 컨텍스트에 관리되고 있는 객체에 어떠한 변화가 생겨 이 스냅샷과 다르게 된다면 update문을 쓰기지연 장소에 저장한 후 commit할 때 영속성 컨텍스트에 flush가 실행되어 DB에 변화가 반영된다. 

 

영속성 컨텍스트란 JPA 런타임 환경으로 엔티티의 수명주기 및 DB 영속성을 관리하는 역할을 맡는다. 쉽게말해 엔티티의 영구 저장 환경이자 애플리케이션과 DB사이의 객체를 보관하는 논리적 개념이다.

 

결과적으로 Dirty checking이 되기 위한 조건은 영속성 컨텍스트에 관리된 상태라는 것이다. 그렇다면 영속성 컨텍스트로 관리되는 시점이 무엇일까? 바로 트랜잭션 내부에 있을 때이다.

 

@Transactional


@Transactional은 트랜잭션의 시작과 종료 지점을 구분하는 트랜잭션 경계를 선언할 때 사용된다.

@Transactional이 붙은 메소드가 호출되면 JPA는 새로운 트랜잭션을 생성하여 영속성 컨텍스트와 연결시킨다. 트랜잭션의 종료 지점은 해당 트랜잭션이 커밋되는 시점으로 볼 수 있다. 이 커밋 시점에 영속성 컨텍스트는 영속성 컨텍스트가 엔티티에 변화를 감지하여 알맞은 update Query문 생성한다. 

 

여기서 중요한 점은 영속성 컨텍스트가 모든 엔티티의 변화를 감지하지 않는다는 것이다. 영속성 컨텍스트는 영속성 컨텍스트가 관리하는 엔티티, 즉 영속 상태의 엔티티만 dirty checking한다. 엔티티에겐 4가지 상태(비영속, 삭제, 영속, 준영속)이 있고 이 중 영속 상태의 엔티티만을 감지한다.


여기까지 공부하면 아래의 코드가 왜 save문이 없어도 업데이트가 반영되는지 알 수 있다.

  • DB에서 꺼내오 post는 영속성 컨텍스트에 의해 관리되고 있고
  • @Transactional로 인해 트랜잭션 안에 있는 상태에서 변경이 되고 있으니
  • 더티 체킹으로 인해 메소드가 끝날 때 DB에 반영이 되겠구나! 
    @Transactional
    public void edit(Long id, PostEdit postEdit) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다."));
        post.setTitle(postEdit.getTitle());
    }

 

 

 

save는 트랜잭션 상태인가?


여기서 한가지 의문이 더 생겼다.

@Transactoinal이 없을 경우 엔티티에 변화가 생겨도 DB에 제대로 반영되지 않은 이유(즉, 더티체킹이 안되는 이유)는 트랜잭션 안에 있지 않기 때문임을 위의 설명으로 이해할 수 있었다. 그런데 save는 어째서 바로 반영이 되는걸까? 마찬가지로 영속성 컨텍스트에 관리되고 있는 상태여야 가능할 것 같은데...

 

결론부터 말하면 save메소드 안에는 @Transactoinal 어노테이션이 있었다.

JpaRepository를 상속받으면 가져오게 되는 구현체인 SimpleJpaRepository에서는 save위에 @Transactional가 선언되어있는 것을 볼 수 있다.

따라서 아래의 코드는

  • findById메소드로 DB에서 가져오면 준영속상태로 바뀌었고
  • 다시 save 메소드로 실행되면서 @Transactional로 인해 영속 상태가 되는데
  • 1차캐시에 이미 해당 Entity에 대한 정보를 갖고 있어서 insert문이 아니라 update문이 실행되기 된다.

(정확히 1차캐시라는 용어가 맞을지는 모르겠지만.. 틀리면 말해주세요. save메소드가 insert와 update문을 어떻게 구별하는지 관해 공부해보면 나올 것 같은데 저는 계속 깊게깊게 들어가는 것 같아 일단 패스합니다.)

    public void edit(Long id, PostEdit postEdit) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다."));
        post.setTitle(postEdit.getTitle());
        postRepository.save(post);
    }

 

결론은 날라가는 쿼리문은 동일하다.

 

둘 중에 뭐가 더 나은 방법인가?


날라가는 쿼리문도 동일하고, 결과도 동일하지만

1. 더티체킹으로 update하는 것은 객체가 자기 할 일만 하는 코드이고

2. save를 이용하는 것은 객체의 관점에서 자신의 상태를 변경한 후에 DB에도 따로 반영해주는 코드이다.

 

따라서 객체지향 관점에서 더티체킹을 이용하는 것이 좋다. 


출처

https://onejunu.tistory.com/146

https://github.com/jojoldu/freelec-springboot2-webservice/issues/47

https://velog.io/@blackbean99/SpringBoot-Save%EB%8A%94-Insert%EC%9D%B8%EA%B0%80%EC%9A%94-update%EC%9D%B8%EA%B0%80%EC%9A%94

https://jaehoney.tistory.com/273