| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 다짐글
- 회고
- 글쓰기세미나
- 레벨1
- Level2
- 클라우드아키텍처
- 헥사고날 아키텍처
- constructor
- QueryDSL
- static-factory-method
- 3계층 아키텍처
- 글또10기
- 코딩테스트
- OpenSearch
- HashMap
- 클린 아키텍처
- 프로그래머스
- React
- SpringBoot
- 포트앤어댑터 아키텍처
- 글또
- DevOps
- 가용영역
- object-creation
- UserLand
- axios
- ReverseNested
- design-pattern
- builder-pattern
- 코엑스그랜드볼룸
- Today
- Total
oguri's garage
Spring Data JPA Native Query에서 만난 InvalidDataAccessApiUsageException 본문
Spring Data JPA Native Query에서 만난 InvalidDataAccessApiUsageException
oguri 2025. 10. 21. 23:30문제 상황
사용자 정보를 upsert하는 기능을 구현하던 중, 특정 메소드에서 계속 에러가 발생했다.
InvalidDataAccessApiUsageException: Executing an update/delete query
처음에는 쿼리 문법 문제인가 싶어서 SQL을 여러 번 확인했지만, 쿼리 자체는 문제가 없었다.
DB에서 직접 실행하면 정상적으로 동작했기 때문이다.
초기 코드
문제가 발생한 코드는 다음과 같은 구조였다.
@Service
public class UserService {
public void updateUserInfo(UserDTO dto) {
updateEmail(dto);
updatePhone(dto); // 여기서 에러 발생
}
@Transactional
private void updateEmail(UserDTO dto) {
userRepository.updateEmail(dto.getId(), dto.getEmail());
}
@Transactional
private void updatePhone(UserDTO dto) {
userRepository.updatePhone(dto.getId(), dto.getPhone()); // 에러 발생 지점
}
}
Repository 인터페이스는 이렇게 정의되어 있었다.
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query(value = "UPDATE users SET phone = ?2 WHERE id = ?1",
nativeQuery = true)
void updatePhone(Long id, String phone);
}
원인 분석
에러 메시지를 다시 찬찬히 읽어보니, Spring Data JPA가 이 쿼리를 일반 SELECT 쿼리처럼 처리하려고 한다는 것을 알 수 있었다.
Native Query는 기본적으로 SELECT로 인식되기 때문에, @Modifying 어노테이션만으로는 이것이 수정 쿼리라는 것을 완전히 알려주지 못한 것이다.
더 큰 문제는 트랜잭션 관리였다.
상위 메소드인 updateUserInfo에는 @Transactional이 없었고, 하위 메소드들에만 개별적으로 붙어 있었다.
이렇게 되면 각 메소드가 독립적인 트랜잭션으로 실행되면서, 트랜잭션 컨텍스트가 제대로 설정되지 않는 문제가 발생한다.
해결 방법
두 가지를 수정했다.
1. @Modifying 어노테이션 옵션 추가
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "UPDATE users SET phone = ?2 WHERE id = ?1",
nativeQuery = true)
void updatePhone(Long id, String phone);
}
clearAutomatically = true: 쿼리 실행 후 영속성 컨텍스트를 자동으로 클리어한다flushAutomatically = true: 쿼리 실행 전 자동으로 flush한다
2. 트랜잭션 위치 변경
@Service
public class UserService {
@Transactional
public void updateUserInfo(UserDTO dto) {
updateEmail(dto);
updatePhone(dto);
}
private void updateEmail(UserDTO dto) {
userRepository.updateEmail(dto.getId(), dto.getEmail());
}
private void updatePhone(UserDTO dto) {
userRepository.updatePhone(dto.getId(), dto.getPhone());
}
}
상위 메소드에 @Transactional을 추가하고, 하위 private 메소드들의 @Transactional은 제거했다.
이렇게 하면 전체 작업이 하나의 트랜잭션 안에서 실행되고, 중첩 트랜잭션으로 인한 복잡도도 줄어든다.
배운 점
Native Query로 INSERT/UPDATE를 실행할 때는 단순히 @Modifying만 붙이는 것으로는 부족하다.
트랜잭션 컨텍스트가 제대로 설정되어야 하고, 영속성 컨텍스트 관리를 위한 옵션도 명시적으로 지정해주는 것이 안전하다.
또한 트랜잭션 경계를 어디에 설정할지도 중요하다.
여러 Repository 호출이 논리적으로 하나의 작업 단위라면, 상위 Service 메소드에 트랜잭션을 설정하는 것이 더 명확한 구조가 된다.
참고 자료
'개발하다 > Spring' 카테고리의 다른 글
| JPA Entity에서 Lombok @EqualsAndHashCode 사용 시 주의사항 (0) | 2025.10.24 |
|---|---|
| Spring DTO 설계 시 기본 생성자만 두는 이유 (0) | 2025.10.22 |
| DB부터 시작하는 JPA Entity 매핑 정리 (0) | 2025.10.20 |
| JPA에서 DISTINCT가 필요한 경우와 필요하지 않은 경우 (0) | 2025.10.18 |
| Spring에서 실행 시간 측정하기 (0) | 2025.10.17 |