더티 체킹(Dirty Checking)
더티 체킹(Dirty Checking)이란 객체 지향 프로그래밍 환경에서 객체의 상태가 변경되었는지 감지하여 데이터베이스에 자동 반영하는 메커니즘이다. 즉, 개발자가 직접 SQL UPDATE 명령을 작성하지 않아도, 객체의 속성을 변경한 후 트랜잭션을 커밋하면 자동으로 변경 사항이 DB에 반영되는 기능이다.
주로 JPA(Java Persistence API), Hibernate, SQLAlchemy 등 ORM(Object-Relational Mapping) 프레임워크에서 사용된다. 더티 체킹은 Python 환경에서도 사용되긴 하지만 주로 Spring Boot + JPA(Hibernate) 환경에서 가장 많이 사용되는 패턴 중 하나이다. 실무에서는 더티 체킹을 사용할 때 대부분 Spring Boot 환경에서 @Transactional, Entity 변경, 자동 UPDATE 흐름으로 사용한다.
전통적인 DB 프로그래밍에서는 데이터 변경이 일어나면 다음과 같은 과정을 거쳐야 했다.
- 데이터를 조회한다.
- 필요한 값을 변경한다.
- 변경된 내용을 SQL UPDATE로 직접 DB에 반영한다.
하지만 ORM에서는 다음과 같은 방식으로 처리된다.
- 객체를 조회한다.
- 객체의 필드를 수정한다.
- 트랜잭션을 커밋하면 ORM이 변경된 필드를 자동 감지하여 DB에 업데이트한다.
이때 2와 3 사이의 과정을 더티 체킹(Dirty Checking)이라고 한다.
Dirty Checking의 내부 작동 방식 순서는 다음과 같다.
- ORM은 영속 상태의 객체의 최초 상태(스냅샷)를 메모리에 저장해 둔다.
- 트랜잭션 커밋 시점에 현재 상태와 스냅샷을 비교한다.
- 변경된 필드가 있으면 UPDATE 쿼리를 생성하여 DB에 반영한다.
이로 인해 개발자는 직접 UPDATE 쿼리를 작성하지 않고도 변경을 안전하게 반영할 수 있으며, 비즈니스 로직에 집중할 수 있는 장점이 있다.
단, 더티 체킹은 객체가 영속(persistent) 상태일 때만 작동한다. 이 부분은 JPA의 Entity의 Lifecycle과 관련된 부분인데 예제 이후 다시 살펴보자.
예제 1(Java - JPA 기준)
@Transactional
public void updateUsername(Long userId, String newName) {
User user = em.find(User.class, userId); // 영속 상태
user.setUsername(newName); // 값 변경 (더티 상태)
// 트랜잭션 커밋 시 자동으로 UPDATE 쿼리 실행
}
위 코드에서 user.setUsername(newName) 호출은 메모리 상 객체 값을 변경한 것이며, 이 시점에서는 DB에 아무 영향도 없다. 그러나 트랜잭션이 커밋되면, JPA는 해당 객체의 초기 상태(snapshot)와 현재 상태를 비교하여 변경된 필드를 감지하고, 그에 맞는 SQL UPDATE문을 자동 생성하여 실행한다.
예제 2(Python - SQLAlchemy 기준)
from sqlalchemy.orm import Session
with Session(engine) as session:
user = session.get(User, 1)
user.name = "new_name" # 값 변경
session.commit() # 더티 체킹 → UPDATE 쿼리 자동 실행
SQLAlchemy 역시 JPA와 동일한 방식으로 객체의 변경을 추적하고, 커밋 시 DB에 반영한다. 변경이 없는 경우에는 UPDATE 쿼리를 실행하지 않으며, 이를 스냅샷 기반 변경 감지라고 한다.
JPA의 Entity(엔티티) Lifecycle
JPA에서는 엔티티 객체의 상태를 아래 네 가지로 구분한다. 이 상태에 따라 더티 체킹의 동작에 대해 주의할 점이 있다.
- New(Transient, 비영속): 아직 EntityManager에 저장되지 않은 객체. DB와 전혀 연관 없음
- Persistent(영속): EntityManager가 관리 중인 상태. DB와 연결됨
- Detached(준영속): 한때 영속이었지만, 현재는 EntityManager가 관리하지 않음
- Removed(삭제 예정): 삭제가 예정된 상태. 트랜잭션 커밋 시 DB에서 삭제됨
각 상태를 코드로 간단하게 살펴보면 다음과 같다.
비영속(Transient)
Transient 상태는 아직 DB에 저장되지 않았고, EntityManager가 관리하지 않는 상태를 말한다.
User user = new User("new name"); // 아직 persist 안됨
영속(Persistent)
Persistent 상태는 EntityManager.find() 또는 JPQL로 조회한 엔티티는 영속 상태가 된다.
User user = em.find(User.class, 1L); // 영속 상태
user.setUsername("new name"); // 변경
em.flush(); // 변경 감지 → UPDATE
준영속(Detached)
Detached 상태는 EntityManager가 더 이상 해당 객체를 추적하지 않기 때문에, 커밋 시에도 아무 쿼리도 발생하지 않는다.
User user = em.find(User.class, 1L);
em.detach(user); // 이제 더 이상 관리하지 않음
user.setUsername("change"); // 변경해도 감지 안 됨
삭제 예정(Removed)
Removed 상태는 JPA에서 EntityManager.remove()를 호출한 엔티티가 갖게 되는 상태이다. 따라서 이 상태의 엔티티는 트랜잭션 커밋 시 실제 DB에서 삭제되며, 더티 체킹은 무시되고 삭제 쿼리만 발생한다.
User user = em.find(User.class, 1L); // 영속 상태
em.remove(user); // Removed 상태 전환
user.setUsername("change"); // 무시됨, 더티 체킹 비활성
Lifecycle Flow
이 JPA Entity의 4가지 상태를 도식화하면 다음과 같이 나타낼 수 있다.
이 상태들을 하나의 흐름으로 코드를 구성해보면 다음과 같다.
@Transactional
public void lifecycleDemo() {
// 1. 비영속
User user = new User("kim");
// 2. 영속
em.persist(user);
// 3. 준영속
em.detach(user); // 또는 em.clear();
// 4. 다시 영속
em.merge(user);
// 5. 삭제 예정
em.remove(user);
}
이 4가지 상태 중 더티 체킹은 객체가 영속(persistent) 상태일 때만 작동한다. 즉, 비영속(transient) 상태나 준영속(detached) 상태에서는 더티 체킹이 일어나지 않는다. 또한 성능 최적화를 위해 변경이 많거나 조건이 복잡한 경우에는 직접 SQL을 작성하거나 벌크 연산을 고려하는 것이 바람직하다.
참고 자료
https://www.javaguides.net/2019/01/jpa-entity-object-life-cycle-new-managed-removed-detached.html
https://jojoldu.tistory.com/415