[Spring] JPA save의 숨겨진 기능?? 😮

AlBan·2022년 5월 27일
1

Spring

목록 보기
13/13
post-thumbnail

TL;DR, JPA의 save는 파라미터로 넘겨준 엔티티를 직접 업데이트한다.

발단

2주만의 모각코에서 JPA를 다루던 친구가 이상한 상황이 발생한다며 테스트에 성공한 코드를 보여주었다.

다음은 보여준 코드의 일부이다.

@Test
public User testUser(User user){	// 1
	// ...
	userRepository.save(user)
    doSomethingWithSeq(user.getSeq())	// 2
    return user
}

의문의 코드는 크게 2부분으로 나뉘어진다.

  1. 저장을 위한 엔티티를 넘겨준다. 이 때, 해당 엔티티는 db에 저장된 적이 없기 때문에 User 엔티티의 Primary Key인 seq 필드는 값이 정해지지 않아 null이 된다.
  2. user의 primary key인 seq필드를 이용하는 로직을 실행한다.

이상한 상황은 여기서 발생한다.

메소드가 시작할때부터 user의 seq는 null이었다. 하지만, JPA의 save 메소드 시행 이후 곧바로 seq의 값을 가져와서 다음 메서드를 실행한다.

doSomethingWithSeq()에는 당연히 null check 구문이 있었음에도 불구하고 테스트가 성공한다.
save의 리턴값을 따로 할당하지 않았기 때문에 다음 구문의 메서드를 실행하면 null로 인한 예외가 발생해야 했다. 하지만, 너무나 평온하게 테스트를 성공한다. 왜일까??🤔🤔🤔

원인 파악하기

테스트를 위해 다음과 같이 테스트 코드를 작성했다.

 	@Test
    fun signUp() {
    	val user = User(
    		seq = 0,
    		id = "test@email.com",
    		password = "test",
    		name = "test",
    		isAdmin = true,
    		createdAt = LocalDateTime.now(),
    		updatedAt = LocalDateTime.now(),
    		authorities = mutableSetOf(UserRole.USER),
    		joinGroups = mutableSetOf()
    	)
    	println("before seq : ${user.seq}")
    	repository.save(user)
    	println("after seq : ${user.seq}")
  	}

테스트 코드는 JPA의 save 메서드를 전/후로 파라미터로 전달한 user 객체가 업데이트가 되는지 확인하는 코드다.
assert 구문이 없기 때문에 테스트는 당연히 통과하고, 출력된 결과는 다음 이미지와 같다.

save 전/후로 user객체를 재할당 하는 코드는 없는데 seq의 값이 Auto Increment되어 값이 변하였다.
이를 보면 save() 메서드 내부 로직에서 객체를 직접 업데이트 하는 부분이 있을 것이라고 확신할 수 있다.

save() 메서드 디버깅

원인을 파악했으니 디버깅을 진행하면서 어떠한 방식으로 save가 진행되고 왜 user 객체의 값이 업데이트 되는지 확인을 해보자

위 이미지를 보면, ReflectiveMethodInvocation 클래스 인스턴스의 proceed()를 실행하고 나니 user 객체의 seq의 값이 변경되었다.😮😮

그러면 proceed() 메서드를 더 파보자!

ReflectiveMethodInvocation 클래스 생성자

ReflectiveMethodInvocation클래스의 인스턴스를 생성할때, user 객체는 4번째 파라미터로 할당되어 인스턴스가 생성되고, 내부에서 클래스의 arguments필드에 할당된다.

이후, 더 이상 interceptor chaininterceptor가 없을 때, 현재의 interceptor를 invoek하면서 실행해야 하는 method를 실행한다.
이 때, 실행해야 하는 method는 CRUDRepository 인터페이스 save()메서드를 실행한다.

save() 메서드를 실행하게 되면, SimpleJpaRepository 클래스의 save가 실행 되고, 이어서 entityManager의 persist()함수가 실행된다.

persist() 함수는 PersistEvent를 발행해서 객체를 DB에 저장한다.

(분명 이 뒤에 더 많은 코드가 있으나, 어떤 함수를 따라가서 분석을 해야하는지 잘 모르겠어서 찝찝하게 글을 마무리 했습니다..😭😭 계속 분석해서 새롭게 알게 된다면 업데이트 하겠습니다.)

아직까지 궁금한 점? 디버깅을 하다 보니 객체의 메모리 주소가 바뀌지 않는다. 그렇다면 setter를 이용했다는 소리가 되는데, 객체의 모든 필드는 private이라 setter가 없는데 어떻게 넘겨준 객체가 업데이트가 되었을 까?
이 또한, 더 분석을 진행하다보면 알 수 있을 것 같다.

결론

JPA의 save 함수는 엔티티를 DB에 객체를 저장하고 업데이트 된 정보를 반환도 하지만, 인자로 넘겨준 객체를 직접 업데이트 하기도 한다!

profile
[Spring, React를 공부하는 끈질긴 개발자 지망생] 잊어버리지 않도록! 정리 또 정리!

1개의 댓글

comment-user-thumbnail
2023년 5월 17일

그렇다면 setter를 이용했다는 소리가 되는데, 객체의 모든 필드는 private이라 setter가 없는데 어떻게 넘겨준 객체가 업데이트가 되었을 까?

라는 부분에 대해서는 Jpa 가 "변경 감지" 기능을 어떻게 구현했는지 (Reflection) 알아보시면 좋을거같습니다.

답글 달기