Spring을 공부하면서 간단한 CRUD 프로젝트를 진행하고 있었는데, 게시글 업데이트 기능이 제대로 작동하지 않는 문제가 발생하였다.
게시물의 수정 사항이 DB에 전혀 반영되지 않았다.
문제가 발생했던 코드는 다음과 같다.
@PostMapping("post/update/{id}") // PostController 클래스의 메서드
public String updatePost(Model model, @PathVariable Long id, PostFormDto postUpdateDto){
Post target = postService.findById(id).orElseThrow(() -> new IllegalStateException("postId not found"));
target.postUpdate(postUpdateDto);
return "redirect:/post/list";
}
정말 기초적인 실수였는데, 아직 Spring과 JPA를 막 공부하기 시작한 참이라 JPA의 동작 원리를 제대로 이해하지 못해서 발생했던 실수였다.
위 코드에서 실제 Entity 오브젝트의 값을 변경하는 부분은 target.postUpdate(postUpdate)
부분인데, 이 부분의 코드를 살펴보면 아래와 같다.
@Getter
@NoArgsConstructor
@Entity
public class Post extends BaseTimeEntity {
...
public void postUpdate(PostFormDto postDto){
this.title = postDto.getTitle();
this.content = postDto.getContent();
}
}
target.postUpdate(postUpdate)
은 단순히 객체의 필드만 수정하는 메서드이고, 별도의 @Transactional
어노테이션이 붙어있지 않기 때문에 위 메서드는 스프링에서 트랜잭션을 통해 DB의 데이터를 변경하는 메서드임을 알 수 없다. 따라서 JPA의 변경 감지가 동작하지 않게 되고, 변경사항이 반영되지 않는 것이다.
DB는 기본적으로 트랜잭션 단위로 작업을 수행한다.
우리가 보통 DB에서 값을 조회하거나 수정할 때에는 다음과 같은 과정을 거치게 된다.
- 트랜잭션 시작
- 트랜잭션으로 묶여있는 각종 SQL 쿼리를 수행
- 트랜잭션 종료(커밋)
2번 과정에서 발생한 DB의 변경사항은 트랜잭션이 정상적으로 종료되어 커밋되는 시점에 DB에 비로소 반영되게 된다.
JPA는 2번 과정에서 수행하는 SQL 쿼리를 직접 작성하는 대신 Entity의 객체의 변경 사항을 자동으로 감지하고 해당 변경 사항에 알맞는 SQL 쿼리를 자동으로 DB에 날려준다.
JPA를 통해 Entity 객체의 변경사항을 DB에 반영하는 과정은 다음과 같다.
- 트랜잭션 시작
- JPA 영속성 컨텍스트에서 Entity 객체를 검색
- 찾은 Entity 객체의 변경 사항을 감지하고, 영속성 컨텍스트에 해당 변경 사항을 반영한다.
- 트랜잭션 종료(커밋)
위 과정에서 4. 트랜잭션 종료(커밋)
되는 시점에 영속성 컨텍스트의 변경 사항을 DB에 반영시킨다.
조금 더 설명을 덧붙이자면, 트랜잭션이 커밋되는 시점에 EntityManager
에서 flush()
라는 메서드를 실행시키는데, 이 flush()
라는 메서드가 DB에 변경 사항을 반영해주는 역할을 한다.
JPA에서 Entity 객체의 변경 사항을 감지하고 이를 DB에 반영하기 위한 조건은 다음과 같다.
- Entity 객체의 데이터를 변경하는 로직이 트랜잭션으로 묶여 있어야 한다.
- 트랜잭션이 정상적으로 커밋 되어야 한다.
- 변경하려는 Entity 객체가 영속상태여야 한다.
내 경우에는 근본적으로 1번 조건을 만족하지 않아서 변경사항이 반영되지 않았던 것이다.
(물론, 트랜잭션을 애초에 시작하지도 않았으니, 2번 역시도 원인이라고 할 수 있겠다.)
3번 조건의 경우, post를 처음 생성해 DB에 저장하는 시점이 em.persist(post)
를 실행하므로 만족하는 상황이었다.
@PostMapping("post/update/{id}") // PostController 클래스의 메서드
public String updatePost(Model model, @PathVariable Long id, PostFormDto postUpdateDto){
Post target = postService.findById(id).orElseThrow(() -> new IllegalStateException("postId not found"));
postService.postUpdate(target.getId(), postUpdateDto); // 수정된 부분
return "redirect:/post/list";
}
해결 방법은 굉장히 간단하다. 주입받은 PostService 객체를 이용하여 update를 수행하면 되는 것이다.
@Service
public class PostService {
...
@Transactional
public Post postUpdate(Long postId, PostFormDto postDto) {
Post targetPost = postRepository.findById(postId)
.orElseThrow(() -> new IllegalStateException("postId not found"));
targetPost.postUpdate(postDto);
return targetPost;
}
...
}
PostService
클래스의 postUpdate
메서드에는 @Transactional
어노테이션이 붙어있으므로, 위 메서드에서 DB 조작과 관련된 모든 코드는 하나의 트랜잭션으로 묶여서 처리하게 된다.
postService
를 사용해야 하는 것을 깜빡하고, 그냥 Entity 객체의 값만 수정해 발생한 오류였다.
아무래도 Post 클래스의 업데이트 메서드와 PostService 클래스의 업데이트 메서드의 이름이 동일해서 이런 실수를 한 것 같은데, 다음부터는 메서드 이름을 조금 더 신중하게 짓도록 해야겠다.