RestAPI (8) @트랜잭션의 커밋 시점

별빛사막·2024년 12월 24일

1. 문제 : null이 출력됨

댓글 작성 메서드(POST) : http://localhost:8080/api/v1/posts/3/comments

실행결과

{
    "resultCode": "201-1",
    "msg": "null번 댓글이 작성되었습니다."
}

2. 원인 : @Transactional 어노테이션 수행 시점

  • @GeneratedValue(strategy = IDENTITY)와 같은 ID 생성 전략에서, ID는 엔티티가 DB에 실제로 저장되기 전까지 생성되지 않는다.

  • @Transactional 어노테이션이 붙은 메서드는 메서드는 해당 메서드가 완벽히 종료된 이후 영속성 전이가 발생한다.

  • 즉 트랜잭션이 끝나야 DB에 insert되면서 getId()가 가능한데, 현재는 트랜잭션이 끝나지 않은 중간에 getId()를 하려고 해서 id에 null이 나오는 것이다.

    @PostMapping
    @Transactional
    public RsData<Void> writeItem(
            @PathVariable long postId,
            @RequestBody @Valid PostCommentWriteReqBody reqBody
    ) {
        Member actor = rq.checkAuthentication();
        Post post = postService.findById(postId).orElseThrow(
                () -> new ServiceException("404-1", "%d번 글은 존재하지 않습니다.".formatted(postId))
        );
        PostComment postComment = post.addComment(
                actor,
                reqBody.content
        );
        
        // postComment.getId()는 영속성 컨텍스트가 DB에 반영되기 전에는 null일 수 있음
        return new RsData<>(
                "201-1",
                "%d번 댓글이 작성되었습니다.".formatted(postComment.getId())
        );
    }

Hibernate > 영속성 컨텍스트 저장 > 쓰기 지연으로 DB 반영 대기 > ID 생성 지연 > DB 반영 전까지 getId()는 null

3. 해결방법 3가지

3-1. 액션 메서드와 일반 메서드 분리 (self 활용)

public class ApiV1PostCommentController {
    @Autowired //추가함
    @Lazy //추가함
    private ApiV1PostCommentController self;
    
    @PostMapping
    public RsData<Void> writeItem(
            @PathVariable long postId,
            @RequestBody @Valid PostCommentWriteReqBody reqBody
    ) {
        PostComment postComment = self._writeItem(postId, reqBody); //추가함

        return new RsData<>(
                "201-1",
                "%d번 댓글이 작성되었습니다.".formatted(postComment.getId())
        );
    }

	
    @Transactional
    public PostComment _writeItem( //추가함
            long postId,
            PostCommentWriteReqBody reqBody
    ) {
        Member actor = rq.checkAuthentication();

        Post post = postService.findById(postId).orElseThrow(
                () -> new ServiceException("404-1", "%d번 글은 존재하지 않습니다.".formatted(postId))
        );

        return post.addComment(
                actor,
                reqBody.content
        );
    }
  • @Autowired : 현재 클래스의 프록시 객체(Proxy)를 주입받아야 하기 때문에 사용
  • @Lazy : Spring이 실제로 self가 필요할 때만 초기화하도록 지연하기 위해 사용함.

3-2. EntityManager에서 제공하는 flush() 이용하기

public class ApiV1PostCommentController {
    private final EntityManager em; //추가함
    
    @PostMapping
    @Transactional
    public RsData<Void> writeItem(
            @PathVariable long postId,
            @RequestBody @Valid PostCommentWriteReqBody reqBody
    ) {
        Member actor = rq.checkAuthentication();

        Post post = postService.findById(postId).orElseThrow(
                () -> new ServiceException("404-1", "%d번 글은 존재하지 않습니다.".formatted(postId))
        );

        PostComment postComment = post.addComment(
                actor,
                reqBody.content
        );

        em.flush(); //추가함

        return new RsData<>(
                "201-1",
                "%d번 댓글이 작성되었습니다.".formatted(postComment.getId())
        );
    }

EntityManage 이란?

  • Controller에서 수행되는 트랜잭션의 영속성 컨텍스트를 관리하는 객체

  • 현재 활성화된 트랜잭션의 영속성 컨텍스트를 사용하거나, 트랜잭션이 없으면 새로운 영속성 컨텍스트를 생성

  • 하지만 대부분의 경우 서비스 계층에서 트랜잭션이 시작되므로, Controller에서 직접 EntityManager를 사용하는 것은 권장하지 않는다.

3-3. Service에서 Repository.flush() 이용

JpaRepository의 flush() 메서드를 호출하여 변경 사항을 DB에 반영하는 방식

 postService.flush();
 
 
 public void flush() {
        postRepository.flush(); // em.flush(); 와 동일
    }
 

정리

  • JpaRepository.flush()는 내부적으로 EntityManager.flush()를 호출하기 때문에 동작은 동일하지만, 추상화 수준에서 차이가 있다.
  • 일반적으로 Spring Data JPA를 사용하는 환경에서는 JpaRepository.flush()를 사용하는 것이 더 간결하고 직관적이다.
profile
조금씩 매일 성장하자

0개의 댓글