Intro
안녕하세요. 환이s입니다👋
이전 포스팅에서 주문 검색 기능을 JQPL, JPA 표준 Criteria 그리고 Querydsl을 활용해서 알아봤습니다.
오늘은 JPA에서 데이터를 변경하는 방법에 대해 소개하고 변경 시에 주의할 점에 대해서 소개하려고 합니다🙂
데이터를 변경하는 방법에는 변경 감지(Dirty Checking)와 병합(Merge)기능이 JPA의 핵심적인 요소로, 애플리케이션의 성능과 데이터 일관성을 유지하는 데 중요한 역할을 합니다.
순차적으로 알아보겠습니다.
준영속 엔티티
준영속 엔티티는 JPA에서 영속성 컨텍스트와의 연결이 끊어진 상태의 엔티티를 의미합니다.
즉, 엔티티가 처음에는 영속 상태로 관리되다가, 영속성 컨텍스트가 종료되거나 명시적으로 분리될 때 준영속 상태로 전환됩니다.
주요 특징은 다음과 같습니다.
1️⃣ 상태 유지 : 준영속 엔티티는 데이터베이스에 저장된 상태를 유지하지만, 더 이상 JPA의 영속성 컨텍스트에 의해 관리되지 않기 때문에, 상태 변경이 자동으로 데이터베이스에 반영되지 않습니다.
2️⃣ 수정 필요 : 준영속 엔티티의 상태를 변경한 후에는 다시 영속성 컨텍스트에 병합(Merge)하여 데이터베이스에 반영해야 합니다. 이는 EntityManager.merge() 메서드를 통해 수행합니다.
3️⃣ 식별 가능성 : 준영속 엔티티는 동일한 식별자(Primary Key)를 가진 다른 엔티티와 비교하여 JPA가 상태를 관리하도록 할 수 있습니다. 이 경우, JPA는 어떤 엔티티가 데이터베이스에 있는지 확인하고, 필요시 업데이트를 수행합니다.
준영속 엔티티는 종종 트랜잭션이 종료된 후에도 데이터를 수행해야 할 필요가 있는 상황에서 유용합니다.
예를 들어, 웹 애플리케이션에서 사용자가 입력한 데이터를 처리한 후, 그 데이터를 다시 영속성 컨텍스트에 반영하고자 할 때 준영속 상태의 엔티티를 활용할 수 있습니다.
준영속 엔티티에 대해 이해는 변경 감지와 병합을 제대로 활용하기 위한 기초 지식이라서 먼저 알아봤습니다.
다음으로 변경 감지 기능을 사용해서 알아보겠습니다.
변경 감지(Dirty Checking)
변경 감지(Dirty Checking)는 엔티티의 상태가 변경되었는지를 자동으로 감지하여, 필요시 데이터베이스에 업데이트를 수행합니다. 이를 통해 개발자는 직접적인 SQL 쿼리 없이도 데이터의 변화를 관리할 수 있습니다.
변경 감지 기능 코드를 확인해 보겠습니다.
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다.
findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
}
위 코드처럼 영속성 컨텍스트에서 엔티티를 다시 조회한 후 데이터를 수정하는 코드입니다.
영속성 컨텍스트에서 엔티티를 다시 조회한 후 데이터를 수정하는 과정은 JPA의 강력한 기능 중 하나로, 변경 감지(Dirty Checking)를 활용하여 효율적으로 데이터베이스와 상호작용을 할 수 있습니다.
이 과정을 단계별로 살펴보겠습니다.
✅첫 번째로,
트랜잭션 내에서 영속성 컨텍스트에 존재하는 엔티티를 다시 조회합니다.
이때, Entitymanager를 사용하여 식별자(Primary Key)를 기반으로 엔티티를 검색합니다. 이렇게 조회된 엔티티는 영속 상태로 관리되며, 이후의 모든 변경 사항은 자동으로 추적됩니다.
✅두 번째로,
변경할 값을 선택하여 엔티티의 속성을 수정합니다.
이 과정에서 개발자는 직접 SQL 쿼리를 작성할 필요 없이, 객체 지향적인 방식으로 엔티티의 필드를 변경할 수 있습니다. 예를 들어, entity.setField(value)와 같이 속성을 업데이트합니다.
✅마지막으로,
트랜잭션 커밋 시점에 JPA는 변경 감지를 통해 수정된 엔티티의 상태를 확인합니다.
이때, 영속성 컨텍스트 내에서 변경된 사항이 감지되면, JPA는 자동으로 데이터베이스에 UPDATE SQL을 실행하여 변경된 내용을 반영합니다. 이러한 과정 덕분에 개발자는 복잡한 데이터베이스 작업을 간편하게 처리할 수 있습니다.
병합(Merge)
병합(Merge)은 JPA에서 준영속 엔티티를 영속성 컨텍스트에 다시 연결하여 데이터베이스와 동기화하는 과정입니다. 이 과정은 애플리케이션에서 작업한 엔티티의 상태를 데이터베이스에 반영하기 위해 필요합니다.
@Transactional
void updateMember(Member memberPaream) { //memberParam: 파리미터로 넘어온 준영속 상태의 엔티티
Member mergeMember = em.merge(memberPaream);
}
병합(Merge)을 수행할 때, 먼저 준영속 엔티티를 em.merge() 메서드를 사용하여 영속성 컨텍스트로 전달합니다. 이 메서드는 주어진 엔티티를 기반으로 새로운 영속 엔티티를 생성하고, 기존 데이터베이스의 내용을 참조하여 상태를 업데이트합니다.
연결하는 과정은 다음과 같은 단계로 이루어집니다.
1️⃣ merge() 메서드를 호출합니다.
2️⃣ 파라미터로 전달된 준영속 엔티티의 식별자 값을 사용하여 1차 캐시에서 해당 엔티티를 조회합니다. 만약 1차 캐시에 엔티티가 존재하지 않으면, JPA는 데이터베이스에서 해당 엔티티를 조회한 후 1차 캐시에 저장합니다.
3️⃣ 조회된 영속 엔티티(예: mergeMember)에 member 엔티티의 값을 채워 넣습니다. 이 과정에서 member 엔티티의 모든 속성이 mergeMember에 복사되며, 예를 들어 mergeMember의 "회원 1"이라는 이름이 "회원명변경"으로 업데이트됩니다.
4️⃣ 마지막으로, 영속 상태인 mergeMember가 반환됩니다.
이러한 과정을 통해 병합은 준영속 엔티티의 상태를 영속성 컨텍스트에 반영하고, 데이터베이스와의 동기화를 원활하게 수행합니다.
Merge 시 주의할 점
하지만 Merge를 사용할 때 주의할 점이 있습니다.
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경됩니다.
예를 들어, 병합 시 값이 없으면 nul로 업데이트할 위험이 있습니다.(병합은 모든 필드를 교체합니다.)
이전 포스팅에서 구현한 상품 리포지토리의 저장 메서드로 알아보겠습니다.
@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)이라는 의미는 신규 데이터를 저장하는 것뿐만 아니라 변경된 데이터의 저장이라는 의미도 포함합니다. 이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 클라이언트의 로직이 단순해집니다.
하지만 실무에서는 보통 업데이트 기능이 매우 제한적입니다.
병합은 모든 필드를 변경해 버리고, 데이터가 없으면 null로 업데이트해버립니다. 병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데이터를 항상 유지해야 합니다. 실무에서는 보통 변경 가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭습니다.
가장 좋은 해결 방법
가장 좋은 해결 방법으로는 엔티티를 변경할 때는 항상 변경 감지를 사용하시면 됩니다.
추가적으로 로직 작성할 때 다음과 같은 규칙을 정해두고 개발하시면 좋은 해결 방법이 될 거 같습니다.
1️⃣ 컨트롤러에서 어설프게 엔티티를 생성하지 않는다.
2️⃣ 트랜잭션이 있는 서비스 계층에 식별자(id)와 변경할 데이터를 명확하게 전달한다.(파라미터 or dto)
3️⃣ 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경한다.
4️⃣ 트랜잭션 커밋 시점에 변경 감지가 실행됩니다.
@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";
} }
@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);
} }
마치며
변경 감지(Dirty Checking)와 병합(Merge)은 JPA에서 엔티티 상태 관리를 효과적으로 수행하는 핵심 기능입니다.
변경 감지(Dirty Checking)는 영속성 컨텍스트 내에서 엔티티의 상태 변화를 자동으로 추적하고, 트랜잭션 커밋 시점에 이를 데이터베이스에 반영하는 과정을 통해 개발자가 직접 SQL 쿼리를 작성할 필요 없이 간편하게 데이터를 관리할 수 있게 합니다.
반면, 병합(Merge)은 준영속 상태의 엔티티를 영속성 컨텍스트에 다시 연결하여 데이터베이스와의 동기화를 가능하게 합니다. 이 과정은 엔티티의 식별자를 기반으로 기존 데이터를 조회하고, 필요한 경우 업데이트를 수행함으로써 데이터의 일관성을 유지합니다.
가장 좋은 해결 방법을 알려드렸지만 이 두 기능을 적절히 활용하면, 애플리케이션의 성능을 극대화하고, 데이터 무결성을 보장할 수 있습니다.
따라서 JPA를 사용하는 개발자는 변경 감지와 병합의 작동 원리를 깊이 이해하고, 이를 실무에 효과적으로 적용하는 것이 중요합니다. 이러한 이해는 더 나아가 코드의 품질과 유지보수성을 높이는 데 기여할 것입니다😃
다음 포스팅에서 뵙겠습니다👋
위 포스팅 글은 김영한님의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발을 참고했습니다.
'[ ORM ] > JPA' 카테고리의 다른 글
[ JPA ] E-commerce 프로젝트 - 주문 검색 기능 개발 (JPQL,Criteria,Querydsl) (2) | 2025.01.13 |
---|---|
[ JPA ] E-commerce 프로젝트 - 주문 도메인 개발 (1) | 2025.01.11 |
[ JPA ] E-commerce 프로젝트 - 상품 도메인 개발 (1) | 2025.01.06 |
[ JPA ] E-commerce 프로젝트 - 회원 도메인 개발 (3) | 2024.12.27 |
[ JPA ] E-commerce 프로젝트 - 도메인 분석 설계 (4) | 2024.12.23 |