준영속 엔티티를 수정하는 방법은 두 가지가 있다.
1. merge ( 병합 )
2. DirtyChecking ( 변경감지 )
준영속 엔티티 수정은 merge보다는 DirtyChecking이 권고된다. 왜 그럴까?
준영속 엔티티란?
준영속엔티티란 영속성 엔티티의 관리를 받고 있지 않은 엔티티를 의미한다.
예를들어,
DB에서 조회한 Book 엔티티 리스트를 추출하여 만든 화면이다. '조회' 작업이 끝나면 트랜잭션이 종료되어 영속성컨텍스트도 종료된다. 여기서 수정 버튼을 클릭해보자.
데이터를 수정하고 Submit 버튼을 누르면, 새로운 Book 엔티티가 생성된다. 근데 다른 점이 하나 있다. 동일한 식별자가 이미 DB에 있다. '생성(create)'이 아닌 '수정(update)'은 식별자가 가리키는 레코드를 수정하는 작업이다. 그러므로 UPDATE를 위한 EntityManager와 영속성 컨텍스트가 생성되면 아래 그림과 같은 상태가 된다.
DB와 엔티티는 식별자를 가지고 있지만 영속성컨텍스트는 엔티티를 가지고 있지 않은 엔티티를 준영속 엔티티라 부른다. 그러므로 UPDATE를 하려면 먼저 1차캐시에 준영속엔티티를 등록하여 엔티티를 영속화한 다음, DirtyChecking으로 UPDATE문을 자동생성해야 한다.
DirtyChecking
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity){
Item findItem = itemRepository.findOne(itemId); //영속화 된 엔티티 만들기
findItem.setPrice(price);
findItem.setName(name);
findItem.setStockQuantity(stockQuantity);
}
}
itemId는 식별자이다. 식별자로 영속성컨텍스트에 조회하면 1차캐시에 존재하지 않으니 SELECT문을 생성하여 DB에 조회한다. 조회가 완료되면 엔티티를 생성하여 반환한다. findItem은 영속화된 엔티티이다. 엔티티를 Setter함수로 수정하면 DirtyChecking이 이루어져 자동으로 UPDATE문이 생성된다.
자세한 내용은 아래 링크를 참고하기를 바란다.
준영속 엔티티를 수정하는 방식이 하나 더있다. 바로 merge이다.
merge
public void save(Item item){
if(item.getId() == null){ // 신규생성
em.persist(item);
}else{
em.merge(item); // 수정
}
}
위 코드는 Item 엔티티의 식별자가 없으면 영속화를, 식별자가 있으면 준영속 엔티티로 판단하여 merge하는 로직이다. merge는 위에서 보았던 DirtyChecking과 같은 원리로 돌아간다. JPA가 단 한줄로 자동화 했을 뿐이다. merge 메소드로 준영속엔티티를 파라미터로 넘기면 영속성컨텍스트에서 식별자를 체크한다. 식별자가 존재하지 않으면 DB 조회를 한다. 조회된 데이터는 1차캐시에 저장되고 준영속엔티티의 데이터를 토대로 조회된 데이터는 수정(병합)된다. 그리고 병합된 데이터를 토대로 생성한 엔티티를 반환한다.
위 과정이 단 한줄로 자동화되었다. 그런데 문제가 하나 있다. 준영속 엔티티의 모든 데이터가 병합 된다는 것이다. 조회된 Book의 가격이 20000이었는데 준영속 엔티티 Book의 가격이 Null이면 Null로 덮어 씌어진다. merge는 이런 위험성이 있어서 권고되지 않는다. 차라리 DrityChecking으로 개발자가 하나씩 체크하는 것이 안전하고 좋다.
Setter 사용하지 않기
DirtyChecking으로 수정해야 하므로 개발자는 적절한 수정코드를 작성해야 한다. 개발자가 가장 자주하는 실수는 Setter함수를 무분별하게 남발하는 코드 작성이다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity){
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(price); // SETTER1
findItem.setName(name); // SETTER2
findItem.setStockQuantity(stockQuantity); //SETTER3
}
}
price,name,stockQuantity가 모두 setter로 수정되었다. setter는 어디서나 사용되는 코드이다. setter가 어떤 목적으로 사용되었는지 추적하기 힘들고 Setter에는 필드명이 그대로 노출되어 안전하지 못하다. 필드명을 은닉하고 추적가능한 의미있는 하나의 메소드로 만들어야 한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity){
Item findItem = itemRepository.findOne(itemId);
findItem.changeItem(price,name,stockQuantity); // 메소드 하나로 줄어듦
}
changeItem이라는 이름은 메소드가 update 로직을 갖고 있음을 암시한다. 그리고 Setter가 사라져 Item이 어떤 이름의 필드를 가지고 있는지 은닉되었다.
한 가지 더 수정해보자.
파라미터가 너무 많다. 파라미터가 너무 많으면 DTO를 사용하여 파라미터 수를 줄이는 것이 좋다. 파라미터를 줄여야 단순한 코드가 된다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void updateItem(ItemDto itemDto){
Item findItem = itemRepository.findOne(itemDto.getId());
findItem.changeItem(itemDto);
}
4개였던 파라미터가 한 개로 줄어 코드가 훨씬 보기 좋아졌다. 이처럼 파라미터가 너무 많아 관리하기 힘든 경우 DTO 객체를 만들어 파라미터를 줄이는 방법도 좋다.
정리하면, 준영속 엔티티는 DB에 저장된 식별자를 보유하고 있는 엔티티로, 영속성컨텍스트에 다시 식별자가 등록되면 DirtyChecking으로 UPDATE를 구현할 수 있다. merge는 준영속 엔티티 데이터 전체를 병합하는 방법으로 Null 값등 이상한 값이 병합될 가능성이 있으므로 추천되지 않는다. 그러므로 적절한 DirtyChecking을 위해 Setter를 줄이거나 파라미터를 줄이는 등 깔끔한 코드를 작성하는 것이 좋다.
참고자료
'JPA > JPA Basic' 카테고리의 다른 글
[JPA] 조회 API 성능 최적화하기 ( XToOne ) (0) | 2023.06.28 |
---|---|
[JPA] DTO의 필요성 (0) | 2023.06.28 |
[JPA] 값타입 컬렉션 ( @ElementCollection, @CollectionTable ) (0) | 2023.06.12 |
[JPA] 임베디드 타입 ( @Embedded, @Embeddable ) (0) | 2023.06.09 |
[JPA] 영속성 전이와 고아객체 (0) | 2023.06.08 |