이 글은 이전 글인 "JPA 사용시 엔티티 상태 확인하기"에서 이어지는 글이다.

이전 글에서 @Transactional을 사용해서 savePost() 메소드 전체에 영속화 컨텍스트를 적용했고 따라서 그 안에서 새로 저장했던 newPost 인스턴스가 Persistent 상태이길 예상했지만 기대와는 달리 EntityManager의 contains로 확인해본 결과 false가 출력되었다. 결론부터 말하자면,

savePost 메소드에 트랜잭션이 적용되지 않았다.

결론은 간단하지만 구체적으로 '왜 트랜잭션이 적용되지 않았지?'에 답하려면 스프링이 애노테이션 기반의 트랜잭션을 어떻게 처리하는지 이해해야 하는데 @Transiaction이라는 애노테이션을 쓰는건 맞지만 private 메소드에 쓰는건 무의미하다. 오버라이딩이 가능한 메소드에 써야 한다. 그렇다면 savePost() 메소드를 public으로 만들면 될까?

@Transactional
public void savePost() {
    Post post = new Post();
    post.setTitle("keesun");

    Post newPost = postRepository.save(post);
    System.out.println(postRepository.findById(newPost.getId()));
    System.out.println(entityManager.contains(newPost));
}

public 메소드로 바꿨지만 이렇게 해도 savePost()에는 트랜잭션이 적용되지 않고 영속화 컨텐스트는 newPost를 캐싱하지 않는다. 그래서 결과는 private 메소드를 사용했을 때와 동일하다. 아니 왜? public 메소드는 오버라이딩이 가능하고 그 위에 @Transactional을 붙였으니까 되야 하는게 맞는거 아냐?

뭐 그렇게 생각할 수도 있겠지만.. 스프링의 트랜잭션 처리가 스프링 AOP를 기반으로 하고 있으며 스프링 AOP가 다이나믹 프록시를 기반으로 동작한다는 것을 알고 있다면 이렇게 해도 트랜잭션이 적용되지 않는 이유를 이해할 수 있을 것이다.

프록시 기반 AOP의 단점 중에 하나인 프록시 내부에서 내부를 호출할 때는 부가적인 서비스(여기서는 그게 바로 트랜잭션)가 적용되지 않는다. 호출하려는 타겟을 감싸고 있는 프록시를 통해야만 부가적인 기능이 적용되는데 프록시 내부에서 내부를 호출 할 때는 감싸고 있는 영역을 거치지 않기 때문이다.

프록시로 감싼 타겟 (JpaRunner)를 외부에서 호출할 때 run()이라는 public 메소드를 호출하는데 (ApplicationRunner를 구현했기 때문에) 이 때 run() 메소드에는 트랜잭션이 적용되지 않는다. 왜냐면 @Transactional을 savePost()만 붙였으니까. 그런데 그렇게 호출한 run()이 내부에서 @Transactional을 사용한 savePost()를 호출하더라도, JpaRunner 밖에서 호출이 되는게 아니라 프록시 내부에서 savePost()를 바로 호출하기 때문에 타겟을 감싼 트랜잭션이 적용되지 않는 것이다. 차라리 JpaRunner 밖에서 savePost() 메소드를 바로 호출했다면 트랜잭션이 적용됐을 것이다.

그래서 이 문제를 어떻게 해결하냐고? 뭐 여러가지 방법이 있지만 제일 간단한 방법은 @Transactional을 run() 메소드로 옮기면 된다. 그럼 run()을 호출 할 때부터 트랜잭션이 적용되면서 그 메소드에서 호출하는 다른 메소드도 전부 해당 트랜잭션 안에서 처리하기 때문에 insert 쿼리 이후에 select도 발생하지 않으며 EntityManager의 contains 메소드가 true를 리턴하는 것을 확인할 수 있다.

@Transactional
@Override
public void run(ApplicationArguments args) throws Exception {
    savePost();
}

public void savePost() {
    Post post = new Post();
    post.setTitle("keesun");

    Post newPost = postRepository.save(post);
    System.out.println(postRepository.findById(newPost.getId()));
    System.out.println(entityManager.contains(newPost));
}

해결 방법은 이토록 간단하지만 이런 배경 지식을 학습하지 않는다면 다음에 비슷한 문제가 생겨도 해결하기 어려울 것이다.

스프링 프레임워크 핵심 기술 - 인프런
이번 강좌는 스프링 부트를 사용하며 스프링 핵심 기술을 학습합니다 따라서 스프링 부트 기반의 프로젝트를 사용하고 있는 개발자 또는 학생에게 유용한 스프링 강좌입니다.
더 자바, 코드를 조작하는 다양한 방법 - 인프런
여러분이 사용하고 있는 많은 자바 라이브러리와 프레임워크가 ”어떻게” 이런 기능을 제공할 지 궁금한적 있으신가요? 이번 강좌를 통해 자바가 제공하는 다양한 코드 또는 객체를 조작하는 방법에 대해 학습하고 여러분의 자바 기술을 한 단계 업그레이드 하세요.

참고