
문제상황
문제는 변경이 된 객체가 조회는 변경이 안 된 상태로 들어가는 현상이었다.
좋아요를 구현하는 로직이었는데 좋아요 요청이 들어오면 카운트를 db에 1 올리고 재차 올라간 상태를 다시 반환하는 로직이었다.
CourseDetailLike logg = CourseDetailLike.builder()
.courseDetailId(detailId)
.courseDetail(courseDetailRepository.findById(detailId).orElseThrow(()->new IllegalArgumentException("찾을수 없는 코스 입니다.")))
.member(memberRepository.findByUsername(username).orElseThrow(()->new IllegalArgumentException("찾을수 없는 유저입니다.")))
.likedAt(LocalDateTime.now())
.build();
likeRepo.save(logg);
//좋아요 카운트
courseDetailRepository.plusLikeCount(detailId);
//좋아요 조회 반환
return courseDetailRepository.findById(detailId)
.map(CourseDetail::getLikeCount)
.orElse(0);
하지만 좋아요를 눌러도 눌렀을 때 0으로 조회되고 다시 조회되면 정상적인 좋아요 숫자로 조회가 되었다.
해결방법
이 문제의 원인은 한 트랜잭션 안에서 저장 및 조회를 했다는 것이다.
내가 현재 사용하는 JPA는 1차 캐시와 2차 캐시를 사용한다.

그림에서 보듯이 JPA는 영속성 콘텍스트 데이터를 변경하면 바로 데이터베이스에 저장을 하는 게 아니라 트랜잭션 동안에 변경된 데이터를 캐시에서 반환하다가 트랜잭션이 끝나면 데이터베이스에 저장(flush)하게 된다.
즉 내가 짠 코드 로직은 변경된 데이터에서 조회를 하려 했으나 자꾸 캐시에서 조회를 했기 때문에 변경이 되지 않은 데이터를 받았던 것이다!
해결책은 여러 가지 방법이 있지만 공통적으로 캐시를 업데이트하게 한다.
1. 강제로 flush() 하기
em.flush();
em.clear();
엔티티 매니저의 flush는 원래 트랜잭션이 끝나면 이루어지지만 강제로 데이터베이스에 업데이트하게 하여 업데이트된 데이터를 받게 한다.
2. 변경 감지(Dirty Checking) 사용
courseDetail.increaseLikeCount();
// CourseDetail 엔티티 내부에 추가할 메서드
public void increaseLikeCount() {
this.likeCount++;
}
이번엔 엔티티 매니저를 사용하는 것이 아닌 직접 엔티티를 업데이트 하게 하는 것이다. JPA는 최초 조회 할시 스냅숏을 떠놓는데
사용자가 직접 repository에 업데이트할 필요 없이 트랜잭션이 끝나는 시점에 엔티티에 변경이 생겼을 경우 JPA가 알아서 업데이트 쿼리를 날린다.
즉 엔티티에 @DynamicUpdate 어노테이션만 달아놓으면 변경 감지를 JPA가 알아서 해준다!
3.@Modifying 어노테이션 활용
현재 내 리포지토리에선 단순 업데이트 어노테이션인 @Modifying을 사용 중이다
@Modifying
@Query("UPDATE CourseDetail cd SET cd.likeCount = cd.likeCount + 1 WHERE cd.id = :id")
void plusLikeCount(@Param("id")Long DetailCourseId);
여기서 @Modifying 어노테이션에 옵션을 추가할 수 있는데
@Modifying(clearAutomatically = true, flushAutomatically = true)
이런 식으로 바꾸면 쿼리를 실행하고 clear()와 flush()를 자동으로 해준다.
결론
JPA를 단순 편하게 쿼리를 날려주는 라이브러리라 생각했지만 영속성 콘텍스트와 컨테이너의 관리에 따라 여러 가지 문제가 발생할 수 있다는 걸 알았다.
단순히 쿼리만 날리는 게 아니라 JPA의 플로우를 따라가고 사용법을 익히는 게 JPA의 올바른 사용법이란 걸 명심하고 개발할 것이다.
'탐구' 카테고리의 다른 글
| 노가다에서 벗어나기: ModelMapper보다 MapStruct를 선택한 이유 (1) | 2025.12.30 |
|---|---|
| Copy on Write로 알아본 계층을 관통하는 철학 (1) | 2025.12.26 |
| CORS가 협업을 자꾸 힘들게해요 (0) | 2025.04.12 |