BackEnd/JPA

[JPA] 변경감지와 병합(merge)

샤아이인 2022. 4. 11.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

세션 14. 변경감지와 병합

우선 준영속 엔티티에 대하여 알아보자.

 

준영속 엔티티는 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 의미한다.

다음 코드를 살펴보자.

@PostMapping("/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form, @PathVariable String itemId){
    Book book = new Book();
    book.setId(form.getId());
    book.setName(form.getName());
    book.setPrice(form.getPrice());
    book.setStockQuantity(form.getStockQuantity());
    book.setAuthor(form.getAuthor());
    book.setIsbn(form.getIsbn());

    itemService.saveItem(book);

    return "redirect:/items";
}
 

위의 새로 생성 된 book객체는 new를 통해 새로 생성된 객체이고, 아직 영속성 컨텍스트에는 등록되지 않았다.

단지 식별자를 갖고있을 뿐이다.

 

문제는 이 식별자는 DB상에 저장된 어떠한 엔티티의 식별자 값이라는 점이다.

비록 book이라는 객체가 new를 통해 새롭게 만들어 졌지만(JPA는 이 book이라는 객체를 감시하지 않는 상황),

DB에 저장되어 있는 엔티티의 id값인 식별자가 부여되었기 때문에 준영속 상태라고 할수있다.

(Book 객체는 이미 DB 에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준 영속 엔티티로 볼 수 있다.)

 

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

1. 변경감지 기능

2. 병합(merge) 사용하기

 

● 변경감지 기능

@Transactional
public void updateItem(Long itemId, Book param){ // 파라미터로 넘어온 준영속 상태의 book 엔티티
    Item findItem = itemRepository.findOne(itemId); // id값으로 DB에서 조회
    findItem.setPrice(param.getPrice()); // 데이터 수정
    findItem.setName(param.getName());
    findItem.setStockQuantity(param.getStockQuantity());
}
 

영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법이다.

트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 => 트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 이 동작해서 데이터베이스에 UPDATE SQL 문을 날려준다.

 

● 병합(merge) 사용

@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
    Item mergeItem = em.merge(item);
}
 

준영속 상태의 Item을 전달받아 영속성 컨텍스트에 병합시켜 준다.

merge의 내부 동작 원리는 사실상 변경감지 기능과 유사하다.

 

- merge()의 동작 방식

1. merge() 를 실행한다.

 

2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.

    2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.

 

3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다.

(member 엔티티의 모든 값 을 mergeMember에 밀어 넣는다. 즉, member의 모든 값을 mergeMember에 setting해준다.)

 

4. 영속 상태인 mergeMember를 반환한다.

 

5. 이후 트랜잭션 commit 시점에 변경된 내용을 반영한 SQL UPDATE 문을 DB에 보내게 된다.

 

주의! : 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다.

병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)

 

병합 기능을 사용하여 Item 엔티티를 저장하는 Repository의 코드는 다음과 같다.

@Repository
public class ItemRepository {
    @PersistenceContext
    EntityManager em;

    public void save(Item item) {
        if (item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }
    //...
}
 

이 메서드 하나로 저장과 수정(병합)을 다 처 리한다.

코드를 보면 식별자 값이 없으면 새로운 엔티티로 판단해서 persist() 로 영속화하고 만약 식별자 값이 있으면 이미 한번 영속화 되었던 엔티티로 판단해서 merge() 로 수정(병합)한다.

 

결국 여기서의 저장 (save)이라는 의미는 신규 데이터를 저장하는 것뿐만 아니라 변경된 데이터의 저장이라는 의미도 포함한다.

이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 클라이언트의 로직이 단순해진다.

 

하지만 실무는 이렇게 merge로 모든 필드를 매꿔버릴수 있는 경우는 드물다.

병합은 모든 필드를 변경해버리고, 데이터 가 없으면 null 로 업데이트 해버린다.

병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데 이터를 항상 유지해야 한다.

실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.

 

결론 : 엔티티를 변경할때는 항상 변경감지 기능을 사용하세요

 

- 컨트롤러에서 어설프게 엔티티를 생성하지 마세요.

- 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달하세요.(파라미터 or dto)

트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하세요.

- 트랜잭션 커밋 시점에 변경 감지가 실행됩니다.

 

다음 컨트롤러를 살펴보자.

이글 맨위에서 본 컨트롤러 처럼 Book 이라는 엔티티를 생성하여 setter로 Book에 데이터를 전부 전달하지 않는다.

@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 "redirect:/items";
    }
}
 

오히려 itemService에 updateItem 메서드를 호출하여 데이터만 전달하고 있다.

@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    /**
    * 영속성 컨텍스트가 자동 변경
    */
    @Transactional
    public void updateItem(Long id, String name, int price) { // id와 데이터를 전달받음
        Item item = itemRepository.findOne(id); // 서비스 계층에서 엔티티 조회하고
        item.setName(name); // 데이터 수정
        item.setPrice(price);
    }
}
 

서비스 계층의 트랜젝션 범위 안에서 데이터가 변경되고, commit 시점에 변경사항들이 적용된다.

댓글