트랜잭션의 범위와 영속성 컨텍스트의 범위는 별개
짧게 결과만 말하자면 위와 같다.
지금까지 트랜잭션의 범위 == 영속성 컨텍스트의 범위라고 알고 사용해서 종종 느껴지는 이게 왜 되는건지에 대한 문제가 OSIV 때문이였다. OSIV 설명 전에 엔티티의 생명주기 먼저 복습하고 간다.
엔티티의 생명주기
비영속 transient
객체를 생성하고 영속성 컨텍스트가 관리는 안하는 상태
보통의 경우 엔티티 식별자(id)를 DB에 맡기기 때문에 아직 식별자가 존재하지 않는다.
영속 managed
영속성 컨텍스트에 저장된 상태
비영속 엔티티를 persist 하거나 DB의 엔티티를 find 시 영속성 컨텍스트에 영속된다.
준영속 detached
영속성 컨텍스트에 저장되었다가 분리된 상태
저장되었던 적 있기 때문에 엔티티 식별자를 갖고 있는 객체
삭제 removed
DB에 삭제를 요청한 상태의 엔티티
OSIV
OSIV ON
디폴트로 켜져있으며 트랜잭션의 범위 밖도 영속성 컨텍스트의 범위엔 속한다.
컨트롤러를 넘어올 때 부터 View가 렌더링 될 때까지 영속성 컨텍스트가 관리하는 엔티티를 Managed 상태로 유지해 View에서도 지연로딩을 통해 엔티티의 필드값이나 연관 엔티티의 값을 가져올 수 있다.
// Exameple Controller, OSIV ON
public void method(Long id){
Entity entity = jpaRepository.findById(id).get();
// 트랜잭션이 안걸려도 entity는 영속상태이기 때문에 LAZYLOADING 가능
entity.getReferenceEntity().getId(); // OK
entity.changeSomething(); // No Transtaciton + 엔티티 상태변경
service.transactionMethod(entity) // Transtaction 메소드에 엔티티를 태우면
//영속성 컨텍스트의 범위 안이라 상태변경 내용 저장
}
OSIV OFF
영속성 컨텍스트의 범위가 트랜잭션과 일치해 트랜잭션의 범위 밖의 엔티티는 Detached상태가 된다. 이 경우 트랜잭션이 걸려있는 서비스 밖(컨트롤러, View)에서 아직 로딩되지 않은 프록시 필드나 연관 엔티티에 접근하면 LazyLoadingException이 발생한다.
// Example Controller, OSIV OFF
public void method(Long id){
Entity entity = jpaRepository.findById(id).get();
entity.getReferenceEntity().getId(); // // 트랜잭션이 안걸리면 entity는 비영속상태이기
//때문에 LAZYLOADING시 LazyLoadingException 발생
entity.changeSomething(); // No Transtaciton + 엔티티 상태변경
service.transactionMethod(entity) // Transtaction 메소드에 엔티티를 태우면
//영속성 컨텍스트의 범위 밖이라 상태변경 내용 초기화
}
준영속(Detach) 상태의 엔티티 수정하는법
변경감지
트랜잭션 안에서 영속성 컨텍스트에서 엔티티를 조회한 이후에 데이터를 수정해 트랜잭션 커밋시점에 변경감지(Dirty Checking)을 통해 DB에 UPDATE SQL 실행된다.
병합
준영속 상태의 엔티티를 영속상태로 변경
병합은 준영속 엔티티를 영속으로 만들어주지만 영속 엔티티의 값을 모두 준영속 엔티티의 값으로 교체한 이후 변경감지 기능을 통해 DB에 UPDATE SQL 실행된다.
코드 예시
다음과 같이 컨트롤러 계층에서 @AuthenticationPrincipal을 통한 Entity나 클라이언트에게서 넘어온 Entity는 엔티티의 식별자(id)를 갖고 있는 비영속 Entity이다.
@Controller
@RequiredArgsConstructor
public class SettingsController {
static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
static final String SETTINGS_PROFILE_URL = "/settings/profile";
private final AccountService accountService;
@PostMapping(SETTINGS_PROFILE_URL)
public String updateProfile(@CurrentAccount Account account, @Valid @ModelAttribute Profile profile,
Errors errors, Model model, RedirectAttributes attributes){
if(errors.hasErrors()){
model.addAttribute(account);
return SETTINGS_PROFILE_VIEW_NAME;
}
accountService.updateProfile(account, profile);
attributes.addFlashAttribute("message", "프로필 수정했습니다.");
return "redirect:" + SETTINGS_PROFILE_URL;
}
}
변경감지
@Service
@RequiredArgsConstructor
@Transactional
public class AccountService {
private final AccountRepository accountRepository;
public void updateProfile(Account account, Profile profile) {
Account findAccount = accountRepository.findById(account.getId())
.orElseThrow(EntityNotFoundException::new);
findAccount.updateProfile(profile);
}
}
Repository에서 Entity 조회 후 수정에 필요한 내용 수정, @Transactional을 통해 변경감지, JPQ에 의해 DB UPDATE SQL 발생
병합
@Service
@RequiredArgsConstructor
@Transactional
public class AccountService {
private final AccountRepository accountRepository;
public void updateProfile(Account account, Profile profile) {
account.updateProfile(profile);
accountRepository.save(account);
}
}
public interface JPARepository<T>{
@PersistenceContext
EntityManager em;
public T save(T t){
if(t.getId() == null)em.persist(t); // 비영속 상태
else em.merge(t); // 준영속 상태
}
}
준영속 상태의 Entity의 정보를 변경 후 save할 때 엔티티 식별자의 유무에 따라 Merge 발생. 수정되는 필드 외의 값도 모두 대체되기 때문에 수정되는 profile 외의 값도 모두 들고 있어야 한다.
정리
병합시에 값이 없으면 null로 업데이트할 위험이 있고 해당 기능을 피하기 위해서 변경가능한 데이터외에 데이터도 노출해야하는 보안과 낭비에 대한 문제가 있기 때문에 변경감지만을 이용해 업데이트하는 것이 좋다.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard