[문제 상황]
사용자 회원가입이 잘 되는지 확인하기 위해 다음과 같은 테스트 코드를 작성했다.
"영속성 컨텍스트에 user 정보가 저장되어서 같은 영속성 컨텍스트에 있는 user와 newUser는 같은 객체가 된다"라고 생각했는데 테스트 케이스가 실패하였다. 확인해보니 user가 null로 나온다. ???
그렇다면 왜 이 테스트 케이스는 실패하는가? 이를 위해 userService.save()의 코드를 살펴보았다.
[분석]
먼저 내가 알고 있는 지식이 잘못된 것인지 확인하였다.
[JPA의 객체 비교]
객체를 비교하는 방식에는 총 2가지가 있다.
1. 동일성 비교 (== 비교)
2. 동등성 비교 (equals())
JPA에서는 같은 트랜잭션일 때는 같은 객체임이 보장된다. 따라서 다음은 동일성 비교에서 true가 나온다.
Long userId = "1";
User user = userRepository.findById(userId);
User user2 = userRepository.findById(userId);
현재 문제 상황에서는 view -> service -> repo -> service -> view 순으로 로직이 실행된다. 그러면 트랜잭션은 언제 어떻게 생기고 살아지는지 살펴보자.
[영속성 컨텍스와 트랜잭션 생명주기]
스프링의 OSIV(Open Session In View)는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이다. 즉, 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성하고 서비스, 또는 레포지토리에서 로직을 수행할 때 영속성 컨텍스트의 트랜잭션을 시작한다.
트랜잭션의 범위는 서비스 또는 레포지토리의 작업이 끝난 후 사라지며 영속성 컨텍스트는 클라이언트의 요청을 처리할 때까지 살아있다. 따라서 트랜잭션이 끝났음에도 영속성 컨텍스트의 객체 조회는 가능하다.
[문제의 원인]
코드를 다시 보자. Test에서 넘겨준 user는 분명히 영속성 컨텍스트에 있다. 매개변수로 넘어온 user는 point가 없기 때문에 builder()를 이용해 User를 다시 생성한다. 여기에 문제가 있었다. 이를 눈으로 확인하기 위해 디버깅을 찍어보았다.
1. 넘어온 유저는 11784라는 주소를 가지고 있다.
2. 새로 생성된 객체는 11957번 주소이며 이를 리턴한다.
3. 결국 영속성 컨텍스트에 있는 user와 save()로 리턴받은 newUser는 다른 객체가 되어버린다.
[결론]
- 클라이언트의 요청이 들어오면 영속성 컨텍스트가 생성되고, 서비스 또는 레포지토리의 로직이 수행될 때 트랜잭션이 시작된다.
- 영속성 컨텍스트는 클라이언트의 요청이 끝날 때까지 살아있기 때문에 객체의 조회는 가능하다.
- 같은 트랜잭션에서 Id(pk)가 같은 로우를 조회하면, 객체 동일성(==)을 보장해준다. (동일성, equals(), DB Id 모두 같다)
다음과 같이 User엔티티 클래스에 initPoint(), initReward() 메소드를 만들어서 point와 reward를 셋팅해주는 방법으로 바꾸었다.