ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 영속성 컨텍스트 분리로 인한 비밀번호 수정 불가
    프로젝트/법잘알 2024. 2. 11. 18:10

    UserController

    @PreAuthorize("hasRole('ROLE_USER')")
    @PatchMapping("/change/password")
    public ResponseEntity<Void> updatePassword(
            @AuthenticationPrincipal PrincipalDetails principal,
            @RequestBody ChangePasswordRequestDto request) {
        userService.updatePassword(principal.getUser(), request.getPassword(), request.getNewPassword());
        return ResponseEntity.ok().build();
    }

     

     

    UserService

    @Transactional
    public void updatePassword(User user, String password, String newPassword) {
        if (!bCryptPasswordEncoder.matches(password, user.getPassword())) {
            throw new WrongPasswordException(password);
        }
    
        user.updatePassword(bCryptPasswordEncoder.encode(newPassword));
    }

     

    jwt를 사용해서 회원가입, 로그인을 진행하고 있는데, 위와 같이 비밀번호를 수정하려고 할 때 문제가 발생했다.

     

    비밀번호를 수정하는 과정에서 비밀번호가 변경되는 것처럼 보이지만, 트랜잭션 종료 후 update 쿼리를 날리지 않고 커밋 시점에서 다시 원래의 비밀번호로 돌아온다는 문제였다.

     

     

    @Service
    @RequiredArgsConstructor
    public class PrincipalDetailsService implements UserDetailsService {
    
        private final UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
            User userEntity = userRepository.findByEmail(email); // username 대신 email 사용
            return new PrincipalDetails(userEntity);
        }
    }

    UserDetailsService는 사용자 정보를 반환하는 spring security 인터페이스인데, 여기서 반환되어지는 user 객체는 loadUserByUsername이 호출되는 시점에서는 영속 상태지만 인증이 완료된 이후에는 영속성 컨텍스트에서 분리되어서 준영속 상태에 들어간다.

     

    이러한 이유 때문에 jpa의 변경 감지로 인한 수정이 이루어지지 않는 것이다.

     

     

     

    해결

    처음에 사용한 방법은 entityManager를 주입받아서 entityManager.merge(user)를 통해 다시 영속 상태로 만드는 방법이었다.

     

    하지만 merge를 사용한 이 방식은 쿼리를 많이 호출하였기 때문에 트랜잭션 로직 안에서 findById()를 통해 해결했다.

    @Transactional
    public void updatePassword(Long userId, String password, String newPassword) {
        User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
    
        if (!bCryptPasswordEncoder.matches(password, user.getPassword())) {
            throw new WrongPasswordException(password);
        }
    
        user.updatePassword(bCryptPasswordEncoder.encode(newPassword));
    }

     

     

    게시글 같은 경우에는 수정을 할 때 id를 받아오니까 findById를 통해 객체를 찾고, 변경하기 때문에 영속성 컨텍스트를 신경쓰지 않았지만, 사용자의 정보는 jwt를 통해서 헤더에 들어있고, 이 객체는 loadUserByUsername 로직에서는 영속 상태지만 이후에는 준영속 상태이기 때문에 user 객체를 바로 사용할 수 있더라도 영속 상태로 만들거나 findById로 호출해야 된다는 것을 알았다.

     

     

    너무 당연하게 트랜잭션은 해당 로직에서만 수행되기 때문에 user 객체를 주입받아도 사용할 수 없는 건데 '트랜잭션이 왜 안되지? 하고' 한 30분정도 헤매기만 했다...

     

    항상 당연하게 사용했던 변경 감지 기능이지만 jpa에서 가장 중요하다고 말해도 이상하지 않을 정도로 중요한 영속성 컨텍스트이기 때문에 앞으로도 제대로 알고 사용하며 이러한 실수를 하지 않아야 겠다.

Designed by Tistory.