package jpabook.jpashop.repository;
import jakarta.persistence.EntityManager;
import jpabook.jpashop.domain.item.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item i", Item.class)
.getResultList();
}
}
public void save(Item item) {...} 에선 순차적으로 어떤 일이 일어나는 것일까?
📝 save(Item item) 메서드 동작 상세 설명
1. 메서드 호출
save(Item item);
Item
객체를 메서드의 인자로 전달받습니다.- 이
Item
객체는 새로운 객체(즉, 아직 데이터베이스에 저장되지 않은 객체)일 수도 있고, 이미 데이터베이스에 저장된 후 수정된 객체일 수도 있습니다.
2. item.getId()
를 호출
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
- JPA는 객체의 영속성 상태를 판단하기 위해
item.getId()
를 사용합니다. getId()
값이null
이면, 이 객체는 데이터베이스에 아직 저장되지 않은 새로운 객체로 간주됩니다.getId()
값이null
이 아니면, 데이터베이스에 이미 존재하는 객체(즉, 수정 대상)로 간주됩니다.
3. em.persist(item)
동작 과정:
- 새로운 엔티티를 영속화:
em.persist(item)
를 호출하면 JPA는 이 객체를 영속성 컨텍스트에 등록합니다. - INSERT SQL 생성 준비: JPA는
persist()
를 호출한 시점에 SQLINSERT
문을 준비합니다. 그러나 SQL이 즉시 실행되지 않고 플러시 시점에 실행됩니다. - 플러시 시점: 트랜잭션이 커밋되거나,
em.flush()
가 호출될 때 SQLINSERT
문이 실행되어 데이터베이스에 새로운 행이 추가됩니다.
예시:
INSERT INTO item (name, price, stockQuantity) VALUES ('ItemName', 100, 50);
4. em.merge(item)
동작 과정:
- 영속 객체를 반환:
em.merge(item)
는 영속성 컨텍스트에 있는 객체를 반환하거나, 새로 복사된 영속 객체를 반환합니다. - 속성 복사:
item
의 모든 속성을 데이터베이스에 존재하는 동일한 엔티티에 덮어씌웁니다. - UPDATE SQL 생성 준비:
merge()
는 변경된 데이터에 대해 SQLUPDATE
문을 준비합니다. 이 역시 플러시 시점에 실행됩니다. - 플러시 시점: 트랜잭션 커밋 시점에 SQL
UPDATE
문이 실행됩니다.
예시:
UPDATE item SET name = 'UpdatedItem', price = 150 WHERE id = 1;
5. 영속성 컨텍스트와 데이터베이스 동기화
- JPA는 영속성 컨텍스트와 데이터베이스의 상태를 일치시키기 위해 트랜잭션 커밋 또는
flush()
시점에 필요한 SQL 문을 실행합니다. persist()
는 새로운 엔티티를 저장하기 위한 SQLINSERT
문을,merge()
는 기존 데이터를 갱신하기 위한 SQLUPDATE
문을 실행합니다.
6. 순차적인 실행 흐름
- 메서드 호출:
save(Item item)
이 호출됩니다. - 아이디 체크:
item.getId()
가null
인지 확인합니다.null
이면 새 엔티티 →persist()
.null
이 아니면 기존 엔티티 →merge()
.
- EntityManager 동작:
persist()
는 엔티티를 영속성 컨텍스트에 추가합니다.merge()
는 엔티티 데이터를 데이터베이스의 기존 데이터에 덮어씌웁니다.
- SQL 생성 및 실행:
- 플러시 또는 커밋 시점에
INSERT
또는UPDATE
SQL 문이 실행됩니다.
- 플러시 또는 커밋 시점에
7. 주의점
persist()
는 영속성 컨텍스트에만 영향을 미침:persist()
는 항상 새로운 엔티티에만 사용해야 하며, 기존 엔티티에 사용하면 예외가 발생할 수 있습니다.merge()
는 새로운 인스턴스를 반환:merge()
는 기존 객체를 수정하지 않고 새로운 영속 객체를 반환합니다.merge()
이후에는 반환된 객체를 사용해야 변경 사항이 반영됩니다.- 영속성 컨텍스트 관리:
merge()
된 객체가 영속성 컨텍스트에 관리되므로, 이후 변경 사항은 자동으로 데이터베이스에 반영됩니다.
8. 결론
save(Item item)
메서드는 Item
객체의 상태에 따라 새로운 데이터 저장(persist) 또는 기존 데이터 업데이트(merge)를 수행합니다.
JPA는 이를 통해 객체 지향적으로 데이터베이스와 상호작용할 수 있도록 도와줍니다. 😊
❓ 만약 코드가 다음과 같았다면?
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.persist(item);
}
}
📝 코드 변경 시 발생하는 문제: persist()와 merge()의 차이
1. 문제 발생 시나리오
변경된 코드:
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.persist(item); // 이 부분에 문제가 있음
}
}
동작 설명:
- 새로운 객체 (
item.getId() == null
):em.persist(item)
는 새로운 객체를 영속성 컨텍스트에 추가하고 SQLINSERT
문을 실행합니다.- 정상적으로 동작합니다.
- 기존 객체 (
item.getId() != null
):em.persist(item)
는 새로운 엔티티로 간주하고, 이를 데이터베이스에 추가하려고 시도합니다.- 기존 객체는 이미 데이터베이스에 존재하므로, 중복된 기본 키(PK)로 인해 오류가 발생합니다.
2. 왜 문제가 발생할까?
persist()와 merge()의 차이
- persist()
- 새로운 객체만 처리합니다.
- 항상 새로운 행으로 추가하려고 시도합니다.
- 기존 객체에 대해 호출하면 중복 기본 키 오류가 발생합니다.
- merge()
- 기존 객체를 수정하거나, 비영속 상태의 객체를 복사하여 영속성 컨텍스트에 관리합니다.
- 기존 데이터에 대해 안전하게 작동하며, SQL
UPDATE
문을 생성합니다.
3. 실제 발생할 문제
1. 중복 기본 키 오류 (Primary Key Constraint Violation)
데이터베이스에 이미 존재하는 id
값으로 새 행을 삽입하려 시도하기 때문에 javax.persistence.EntityExistsException
이 발생할 수 있습니다.
INSERT INTO item (id, name, price) VALUES (1, 'Existing Item', 100);
// PRIMARY KEY 제약 조건 위반 오류 발생
2. 데이터 무결성 문제
같은 id
를 가진 객체가 여러 번 persist()
로 삽입되면 데이터가 중복 저장되어 데이터베이스의 무결성이 손상됩니다.
3. JPA 영속성 컨텍스트 문제
persist()
는 새로운 엔티티만 처리할 수 있기 때문에, 기존 엔티티를 persist()
하려고 하면 JPA의 영속성 컨텍스트에 일관성이 없거나 예외가 발생할 가능성이 높습니다.
4. 올바른 코드의 중요성
원래 코드:
public void save(Item item) {
if (item.getId() == null) {
em.persist(item); // 새로운 엔티티 추가
} else {
em.merge(item); // 기존 엔티티 수정
}
}
persist()
는 새로운 객체를 처리하며, SQLINSERT
문을 생성합니다.merge()
는 기존 객체를 처리하며, SQLUPDATE
문을 생성합니다.
새로운 객체와 기존 객체를 정확히 구분하여 처리할 수 있습니다.
5. 대안 및 개선
merge()
를 사용하지 않고 기존 객체도 처리하려면, 다음과 같이 명시적으로 flush()
및 영속성 컨텍스트 관리를 수행해야 할 수 있습니다.
// 비추천 방식
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.detach(item); // 기존 객체를 영속성 컨텍스트에서 분리
em.persist(item); // 새롭게 추가
}
}
이 방식은 권장되지 않으며, merge()
를 사용하는 것이 더 간단하고 효율적입니다.
6. 결론
persist()
는 새로운 객체만 처리할 수 있기 때문에, 기존 객체를 처리하는 데 사용하면 중복 기본 키 오류가 발생합니다.- 기존 객체를 수정하려면 반드시
merge()
를 사용해야 합니다. - JPA의 영속성 컨텍스트와 데이터베이스 무결성을 유지하기 위해 원래 코드처럼
persist()
와merge()
를 상황에 맞게 구분하여 사용하는 것이 가장 안전하고 올바른 방법입니다.
📝 준영속 엔티티란?
준영속 엔티티는 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 말합니다.
예를 들어:
- `
itemService.saveItem(book)
`에서 수정을 시도하는Book
객체는 준영속 엔티티입니다. Book
객체는 이미 데이터베이스에 한 번 저장되어 식별자(Id
)를 가지고 있습니다.- 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있다면 준영속 엔티티로 간주할 수 있습니다.
준영속 엔티티를 수정하는 2가지 방법
- 변경 감지 기능 사용
- 병합(
merge
) 사용
1. 변경 감지 기능 사용
변경 감지 기능은 영속성 컨텍스트에서 엔티티를 다시 조회한 후, 데이터를 수정하는 방식입니다. 트랜잭션이 커밋될 때 Dirty Checking(변경 감지)이 동작하여 데이터베이스에 UPDATE
SQL이 실행됩니다.
코드 예제:
@Transactional
void update(Item itemParam) { // itemParam: 준영속 상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId()); // 같은 엔티티를 조회
findItem.setPrice(itemParam.getPrice()); // 데이터 수정
}
동작 과정:
- 트랜잭션 안에서
em.find()
로 엔티티를 다시 조회합니다. - 조회한 영속 상태의 엔티티(
findItem
)에 변경할 값을 설정합니다. - 트랜잭션 커밋 시점에 JPA의 Dirty Checking이 동작하여 변경된 내용을
UPDATE
SQL로 반영합니다.
2. 병합(merge
) 사용
병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능입니다. 준영속 상태의 엔티티 데이터를 기반으로 새로운 영속 엔티티를 반환하며, 변경 사항을 데이터베이스에 반영합니다.
코드 예제:
@Transactional
void update(Item itemParam) { // itemParam: 준영속 상태의 엔티티
Item mergeItem = em.merge(itemParam);
}
동작 과정:
em.merge()
는 준영속 엔티티(itemParam
)를 기반으로 새로운 영속 엔티티(mergeItem
)를 생성합니다.- 생성된 영속 엔티티가 영속성 컨텍스트에서 관리됩니다.
- 트랜잭션 커밋 시점에 변경 사항이 데이터베이스에 반영됩니다.
변경 감지와 병합의 차이점
구분 | 변경 감지 기능 | 병합 |
---|---|---|
동작 방식 | 영속 상태의 엔티티를 직접 수정 | 준영속 엔티티를 기반으로 새로운 영속 엔티티를 생성 |
영속성 컨텍스트 | 조회한 영속 엔티티를 그대로 사용 | 새로운 영속 엔티티를 반환 |
적용 대상 | 기존 영속 상태의 엔티티 | 준영속 상태의 엔티티 |

