반응형
요구사항
JPA에서 특정 FK(외래 키)에 대한 엔티티 리스트를 업데이트할 때, 다음과 같은 로직이 필요하다.
- 요청으로 받은 리스트의 엔티티들은 업데이트 또는 신규 추가되어야 함.
- 기존 DB에는 있지만 요청 리스트에는 없는 엔티티들은 삭제되어야 함.
문제 코드 (예시)
public void updateEntityList(Long parentId, List<ChildDto> childDtoList){
long result = childRepository.deleteByParentId(parentId);
if(result > 0){
childDtoList.stream().forEach(childDto ->
childRepository.save(childMapper.toEntity(childDto))
);
}
}
문제점
위 코드에서는 기존 엔티티를 일괄 삭제한 후 새로운 데이터를 삽입하는 방식이 사용되었다. 하지만 이렇게 하면:
- 낙관적 락 충돌(Optimistic Locking Conflict) 이 발생할 수 있음. ( OptimisticLockException )
- 이전 데이터가 모두 삭제되므로 트랜잭션이 실패할 경우 데이터 무결성이 깨질 위험이 있음.
- 불필요한 DELETE & INSERT가 증가하여 성능 저하가 발생할 수 있음.
해결 코드 (예시)
해결 방법
- 기존 DB에서 해당 FK에 속한 모든 엔티티를 조회한다.
- 요청받은 엔티티 리스트와 비교하여:
- 동일한 PK가 존재하면 업데이트 (updateFromDto() (사용자 정의 메서드) 활용).
- 새로운 PK이면 저장.
- 기존에는 있지만 요청 리스트에는 없는 엔티티는 삭제.
@Transactional
public void updateEntityList(Long parentId, List<ChildDto> childDtoList){
// 1. 기존 DB에서 해당 parentId에 속한 모든 엔티티 조회
List<Child> existingChildList = childRepository.findByParentId(parentId);
// 2. 요청받은 DTO를 기존 엔티티와 비교하여 매핑
Map<Long, ChildDto> updatedChildDtoMap = childDtoList.stream()
.filter(dto -> dto.getId() != null)
.collect(Collectors.toMap(ChildDto::getId, Function.identity()));
// 3. 기존 엔티티 업데이트 및 삭제 처리
for (Child existingChild : existingChildList) {
if (updatedChildDtoMap.containsKey(existingChild.getId())) {
existingChild.updateFromDto(updatedChildDtoMap.get(existingChild.getId()));
} else {
childRepository.delete(existingChild);
}
}
// 4. 신규 추가 (ID가 없는 엔티티)
List<Child> newChildList = childDtoList.stream()
.filter(dto -> dto.getId() == null)
.map(childMapper::toEntity)
.collect(Collectors.toList());
childRepository.saveAll(newChildList);
}
개선된 점
- Hibernate 세션 충돌 방지: 기존 객체를 유지하며 필드 값만 변경.
- Dirty Checking 활용: 변경된 필드만 DB에 반영되므로 save() 호출이 불필요.
- 성능 최적화: 불필요한 INSERT 및 DELETE 연산 감소.
관련 엔티티 정의
@Entity
@Table(name = "child")
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
public void updateFromDto(ChildDto dto) {
this.name = dto.getName();
}
}
새로 배운 개념: Dirty Checking (변경 감지)
Dirty Checking 이란?
JPA는 트랜잭션이 끝날 때(@Transactional 종료 시) 영속 상태의 엔티티를 자동으로 감지하여 변경된 필드만 업데이트한다. 따라서 save()를 호출하지 않아도 변경 사항이 DB에 반영된다.
Dirty Checking 적용 과정
@Transactional
public void updateEntity(Long id, String newName) {
Child child = childRepository.findById(id).orElseThrow();
child.setName(newName); // 필드 값만 변경
} // @Transactional 종료 시 Dirty Checking으로 UPDATE 자동 실행
save()가 필요한 경우
1. 새로운 엔티티를 추가할 때:
childRepository.save(newChild);
2. 비영속 상태의 엔티티를 업데이트할 때:
chedChild = new Child();
detachedChild.setId(3L);
detachedChild.setName("Updated Name");
childRepository.save(detachedChild); // 비영속 상태이므로 반드시 save 필요
결론
- Hibernate 세션 충돌을 방지하려면 기존 엔티티를 유지하며 필드 값만 변경하는 방식이 필요함.
- Dirty Checking을 활용하면 save() 호출 없이도 자동으로 변경 사항이 반영됨.
- 신규 엔티티의 경우에는 save()가 필요하며, 비영속 상태의 엔티티를 직접 업데이트하는 경우에도 save()를 호출해야 함.
반응형