@Modifying vs save() 비교 정리

@Modifying이란?

Spring Data JPA에서 INSERT, UPDATE, DELETE 작업을 수행하는 커스텀 쿼리에 사용하는 어노테이션입니다.

@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateUserName(@Param("id") Long id, @Param("name") String name);

왜 save() 대신 @Modifying을 사용할까요?

1. 성능 차이

save() 방식

// 1000개 수정 시: 1000번 SELECT + 1000번 UPDATE
List<User> users = userRepository.findByStatus("ACTIVE");
users.forEach(user -> {
    user.setLastLogin(LocalDateTime.now());
    userRepository.save(user);
});

@Modifying 방식

// 1번의 UPDATE로 처리됩니다
@Modifying
@Query("UPDATE User u SET u.lastLogin = :now WHERE u.status = 'ACTIVE'")
int updateActiveUsersLastLogin(@Param("now") LocalDateTime now);

2. 메모리 효율성

save() 방식

// 모든 엔티티를 메모리에 로드합니다 (OutOfMemoryError 위험)
List<User> oldUsers = userRepository.findByCreatedAtBefore(cutoffDate);
userRepository.deleteAll(oldUsers);

@Modifying 방식

// 메모리에 엔티티를 로드하지 않고 DB에서 직접 처리합니다
@Modifying(clearAutomatically = true)
@Query("DELETE FROM User u WHERE u.createdAt < :cutoffDate")
int deleteOldUsers(@Param("cutoffDate") LocalDateTime cutoffDate);

3. 복잡한 조건 처리

@Modifying
@Query("UPDATE User u SET u.point = u.point + :bonus " +
       "WHERE u.level >= :minLevel AND u.lastLogin > :recentDate")
int giveBonusToActiveUsers(@Param("bonus") int bonus,
                          @Param("minLevel") int minLevel,
                          @Param("recentDate") LocalDateTime recentDate);

언제 무엇을 사용해야 할까요?

save() 사용 시기

  • 단일 엔티티를 수정할 때
  • 복잡한 비즈니스 로직이 필요할 때
  • 연관 관계 처리가 필요할 때
  • 엔티티 생명주기 이벤트가 필요할 때
@Service
public class UserService {
    public void updateUserProfile(Long userId, UserProfileDto dto) {
        User user = userRepository.findById(userId).orElseThrow();
        
        // 복잡한 비즈니스 로직
        user.updateProfile(dto);
        user.updateLastModified();
        
        // 연관 관계 처리
        if (dto.getNewRole() != null) {
            user.changeRole(dto.getNewRole());
        }
        
        userRepository.save(user); // 적절한 선택입니다
    }
}

@Modifying 사용 시기

  • 대량 데이터를 처리할 때
  • 성능이 중요한 배치 작업을 수행할 때
  • 단순한 값 업데이트가 필요할 때
  • 조건부 일괄 처리가 필요할 때
@Service
public class BatchService {
    @Transactional
    public void monthlyUserMaintenance() {
        userRepository.updateInactiveUsers();
        userRepository.deleteExpiredSessions();
        userRepository.resetMonthlyLimits();
    }
}

@Modifying 주요 속성

clearAutomatically

  • 쿼리 실행 후 영속성 컨텍스트를 자동으로 클리어합니다.
  • 기본값: false
  • 대량 데이터 수정 시 메모리 효율성을 높일 수 있습니다.

flushAutomatically

  • 쿼리 실행 전 영속성 컨텍스트의 변경사항을 자동으로 플러시합니다.
  • 기본값: false
  • 미처리된 변경사항이 쿼리에 반영되도록 보장합니다.
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);

성능 비교 예시

// 10,000개 사용자 포인트 초기화

// save() 방식: 10,000번의 SELECT + 10,000번의 UPDATE
List<User> users = userRepository.findAll();
users.forEach(user -> {
    user.setPoint(0);
    userRepository.save(user);
});

// @Modifying 방식: 1번의 UPDATE
@Modifying
@Query("UPDATE User u SET u.point = 0")
int resetAllUserPoints();

결론

  • save(): 엔티티 중심의 객체지향적 접근 방식으로, 복잡한 비즈니스 로직이 있을 때 적합합니다.
  • @Modifying: 성능과 메모리 효율성이 중요한 대량 데이터 처리에 적합합니다.