병합(merge()
) 동작 방식
1. 병합 동작 상세
merge()
를 실행한다.- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 1차 캐시에 엔티티가 없으면, 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
- 조회한 영속 엔티티(
mergeMember
)에 준영속 엔티티(member
)의 값을 채워 넣는다.member
엔티티의 모든 값이mergeMember
에 덮어씌워진다.- 예:
mergeMember
의 이름 "회원1"이 "회원명변경"으로 바뀐다.
- 영속 상태인
mergeMember
를 반환한다.
참고: 책 자바 ORM 표준 JPA 프로그래밍 3.6.5
2. 병합 동작 방식 간단 정리
- 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
- 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다. (병합한다.)
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에
UPDATE
SQL이 실행된다.
3. 병합 사용 시 주의사항
- 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있다.
- 병합을 사용하면 모든 속성이 변경된다.
- 병합 시 값이 없으면
null
로 업데이트될 위험이 있다.- 병합은 모든 필드를 교체하기 때문이다.
'JPA' 카테고리의 다른 글
JPA EntityManagerFactory, EntityManager는 사용 후 꼭 닫아야 한다 (3) | 2024.12.05 |
---|---|
JPA 구동방식 (0) | 2024.12.05 |
hibernate entitymanager 의존성 (0) | 2024.12.05 |
도메인 주도설계와 OOP의 관계에 대한 chatGPT와의 질의응답 (0) | 2024.11.24 |
JPA FetchType.EAGER의 N+1 문제 & cascadeType.ALL 사용 이유 & 연관관계 편의 메서드 (0) | 2024.11.24 |
댓글