JPA에서 transaction, AOP로 더티체킹이 되지 않는 이유
보통 아래처럼 @Transactional이 선언되어있는 메서드를 구현해 두고 호출하면 더티체킹이 적용이 된다.
@Transactional
public void update(Long id){
Member m = repo.findById(id).get();
m.setName("update1");
}
그런데 그냥 아무 이유 없이 궁금한게 생겼다. 다른 @Transactional 이 없는 메서드에서 호출을 하면 어떻게 될까?
아래 처럼 코드를 구성하고 테스트를 진행해봤다.
//=== 엔티티 ===
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@NotEmpty
private String name;
@Embedded
private Address address;
}
//=== 컨트롤러 ===
@GetMapping("/test")
public ResponseEntity<String> test(){
service.process();
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
//=== 서비스 ===
public void process(){
Member m = createMember("target", "target", "9", "9999");
Member result = repo.save(m);
update(result.getId());
}
@Transactional
public void update(Long id){
Member m = repo.findById(id).get();
m.setName("update1");
}
실행전에는 그래도 더티체킹이 되겠지~ 라고 생각했다.
그러나 생각과는 다르게 대차게 실패했다.(니가 그렇지 뭐~)
process() 메서드를 호출시에는 Member 의 더티체킹이 되지 않으며 update() 메서드만 호출 할 시는 더티체킹이 된다.
영속성 컨텍스트의 라이프싸이클은 기본적으로 트랜잭션 범위로 알고있다.
아니 update 내에서는 트랜잭션을 보장해주잖아?!?! 그런데 왜 안돼 ㅁㅇㄻㅇㄹ,ㅇ륄;ㅁㄷㅈ!!!!!!!!!!!
접근
가설1. 준영속 상태가 영속 상태로 다시 등록되면서 나중에 영속성 컨텍스트에 등록되야 할 다음 인스턴스가 무시된건가?
process() 메서드에서 Jpa save로 엔티티를 DB 저장과 함께 영속성 컨텍스트에 등록한다.
그 후에는Jpa save로 등록됐던 영속성 컨텍스트의 엔티티는 save 가 끝남과 동시에 transaction이 종료되기 때문에 준영속 상태가 된다.(준영속 상태가 되는 이유는 Spring 에서는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용하기 때문이다.)
그리고 나서 update() 메서드를 호출한다.
update() 메서드 에서는 Jpa findById로 데이터를 가져오게 되는데 영속성 컨텍스트를 먼저 찾게되고 없을시에 DB에서 직접 찾게 된다. DB 찾은 값은 영속성 컨텍스트에 다시 저장한다.
이 과정 혹시 findById가 준영속 상태의 엔티티를 다시 등록해주는건 아닐까? 라는 생각이 들었다.
그렇게 되면 기존 준영속 엔티티가 영속성 컨텍스트에 추가될것이고 findById로 찾은 객체 인스턴스에는 m.setName("update1");` 으로 변경값이 적용되겠지만 해당 객체는 영속성 컨텍스트에 등록되지 않은것이고 실제 등록된것은 process()에서 호출했던 그 Member 객체가 있을 것이라고 판단했다.
그래서 구현체와 관련 자료들을 찾아봤다.
보통 준영속 상태를 다시 등록하기 위해선 merge() 메서드를 사용할텐데 findById의 실제 구현체에서는 존재하지 않았으며, 관련 자료들에서도 준영속 상태를 영속 상태로 등록 시켜주는것은 없었다.
가설2.update() 메서드에서 찾은 영속성 컨텍스트의 엔티티 객체는 사실 프록시 객체이다?
@Transactional
public void update(Long id){
Member m = repo.findById(id).get();
m.setName("update1");
}
아니다. 실제 영속성 컨텍스트에서 가져온 객체이다.(프록시라면 디버깅시 Proxy@Member 이런 주소여야한다.)
그리고 심지어 Member 객체에 변경값 update1 적용된것도 확인할 수 있다.
(해결 및 이유)가설3. 1. 호출하는 상위 메서드에서 트랜잭션 실행하기, 2. 하나의 트랜잭션 안에서 process()+update() 로직 합치기
위의 2가지 상황을 만들어 봤다.
1.호출하는 상위 메서드에서 트랜잭션 실행하기
@Transactional
public void process(){
Member m = createMember("target", "target", "9", "9999");
Member result = repo.save(m);
update(result.getId());
}
public void update(Long id){
Member m = repo.findById(id).get();
m.setName("update1");
}
2.중첩 트랜잭션 적용
@Transactional
public void process(){
Member m = createMember("target", "target", "9", "9999");
Member result = repo.save(m);
update(result.getId());
}
//@Transactional
public void update(Long id){
Member m = repo.findById(id).get();
m.setName("update1");
}
3.하나의 트랜잭션 안에서 process()+update() 로직 합치기
@Transactional
public void processV2(){
Member m = createMember("target", "target", "9", "9999");
Member result = repo.save(m);
Member m1 = repo.findById(result.getId()).get();
m.setName("update1");
}
1,2,3번 방법 모두 더티체킹이 된다. 그런데 공통점이 있다.
모두 호출하는 상위메서드에서 트랜잭션을 적용하면 더티체킹이 된다는 점이다.
잠깐.... 스프링이 트랜잭션을 처리하는거야? 를 찾기 시작했다.
Spring에는 기본적으로 트랜잭션을 Proxy를 통해서 처리한다.
그런데 프록시에서의 트랜잭션은 객체 외부에서 처음 진입하는 메서드를 기준으로 동작한다는 점 이다.
(디버깅을 통해 첫 진입 메서드인 process 부터 프록시, 트랜잭션 동작을 확인 했다.)
만약 첫 진입 메서드에 @Transactional이 없다면 트랜잭션이 적용되지 않으며 이 말인 즉슨
하위메서드에서 트랜잭션을 사용중이더라고 적용되지 않는 말과 같다.
상위 메서드에 트랜잭션이 없는 경우 프록시는 해당 메서드를 호출할 때 트랜잭션을 시작하거나 관리하지 않는다. 이로 인해 영속성 컨텍스트의 상태 변화를 추적하지 않고, 상위 메서드와 하위 메서드 간에 트랜잭션 경계가 분리된다.
상위 메서드와 하위 메서드 간에 트랜잭션 경계가 분리되어 있으므로 더티 체킹은 상위 메서드에서는 수행되지 않는다.
결론
상위 메서드가 트랜잭션을 시작하지 않으면 하위 메서드에 대한 호출은 트랜잭션 컨텍스트 내에서 수행되지 않는다. 따라서 하위 메서드에 존재하는 트랜잭션 어노테이션은 무시된다.
'SPRING > JPA' 카테고리의 다른 글
[JPA] @Transactional(readOnly = true) 은 왜 사용해요? (0) | 2024.03.14 |
---|---|
JPA 엔티티 이름대문자로 설정하기 (0) | 2022.05.31 |
웹 애플리케이션과 영속성 관리 (0) | 2022.03.30 |
객체지향 쿼리언어 (0) | 2022.03.26 |
jpa metamodel must not be empty! 테스트 에러 (0) | 2022.03.24 |