프로젝트 진행 중 JPA 코드를 작성하다 예상치 못한 오류를 마주쳤습니다.
Prerequisite: Spring Data JPA의 Repository는 naming 규칙에 따라 query가 자동 생성됨.
(참고자료: Spring Data JPA naming 규칙.)
이를 통해 다대다 맵핑 (User-Challenge)의 관계에서 user, challenge의 정보를 통해 Entity를 삭제하려고 했습니다.
ex) deleteByUserAndChallenge -> user와 challenge의 entity 정보를 받아서 query 작성.
이 과정에서 error가 발생하였고, 다양한 방법으로 접근 하면서 알게 된 개념을 정리하고 싶었습니다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call;
다음과 같은 error를 접했는데, remove를 call 할 수 없다는 것이었다.
먼저 현재 Domain은 다음과 같습니다.
public class UserChallenge {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
@ManyToOne
@JoinColumn(name = "CHALLENGE_ID")
private Challenge challenge;
}
User, Challenge를 다대다 맵핑을 하기 위해 다음과 같이 새로운 Domain에서 ManyToOne으로 관계 맵핑을 한 상태였습니다.
save는 정상적으로 되는데 findByUserAndChallenge를 하면 위의 error를 뱉기 때문에, PK를 가져오는 부분에서 잘못된 건가 싶어서 Domain의 기본키를 바꿔서 실행했습니다.
@IdClass(UserChallengePK.class)
public class UserChallenge {
@Id
@ManyToOne
@JoinColumn(name = "ID")
private User user;
@Id
@ManyToOne
@JoinColumn(name = "CHALLENGE_ID")
private Challenge challenge;
}
다음과 같이 Domain을 기본키를 변경한 다음,
public class UserChallengePK implements Serializable {
private Integer user;
private Long challenge;
}
public interface UserChallengeRepository extends JpaRepository<UserChallenge, UserChallengePK> {
UserChallenge save(UserChallenge userChallenge);
void deleteByUserAndChallenge(User user, Challenge challenge);
}
기본키에 대한 정보를 입력하고 실행하였지만, 결과는 같았습니다.
deleteBy~의 동작에서 PK가 아닌 값을 통해 지울 때 문제가 된다는 가정은 틀렸습니다.
그럼 뒤에 선언한 User, Challenge의 인자가 잘못된 것인지, deleteById와 deleteBy~의 동작방식에 대해 찾아보기로 했습니다.
먼저 JPA Repository의 delete와 deleteById의 코드를 보면,
delete의 경우 영속성 context를 entityManager의 remove함수를 통해 지우는 것을 확인할 수 있고,
deleteById는 findById로 영속성 context를 가져온 다음, delete를 호출하는 것을 확인할 수 있었습니다.
또한, JPA의 method를 통한 query 생성 과정은
stackoverflow에 잘 설명되어있었습니다. (링크)
Proxy 패턴을 이용하고, MethodInterceptor의 기능을 이용하여 PartTree에 따라 method를 생성합니다. 즉, query 생성은 findBy~ 와 deleteBy~방식이 다르지 않기 때문에 한가지 방법으로 실험을 해봤습니다.
다음과 같이 프로그램을 작성한다음 결과를 확인해보니 성공적으로 지워지는 것을 확인 할 수 있었습니다.
여기서 오류 메세지를 다시 보고, delete 내부 라이브러리를 확인하다 보니 놓친 점이 있었습니다.
"No EntityManager with actual transaction available for current thread"
thread와 관련된 오류라는 것을 뒤늦게 확인했고, delete의 함수를 확인해보니
@Transactional 어노테이션을 확인할 수 있었습니다.
JPA에서 기본적으로 transaction을 제공한다고 생각했기 때문에, 두 개 이상의 DB가 아니면 transaction을 적용하지 않아도 된다고 생각하고 넘어갔던게 실수였습니다.
@Transactional
public void leaveChallenge(Long id, User user){
Challenge findChallenge = isExistChallenge(id);
userChallengeRepository.deleteByUserAndChallenge(user, findChallenge);
}
위의 Service의 함수에 Transaction을 적용하니 에러가 해결되었습니다.
DB에 접근할 때 읽기는 transaction에 크게 영향을 받지 않지만, delete와 insert같은 DB에 직접적인 변화를 주는 행동은 lock을 걸거나 commit에 따라 rollback을 한다든지, transaction에 민감하다는 것을 다시 확인할 수 있었습니다.
+ 추가 할 내용이나 부족한 부분이 있다면, 댓글 작성 부탁드립니다! :)