spring / / 2023. 9. 5. 15:13

JPA에서 @Modifying의 clearAutomatically가 왜 필요한가?

일반적으로 복잡한 로직을 작성할 때 @Transactional 안에서 여러 개의 로직을 구현하는 경우가 많이 있다.

Spring Data Jpa를 사용하여 해당 메소드 내에서 find, modify를 사용하는 경우 기본적으로 영속성 컨텍스트에서 관리를 해주는데 JPQL로 작성된 SQL이 있는 경우 쿼리 결과가 영속성 컨텍스트에 갱신되지 않아서 실제 DB와 메모리 상의 값이 다른 경우가 있다.

이런 케이스에 대해 예제를 만들어보고 어떻게 처리할 수 있는지 알아보자.

시나리오

  1. 사용자 등록
  2. 특정 서비스 메소드를 작성하고 사용자의 이름을 변경
  3. 해당 서비스 메소드 내에서 사용자 이름을 조회해보자.

JpaRepository의 save 이용시

소스 코드

@Transactional
public void modify(User user) {
    User oldUser = find(user.getUserId());
    log.info("old user: " + oldUser.getName());
    userJpaStore.update(user);
    User newUser = find(user.getUserId());
    log.info("new user: " + newUser.getName());
}

public User find(int userId) {
    return userJpaStore.findById(userId);
}
@Repository
@RequiredArgsConstructor
public class UserJpaStore {

    private final UserRepository userRepository;

    public User findById(int userId) {
        return userRepository.findById(userId).map(UserJpo::toDomain)
            .orElseThrow(() -> new IllegalArgumentException("user not found"));
    }

    public void update(User user) {
        userRepository.save(new UserJpo(user));
    }
}

실행

user의 이름을 홍길동 -> 홍길동2로 update해보자.

실행결과

조회를 해보면 new user가 홍길동2로 잘 조회되는 것을 확인할 수 있다.

2023-09-05 14:33:56.560  INFO 24525 --- [nio-8080-exec-5] com.example.service.UserService          : old user: 홍길동
2023-09-05 14:33:56.561  INFO 24525 --- [nio-8080-exec-5] com.example.service.UserService          : new user: 홍길동2

JPQL 사용

소스코드

JPQL을 이용하여 사용자 이름을 저장해보자. 아래에서 updateUser를 통해 저장한다.

public interface UserRepository extends JpaRepository<UserJpo, Integer> {
    @Transactional
    @Modifying
    @Query("UPDATE UserJpo t "
        + "SET t.name = :name "
        + "WHERE t.userId = :userId ")
    void updateUser(
        @Param("userId") int userId,
        @Param("name") String name
    );
}
@Transactional
public void modify(User user) {
    User oldUser = find(user.getUserId());
    log.info("old user: " + oldUser.getName());
    userJpaStore.updateByQuery(user);

    // 변경된 값을 find로 다시 조회
    User newUser = find(user.getUserId());
    log.info("new user: " + newUser.getName());
}

실행

user의 이름을 홍길동 -> 홍길동2로 update해보자.

실행결과

조회를 해보면 new user가 홍길동로 조회가 된다. JpaRepository의 save를 통해 저장한 경우 잘 조회가 되었지만 JPQL에서는 정상적으로 조회가 되지 않는다.

2023-09-05 14:38:32.520  INFO 24633 --- [nio-8080-exec-5] com.example.service.UserService          : old user: 홍길동
2023-09-05 14:38:32.529  INFO 24633 --- [nio-8080-exec-5] com.example.service.UserService          : new user: 홍길동

원인 분석

왜 그런지 소스코드를 분석해봤다.

spring-data-jpa의 SimpleJpaRepository 소스코드를 열어서 보니 findById는 EntityManager에서 값을 가져온다.

