fix: 비밀번호 변경이 실제로 저장되지 않던 버그 (detached 엔티티)

- change-password가 200을 반환하면서도 DB가 안 바뀌었음
- 원인: requireUser()가 트랜잭션 밖에서 조회한 detached User의 필드를 바꿔
  JPA 더티 체킹이 작동하지 않음 (managed 엔티티에만 작동)
- 해결: changePassword 트랜잭션 안에서 findById로 재조회 → managed 상태로 UPDATE
- 학습 포인트 주석 강화: '더티 체킹이 자동 저장'의 숨은 전제(트랜잭션이 관리하는 엔티티)

검증: 변경 200 → 새 비번 로그인 200 → 옛 비번 401. 틀린 현재비번 401.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AWESOMEDEV 2026-07-17 09:13:30 +09:00
parent ecb1298d93
commit 5e8f96719b

View File

@ -152,14 +152,31 @@ public class AuthService {
* 비밀번호 변경·결제·탈퇴처럼 되돌리기 어려운 동작 앞에 세우는 관문이다.
*/
@Transactional
public void changePassword(User user, String currentRawPassword, String newRawPassword) {
public void changePassword(User loginUser, String currentRawPassword, String newRawPassword) {
// 실제로 났던 버그 여기서 배우는 "managed vs detached 엔티티".
//
// 원래 코드는 넘어온 loginUser의 필드를 바로 바꿨다:
// loginUser.changePassword(...) // 이러면 "성공"인데 DB가 바뀐다!
//
// ? loginUser는 requireUser() 트랜잭션 밖에서 조회한 "분리된(detached)" 객체다.
// JPA의 변경 감지(더티 체킹) "지금 이 트랜잭션이 관리하는(managed)" 엔티티에만 작동한다.
// 분리된 객체의 필드를 바꿔봐야 JPA는 알아채지 못해 UPDATE가 나가지 않는다.
//
// 해결: 트랜잭션 안에서 다시 조회한다. 그러면 managed 상태가 되어 더티 체킹이 작동한다.
// (멘토 재설정 resetPasswordByMentor는 findById를 트랜잭션 안에서 불러서 처음부터 정상이었다
// 차이가 버그의 힌트였다.)
//
// 교훈: "더티 체킹이 자동으로 저장해 준다"에는 숨은 전제가 있다 "지금 트랜잭션이 관리하는 엔티티라면".
User user = userRepository.findById(loginUser.getId())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."));
if (!passwordEncoder.matches(currentRawPassword, user.getPasswordHash())) {
throw new ResponseStatusException(
HttpStatus.UNAUTHORIZED, "현재 비밀번호가 올바르지 않습니다.");
}
user.changePassword(passwordEncoder.encode(newRawPassword));
// 학습 포인트: save() 부르지 않아도 된다. @Transactional 안에서 조회한 엔티티는
// JPA가 변경을 감지해(더티 체킹) 트랜잭션 커밋 시점에 자동으로 UPDATE 한다.
// 이제 user는 managed 상태라, 커밋 시점에 JPA가 변경을 감지해 UPDATE 한다.
}
/**