[Spring Data JPA] 메소드 네임쿼리 구현 및 에러 해결(javax.persistence.NoResultException: No entity found for query)
메소드 네임쿼리 직접 구현하기
로그인 기능을 구현하던 도중 에러가 발생했다. jpaRepository를 상속받아 메소드네임쿼리를 사용하면 문제없지만 연습삼아 하는 것이다 보니 이번 프로젝트에서는 직접하기로 마음먹었었는데 후회하고있다...
repository
public Optional<User> findByEmail(String email){
return Optional.ofNullable(em.createQuery("select u from User u where u.email = :email", User.class)
.setParameter("email", email).getSingleResult());
}
service
// 유저 생성 및 수정 서비스 로직
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail()) // 여기서 무한루프
.map(entity -> entity.update(attributes.getName(), attributes.getProfileImage()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
에러 발생 : javax.persistence.NoResultException: No entity found for query
네트워크를 확인해보면 로그인을 카카오 서버에 요청하여 토큰을 발급받고 인증을 하는 과정을 계속해서 무한루프를 돌고 있는 것을 확인 할 수 있다.
디버깅을 해보니 해당 코드에서 계속 리턴되어 무한 반복되고 있었다.
User user = userRepository.findByEmail(attributes.getEmail()) // 여기서 무한루프
user가 존재하지 않을 때에는 null을 반환해주는데 이를 service에서 제대로 받아오질 못하고 다시 리턴되는 것 같다.
그러면 에러 해결 방법은 두 가지이다.
- service에서 예외처리를 잘해주거나
- controller에서 예외값을 잘 보내주던가
그런데 controller에서는 null말고는 보낼 게 없다고 생각해서 servcie 로직을 뒤적뒤적거렸다. try~catch문으로 예외처리를 해주었지만 마찬가지로 또 user를 저장해주는 과정에서 무한루프가 발생했다.
// 유저 생성 및 수정 서비스 로직
private User saveOrUpdate(OAuthAttributes attributes) {
Optional user = null;
try{
user = userRepository.findByEmail(attributes.getEmail());
if(user ==null){
return userRepository.save(attributes.toEntity()); // 여기서 다시 루프 무한 반복
}
}catch (NoResultException e){
}
User updateUser = user.map(entity -> entity.update(attributes.getName(), attributes.getProfileImage()))
.orElse(null);
return userRepository.save(updateUser);
}
해결 : 메소드 네임 쿼리는 값이 null일 때, Optional.empty()를 반환
그렇게 삽질해도 안되길래 jpaRepository에서 제공하는 메소드네임쿼리를 써서 구현을 해보니깐 해결책이 나왔다. 해당 로직에서는 쿼리에 조회되는 값이 없으면 null이 아니라 Optional.empty()를 리턴해주고 있었다. null밖에 없다고 생각한 게 엄청난 착오였다.
그래서 controller단에서 예외처리를 해주었다. catch에서 empty()값을 리턴하게 되면 뷰에서 가끔 가다 스프링 기본 에러 페이지로 이동하기 때문에 무조건적으로 service단에 값을 보내주기 위해 finally문을 추가하여 해당 부분에서 return시켜줬다.
public Optional findByEmail(String email){
Optional user = null;
try{
user = Optional.ofNullable(em.createQuery("select u from User u where u.email = :email", User.class)
.setParameter("email", email).getSingleResult());
}catch (NoResultException e){
System.out.println("###" + e);
user = Optional.empty();
}finally {
return user;
}
}
에러 발생 2: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
그리고 예외처리가 들어가서 그런지 몰라도 에러가 또 발생한다. try문에 들어가면서 영속성 컨텍스트의 범위에서 벗어나는 부분이 있는 것 같다.
No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call ;
해당 부분은 Service클래스에 @Transactional처리를 해주면 해결된다.
그리고 user ==null일 때, Optional.empty()를 반환해주면 service처음 로직이었던 Optional에서 제공하는 map, orElse메소드도 잘 작동한다.
최종 해결 코드
// Service (해당 @Service가 명시된 클래스에 @Transactionl 적용)
// 유저 생성 및 수정 서비스 로직
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getProfileImage()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
// Repository
public Optional<User> findByEmail(String email){
Optional<User> user = null;
try{
user = Optional.ofNullable(em.createQuery("select u from User u where u.email = :email", User.class)
.setParameter("email", email).getSingleResult());
}catch (NoResultException e){
// System.out.println("###" + e);
user = Optional.empty();
}finally {
return user;
}
}