@Override
public Optional<T> findById(ID id) {
    ...

  return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

JpaRepository를 통해 수정한 경우는 EntityManager에 merge를 통해 값을 변경시키는데 JPQL을 사용하는 경우 EntityManager를 통하지 않고 직접 DB를 update한다. 그래서 조회할 시점에서 DB값과 EntityManager 값의 불일치가 생긴것이다.

@Transactional
@Override
public <S extends T> S save(S entity) {

  Assert.notNull(entity, "Entity must not be null.");

  if (entityInformation.isNew(entity)) {
    em.persist(entity);
    return entity;
  } else {
    return em.merge(entity);
  }
}

즉, 수정 로직은 아래와 같다.

@Transactional
public void modify(User user) {
    User oldUser = find(user.getUserId()); // 영속성 컨텍스트에 User를 저장 (홍길동)
    log.info("old user: " + oldUser.getName());
    userJpaStore.updateByQuery(user);  // JPQL로 DB를 직접 업데이트

    // 변경된 값을 find로 다시 조회
    User newUser = find(user.getUserId()); // 영속성 컨텍스트에는 여전히 '홍길동'으로 저장
    log.info("new user: " + newUser.getName());
}

JpaRepository를 사용했을 경우에는 userJpaStore.update(user);에서 영속성 컨텍스트에 값을 변경하기 때문에 find시 영속성 컨텍스트를 조회하면 '홍길동2'로 조회가 되었다.

해결방안

JPQL로 실행 시 영속성 컨텍스트와 값이 일치하지 않는 문제를 해결하기 위해 JPQL 실행 후 영속성 컨텍스트 값을 지워주는 방법이 있다.

이 방법이 @Modifying(clearAutomatically = true) 이다. (기본값: false)

기존 JPQL로 작성된 코드에서 clearAutomatically = true를 붙여주고 다시 실행해보자.

@Transactional
@Modifying(clearAutomatically = true)
@Query("UPDATE UserJpo t "
    + "SET t.name = :name "
    + "WHERE t.userId = :userId ")
void updateUser(
    @Param("userId") int userId,
    @Param("name") String name
);

실행결과

2023-09-05 15:03:49.503  INFO 25860 --- [nio-8080-exec-3] com.example.service.UserService          : old user: 홍길동
2023-09-05 15:03:49.511  INFO 25860 --- [nio-8080-exec-3] com.example.service.UserService          : new user: 홍길동2

실제 내부적으로 SQL이 어떻게 실행되는지 확인하기 위해 hibernate 쿼리를 찍어보자.

application.yml에 아래 부분을 추가한다.

spring:
  jpa.show-sql: true

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: trace

1번: @Modifying을 사용한 경우

2023-09-05 15:06:36.099 DEBUG 25937 --- [nio-8080-exec-5] org.hibernate.SQL                        : select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
Hibernate: select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
2023-09-05 15:06:36.100  INFO 25937 --- [nio-8080-exec-5] com.example.service.UserService          : old user: 홍길동
2023-09-05 15:06:36.106 DEBUG 25937 --- [nio-8080-exec-5] org.hibernate.SQL                        : update user_jpo set name=? where user_id=?
Hibernate: update user_jpo set name=? where user_id=?
2023-09-05 15:06:36.108  INFO 25937 --- [nio-8080-exec-5] com.example.service.UserService          : new user: 홍길동

2번: @Modifying(clearAutomatically = true)을 사용한 경우

2023-09-05 15:08:03.493 DEBUG 25978 --- [nio-8080-exec-4] org.hibernate.SQL                        : select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
Hibernate: select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
2023-09-05 15:08:03.494  INFO 25978 --- [nio-8080-exec-4] com.example.service.UserService          : old user: 홍길동
2023-09-05 15:08:03.500 DEBUG 25978 --- [nio-8080-exec-4] org.hibernate.SQL                        : update user_jpo set name=? where user_id=?
Hibernate: update user_jpo set name=? where user_id=?
2023-09-05 15:08:03.502 DEBUG 25978 --- [nio-8080-exec-4] org.hibernate.SQL                        : select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
Hibernate: select userjpo0_.user_id as user_id1_0_0_, userjpo0_.name as name2_0_0_ from user_jpo userjpo0_ where userjpo0_.user_id=?
2023-09-05 15:08:03.503  INFO 25978 --- [nio-8080-exec-4] com.example.service.UserService          : new user: 홍길동2

2번 방법으로 사용했을 경우 select를 한번 더 하는 것을 알 수 있다. 영속성 컨텍스트에 값이 없으므로 DB를 한번 더 조회를 하게 된다.

이렇듯 동일 트랜잭션에서 JPQL을 사용하여 값을 변경하고 조회하는 경우에 clearAutomatically를 통해 예상한 결과를 조회할 수 있다는 것을 알 수 있다.

clearAutomatically를 사용하는 경우 해당 객체의 값만 영속성 컨텍스트에서 삭제될 것으로 예상이 된다. 하지만 실제로는 영속성 컨텍스트 내의 모든 객체를 삭제한다. 그래서 여러 개의 값을 업데이트 하는 경우는 flushAutomatically = true도 같이 넣어줘야 한다. 그렇지 않고 clearAutomatically만 하게 되면 기존에 update된 값이 모두 초기화될 가능성이 있다.

즉, 아래와 같이 사용해야 한다.

@Modifying(clearAutomatically = true, flushAutomatically = true)

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유