[JPA활용 1] 변경 감지와 병합(merge)
준영속 엔티티
영속성 컨텍스트가 더는 관리하지 않는 엔티티.
준영속 엔티티를 수정하는 방법
- 변경 감지 기능 사용
- 병합(
merge
) 사용
변경 감지 기능 사용
@Transactional
void update(Item itemParam) { // itemParam: 파라미터로 넘어온 준영속 상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId());
findItem.setPrice(itemParam.getPrice()); // 데이터 수정
}
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법이다.
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 -> 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 실행
병합 사용
병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다.
@Transactional
void update(Item itemParam) {
Item mergeItem = em.merge(itemParam);
}
병합 동작 방식
merge()
실행- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
- 조회한 영속 엔티티(
mergeMember
)에member
엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀐다.) - 영속 상태인 mergeMember를 반환한다.
병합 동작 방식 정리
- 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회.
- 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체.(병합한다.)
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
[주의] : 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면
null
로 업데이트 할 위험도 있다.(병합은 모든 필드를 교체한다.)
ItemRepository 메서드 분석
@Repository
public class ItemRepository {
@PersistenceContext
EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
...
}
save()
메서드는 식별자 값이 없으면(null
) 새로운 엔티티로 판단해서 영속화(persist)하고, 식별자가 있으면 병합(merge)- 준영속 상태인 상품 엔티티를 수정할 때는
id
값이 있으므로 병합 수행
새로운 엔티티 저장과 준영속 엔티티 병합을 한번에 처리
save()
메서드를 보면 식별자 값이 없으면 새로운 엔티티로 판단해서 persist()
로 영속화하고, 만약 식별자 값이 있으면 이미 한번 영속화 되었던 엔티티로 판단해서 merge()
로 수정(병합)한다. 이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 로직이 단순해진다.
[참고]
save()
메서드는 식별자를 자동 생성해야 정상 동작한다. 여기서Item
엔티티의 식별자는 자동으로 생성되도록@GeneratedValue
를 선언했다. 따라서, 식별자 없이save()
메서드를 호출하면persist()
가 호출되면서 식별자 값이 자동으로 할당된다.
반면에 식별자를 직접 할당하도록@Id
만 선언했다고 가정한다면, 식별자를 할당하지 않고save()
메서드를 호출하면 식별자가 없는 상태로persist()
를 호출하기 때문에 예외가 발생한다.
[참고]
실무에서는 보통 업데이트 기능이 매우 제한적이다. 그런데 병합은 모든 필드를 변경해버리고, 데이터가 없으면null
로 업데이트 해버리기 때문에 이 문제를 해결하려면, 변경 폼 화면에서 모든 데이터를 유지해야 한다. 실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
해결 방법
엔티티를 변경할 때는 항상 변경 감지를 사용!
- 트랜잭션이 있는 서비스 계층에 식별자(
Id
)와 변경할 데이터를 명확하게 전달.(파라미터 / DTO) - 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경.
- 트랜잭션 커밋 시점에 변경 감지 실행
ItemController
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
// 상품 수정(권장 코드)
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
itemService.updateItem(form.getId(), form.getName(), form.getPrice());
return ...;
}
}
ItemService
@Service
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
// 영속성 컨텍스트가 자동 변경
@Transactional
public void updateItem(Long id, String name, int price) {
Item item = itemRepository.findOne(id);
item.setName(name);
item.setPrice(price);
}
}
댓글남기기