본문 바로가기
BackEnd/JPA

[JPA] 웹 계층 개발(3)

by 개발 Blog 2024. 10. 5.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

변경 감지와 병합(merge)

 

준영속 엔티티란?

영속성 컨텍스트에서 더 이상 관리되지 않는 엔티티를 준영속 엔티티라고 한다. 예를 들어, 데이터베이스에 이미 저장되어 있는 Book 객체는 itemService.saveItem(book)을 통해 다시 수정하려 할 때 준영속 상태가 된다. 이는 식별자 값이 있는 엔티티로, 이미 한 번 영속 상태였지만 현재는 영속성 컨텍스트에서 관리되지 않는 상태를 의미한다.

준영속 엔티티를 수정하는 두 가지 방법

1. 변경 감지(Dirty Checking) 사용

변경 감지는 영속성 컨텍스트에서 관리되는 엔티티에 대해 자동으로 값의 변화를 감지하여 데이터베이스에 반영하는 방식이다. 트랜잭션 내에서 엔티티를 다시 조회하고, 수정할 값을 설정하면 트랜잭션 종료 시점에 변경된 내용이 자동으로 데이터베이스에 반영된다.

@Transactional
void update(Item itemParam) {
    Item findItem = em.find(Item.class, itemParam.getId());
    findItem.setPrice(itemParam.getPrice());
}

 

위 코드에서 영속 상태의 엔티티를 조회한 후, 수정할 값을 지정하고 트랜잭션이 끝나면 변경된 값이 데이터베이스에 자동으로 반영된다.

 

2. 병합(merge) 사용

병합은 준영속 상태의 엔티티를 영속 상태로 다시 전환할 때 사용하는 방법이다. 병합을 통해 준영속 엔티티의 값을 영속 엔티티에 복사하여 다시 관리하게 만든다.

@Transactional
void update(Item itemParam) {
    Item mergeItem = em.merge(itemParam);
}

병합은 준영속 엔티티를 영속 상태로 바꾸면서 데이터를 병합한 새로운 영속 엔티티를 반환한다. 기존 영속 엔티티에 값이 덮어 써지며, 이 과정에서 모든 값이 교체된다.

병합의 동작 방식

  1. merge() 호출 시 준영속 엔티티의 식별자를 사용해 1차 캐시에서 영속 엔티티를 조회한다.
  2. 1차 캐시에 없으면 데이터베이스에서 조회하여 1차 캐시에 저장한다.
  3. 조회된 영속 엔티티에 준영속 엔티티의 값을 복사한다.
  4. 병합된 영속 엔티티를 반환한다.

병합은 모든 속성을 교체하기 때문에, 값이 없는 필드가 null로 변경될 위험이 있다. 따라서 변경이 필요한 속성만을 선택적으로 업데이트할 수 있는 변경 감지를 사용하는 것이 더 안전한 방법이다.

 

상품 리포지토리의 save() 메서드 분석

ItemRepository에서 save() 메서드는 식별자가 없으면 새로운 엔티티로 인식해 영속화(persist)하고, 식별자가 있으면 병합(merge)을 사용해 수정 작업을 수행한다. 이를 통해 새로운 데이터의 저장과 기존 데이터의 수정을 하나의 메서드로 처리할 수 있어, 클라이언트 코드에서 로직을 단순화할 수 있다.

package jpabook.jpashop.repository;
@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()로 병합한다.

 

이로 인해 save() 메서드는 신규 데이터를 저장하는 것뿐만 아니라, 변경된 데이터를 수정하는 역할도 함께 수행하게 된다. 이를 통해 클라이언트 측에서는 저장과 수정을 구분할 필요 없이 간단하게 데이터를 처리할 수 있으며, 로직이 단순해진다.

 

병합을 사용하는 경우 병합은 준영속 상태의 엔티티를 수정할 때 사용된다. 반면, 영속 상태의 엔티티는 변경 감지(Dirty Checking)가 동작해 트랜잭션 커밋 시점에 자동으로 수정된다. 따라서 별도의 수정 메서드를 호출할 필요가 없다.

 

save() 메서드 동작 방식 참고 사항

  1. 식별자 자동 생성 save() 메서드는 식별자를 자동으로 생성해야 정상적으로 동작한다. 예를 들어, Item 엔티티의 식별자가 @GeneratedValue로 자동 생성되도록 설정되면, save() 메서드에서 persist()가 호출될 때 자동으로 식별자가 할당된다.
  2. 직접 식별자 할당 만약 식별자를 직접 할당하는 방식으로 설정되었다면, 식별자가 없을 때 save() 메서드를 호출하면 예외가 발생할 수 있다. persist()는 식별자가 없는 상태에서 호출되면 식별자가 필요하기 때문이다.

실무에서 병합의 한계와 문제점

실무에서는 업데이트 기능이 제한적인 경우가 많다. 병합(merge)을 사용할 때 문제가 되는 것은 모든 필드가 덮어쓰기 되며, 값이 없으면 null로 업데이트되는 위험이 있다는 점이다. 이를 해결하려면 병합 시 모든 데이터를 항상 유지해야 한다. 그러나 실무에서는 보통 일부 필드만 변경 가능한 경우가 많기 때문에, 병합을 사용하기보다는 변경 감지(Dirty Checking)를 사용하는 것이 더 적합하다.

 

가장 좋은 해결 방법: 변경 감지 사용

  1. 엔티티를 변경할 때는 변경 감지 기능을 사용하자
    영속성 컨텍스트에서 관리되고 있는 엔티티는 변경 감지 기능이 동작하여 수정할 때 유리하다. 병합을 사용하는 것보다 안전하고 효율적인 방법이다.
  2. 컨트롤러에서 엔티티를 생성하지 말 것
    트랜잭션이 있는 서비스 계층에서 식별자와 변경할 데이터를 명확하게 전달하는 것이 좋다. 서비스 계층에서 영속 상태의 엔티티를 조회한 뒤, 필요한 데이터를 변경하는 방식을 사용한다.
  3. 트랜잭션이 있는 서비스 계층에서 처리
    트랜잭션 커밋 시점에 변경 감지 기능이 실행되어 데이터베이스에 변경 사항이 반영되도록 한다.
@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;
    /**
     * 상품 수정, 권장 코드
     */
    @PostMapping(value = "/items/{itemId}/edit")
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form")
    BookForm form) {
        itemService.updateItem(itemId, form.getName(), form.getPrice(),
                form.getStockQuantity());
        return "redirect:/items";
    }
}
package jpabook.jpashop.service;
@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;
    /**
     * 영속성 컨텍스트가 자동 변경
     */
    @Transactional
    public void updateItem(Long id, String name, int price, int stockQuantity) {
        Item item = itemRepository.findOne(id);
        item.setName(name);
        item.setPrice(price);
        item.setStockQuantity(stockQuantity);
    }
}
```

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 웹 계층 개발(4)  (0) 2024.10.05
[JPA] 웹 계층 개발(2)  (0) 2024.10.05
[JPA] 웹 계층 개발(1)  (2) 2024.10.04
[JPA] 주문 도메인 개발  (3) 2024.10.03
[JPA] 상품 도메인 개발  (0) 2024.10.03