๑ `⌃´ ๑
@Transactional 시작
    ↓
Entity 조회
    ↓
Entity가 영속 상태가 됨
    ↓
Entity의 값 변경
    ↓
JPA가 변경을 감지
    ↓
flush
    ↓
UPDATE SQL 실행
    ↓
commit

 

1. JPA에서 `findById()`를 하면 무슨 일이 일어날까?

평소에는 보통 이렇게 생각한다.

Product product = productRepository.findById(productId).orElseThrow();

단순하게 보면 `DB에서 Product를 가져온다`고 생각할 수 있다.

그런데 JPA에서는 한 가지 일이 더 있다.

조회한 `Product` 객체를 영속성 컨텍스트가 관리하기 시작할 수 있다.

Database
   │
   │ SELECT
   ▼
Product Entity
   │
   ▼
영속성 컨텍스트가 관리

이때 `Product`를 영속 상태(Persistent State)라고 한다.

 

 

2. 영속성 컨텍스트

JPA가 Entity를 관리하는 공간 또는 관리 영역

 

예를 들어, 트랜잭션 안에서 아래의 코드를 실행한다고 하자.

Product product = productRepository.findById(1L).orElseThrow();
영속성 컨텍스트

┌──────────────────────────┐
│                          │
│ Product                  │
│ id = 1                   │
│ stock = 10               │
│                          │
└──────────────────────────┘

이때 개념적으로 JPA가 이 객체를 관리한다.

JPA가 그냥 DB에서 객체를 만들어서 우리에게 던지고 완전히 잊어버리는 것이 아니다.

 

 

3. 그러면 "관리한다"는 정확히 무엇인가?

처음 조회했을 때의 값과 현재 값을 비교할 수 있도록 Entity의 상태를 추적한다.

 

예를 들어 조회 당시에 `id = 1`, `stock = 10`이었다 치자.

그리고 `product.decreaseStock();`을 호출한다.

그러면 Java 객체는 `id = 1`, `stock = 9`가 된다.

처음 관리하기 시작했을 때
stock = 10

현재
stock = 9
→ 값이 달라졌다.

 

 

4. Dirty Checking

JPA가 관리 중인 Entity의 값이 변경되었는지 확인하는 것

 

조회 시점
stock = 10
	↓
product.decreaseStock()
	↓
현재 상태
stock = 9
	↓
JPA
"10이었는데 9가 됐네?"
	↓
변경 감지

 

예시

`Product`

public void decreaseStock() {
    if (stock <= 0) {
        throw new IllegalStateException("재고가 부족합니다.");
    }

    stock--;
}

 

Test

Product product = new Product(10);

product.decreaseStock();

assertThat(product.getStock()).isEqualTo(9);

위 코드에서는 단순히 메모리의 Java 객체만 바뀌었다.

그런데 JPA 가 관리하는 Entity라면, `Java 객체 변경 + JPA가 그 객체를 관리 중`이라는 조건이 추가된다.

그러면 JPA가 "관리하고 있던 객체가 바뀌었네."를 알아낼 수 있다.

 

 

5. 그래서 왜 `save()`가 없어도 되는가?

@Transactional
public void decrease(Long productId) {
    Product product = productRepository.findById(productId)
            .orElseThrow();

    product.decreaseStock();
}

 

실행흐름

① @Transactional
   트랜잭션 시작
	↓
② findById(productId)
   DB에서 Product 조회
	↓
③ 조회한 Product가 영속성 컨텍스트의 관리 대상이 됨
	↓
④ product.decreaseStock()
   stock
   10 → 9
	↓
⑤ 메서드 정상 종료
	↓
⑥ 트랜잭션 commit 과정
	↓
⑦ JPA가 변경된 Entity 확인
   10 → 9 발견
	↓
⑧ UPDATE SQL 실행
	↓
⑨ COMMIT

 

그래서 별도의 `productRepository.save(product);`가 없어도 변경이 DB에 반영될 수 있다.

이걸 흔히 "변경 감지에 의해 UPDATE 된다"라고 이야기한다.

 

 

6. 여기서 `save()`의 역할을 오해하면 안 된다

반대로 "그럼 JPA에서는 `save()`가 필요 없는 거네?"라고 생각할 수 있다.

예를 들어 새로운 Entity를 처음 저장한다면,

Product product = new Product(...);

productRepository.save(product);

위와 같은 과정이 필요할 수 있다.

지금 말하는 것은 이미 영속 상태인 Entity를 수정하는 경우이다.

핵심 조건은 `이미 JPA가 관리하고 있는 Entity + 트랜잭션 안에서 상태 변경`이다.

 

 

7. `flush`

영속성 컨텍스트의 변경 내용을 DB와 동기화하는 과정
영속성 컨텍스트

Product
stock = 9

↓

flush

↓

Database

UPDATE product
SET stock = 9
WHERE id = 1;

 

여기서 중요한 점은, `flush`는 "트랜잭션을 끝낸다"는 뜻이 아니다.

 

 

8. `flush`와 `commit`은 다르다

flush

JPA가 변경 SQL을 DB에 보낸다.

 

commit

트랜잭션 변경 내용을 최종 확정한다.

 

따라서 개념적으로 아래와 같은 상황도 가능하다.

Entity 변경
	↓
flush
	↓
UPDATE SQL 실행
	↓
DB에는 UPDATE가 전달됨
하지만 아직 트랜잭션 진행 중
	↓
문제 발생
	↓
ROLLBACK
	↓
최종적으로 변경 취소

 

SQL이 실행됐다는 것과 트랜잭션이 Commit 됐다는 것은 같은 말이 아니다.

 

 

9. 시간 흐름으로 보면

@Transactional
public void order() {

    product.decreaseStock();

    // 어떤 시점에 flush

    throw new IllegalStateException();
}
stock = 10
	↓
Java Entity
stock = 9
	↓
flush
	↓
UPDATE stock = 9
DB에 SQL 실행
	↓
예외 발생
	↓
ROLLBACK
	↓
DB 최종 결과
stock = 10

 

그러므로 UPDATE SQL  로그가 찍혔다고 해서 DB에 최종 저장됐다고 판단하면 안 된다.

 

 

10. 그러면 flush는 언제 발생하는가?

JPA는 여러 상황에서 flush를 할 수 있다.

 

트랜잭션 Commit 직전

@Transactional 메서드 정상 종료
	↓
트랜잭션 commit 준비
	↓
flush
	↓
필요한 INSERT / UPDATE / DELETE 실행
	↓
commit

위와 같은 흐름이 발생하고, 그리서 우리가 `product.decreaseStock();`만 해놓고 `save()`를 안 해도 트랜잭션 종료 과정에서 변경사항이 DB로 전달될 수 있다.

 

 

11. `flush()`를 직접 호출할 수도 있다

JPA에는 명시적으로 flush하는 기능도 있다.

// 개념적
entityManager.flush();

// Spring Data JPA에서 상황에 따라
repository.flush();

 

하지만 지금은 "변경 즉시 `flush()`를 호출해야 한다"라고 이해하면 안 된다.

대부분 JPA가 필요한 시점에 처리한다.

 

 

12. 영속성 컨텍스트는 왜 이런 일을 할까?

만약 JPA가 없다면 일반적인 SQL 기반 접근(ex: MyBatis)에서는 우리가 직접 생각해야 한다.

SELECT Product
	↓
Java에서 stock 계산
	↓
UPDATE SQL 직접 호출

 

JPA는 조금 다른 철학을 사용한다.

우리가 `Product 객체의 상태를 변경했다.`에 집중하면, JPA가 그 변경을 추적해서 `UPDATE가 필요 한지 판단`한다.

그래서 객체 중심으로 코드를 작성하기 편해진다.

 

 

13. MyBatis와 비교하면

Product product = mapper.selectProduct(productId);

product.decreaseStock();

mapper.updateProduct(product);
조회 → 객체 변경 → UPDATE 명시적 실행

기존 MyBatis 스타일에서는 보통 위와 같은 흐름을 생각하기 쉽다.

 

하지만 JPA 영속 상태에서는 아래와 같이 될 수 있다.

Product product = productRepository.findById(productId).orElseThrow();

product.decreaseStock();
조회 → 객체 변경 → JPA 변경 감지 → UPDATE

 

 

14. 그런데 모든 Java 객체가 Dirty Checking 대상인가?

Product product = new Product(10);
product.decreaseStock();

그냥 위 코드를 실행했다고 해서 갑자기 DB에 UPDATE를 보내는 것이 아니다.
그 `Product`가 "현재 영속성 컨텍스트가 관리하고 있는 Entity인가?"가 중요하다.

 

그냥 new로 만든 평범한 객체
→ JPA가 모를 수 있음
→ Dirty Checking X

JPA를 통해 조회해서 영속성 컨텍스트가 관리 중인 Entity
→ Dirty Checking 가능

 

 

15. Entity의 상태를 조금만 알아보면

JPA에서는 Entity의 상태를 여러 가지로 구분한다.

대표적으로 두 가지만 알아보면...

 

비영속 상태

JPA가 관리하지 않는 객체.

Product product = new Product(...);

단순히 `new`만 한 상태를 생각하면 된다.

JPA: "저 객체가 있는지도 몰라."

 

영속 상태

JPA가 관리하는 Entity

Product product = productRepository.findById(id).orElseThrow();

JPA로 조회된 Entity가 현재 영속성 컨텍스트에 관리되고 있다면 영속 상태로 볼 수 있다.

JPA: "이 Product 내가 관리하고 있어."

 

 

16. 왜 `@Transactional`과 같이 설명하나?

Spring에서 일반적인 JPA 사용에서는 트랜잭션 범위와 영속성 컨텍스트의 관리 범위가 밀접하게 연결된다.

@Transactional
public void decrease(Long productId) {

    Product product = productRepository.findById(productId).orElseThrow();

    product.decreaseStock();
}
@Transactional 시작
│
├─ Product 조회
│
├─ Product 영속 상태
│
├─ stock 변경
│
├─ Dirty Checking
│
├─ flush
│
└─ commit

위와 같은 흐름을 생각할 수 있고,

그래서 서비스 메서드가 종료될 때 변경 사항이 반영된다.

 

 

17. `@Transactional`이 없다면?

여기서는 주의해야 한다.

단순히 "`@Transactional` 없으면 Dirty Checking은 절대로 안 된다."라고 외우면 안 된다.

실제 동작은 Repository 메서드의 트랜잭션, 영속성 컨텍스트 범위 등 여러 조건에 영향을 받는다.

@Transactional
public void decrease(...) {
    ...
}

하지만 우리가 작성하는 비즈니스 변경 서비스 코드에서는, 명시적인 트랜잭션 경계 안에서 Entity를 조회하고 변경하는 패턴이 일반적이다.

 

 

18. 요약도

Client
  ↓
Controller
  ↓
ProductService.decrease()

@Transactional 시작
───────────────────────────────

  ↓
productRepository.findById()
  ↓
SELECT product
  ↓
Product Entity 반환
stock = 10
  ↓
영속성 컨텍스트가 Product 관리
  ↓
product.decreaseStock()
stock = 10 → 9
  ↓
Service 메서드 종료
  ↓
JPA Dirty Checking
"stock이 10에서 9로 바뀌었네"
  ↓
flush
UPDATE product
SET stock = 9
WHERE id = ?
  ↓
commit
───────────────────────────────
@Transactional 종료

 

그리고 여기에는 `productRepository.save(product);`가 없다.

 

 

19. 프레임워크가 해주는 것과 우리가 하는 것

우리가 작성하는 것

Product product = productRepository.findById(...).orElseThrow();

product.decreaseStock();
@Transactional

업무의 트랜잭션 경계를 표현한다.

 

Product가 하는 것

product.decreaseStock();

내부에서 `재고 부족 검사`, `재고 차감`이라는 비즈니스 규칙을 수행한다.

 

JPA가 하는 것

Entity 관리
변경 상태 추적
Dirty Checking
필요한 UPDATE SQL 생성 및 실행

 

Spring이 하는 것

@Transactional 경계 관리
트랜잭션 시작
정상 종료 시 commit
Rollback 조건 발생 시 rollback

 

// 이 코드 보다는
product.setStock(product.getStock() - 1);

// 이 코드가 더 나음
product.decreaseStock();
// 비즈니스 행동을 표현

이제 JPA까지 연결하면, 아래와 같이 된다.

Product가 자기 규칙에 따라 자기 상태를 변경 → JPA는 그 상태 변경을 감지 → DB UPDATE

 

즉 코드에서는 `product.decreaseStock();`라는 도메인 행동에 집중하고, DB 반영은 Dirty Checking이 도와주는 구조가 가능하다.

 

 

20. 예제

1) 다음 코드를 보고 실행 흐름을 적어본다.

@Transactional
public void decrease(Long productId) {
    Product product = productRepository.findById(productId)
            .orElseThrow();

    product.decreaseStock();
}
① @Transactional
→ 트랜잭션 시작

② findById()
→ SELECT
→ Product가 영속 상태가 됨

③ 조회된 Product
→ 어떤 상태? 영속성 컨텍스트의 관리 대상이 됨

④ decreaseStock()
→ Java 객체의 stock: 10 → 9

⑤ 메서드 정상 종료
→ Dirty Checking

⑥ flush
→ UPDATE SQL 실행

⑦ commit
→ DB 변경 최종 확정

 

2) 다음 문장을 각각 `맞다 / 틀리다`로 판단하고 이유를 적어본다.

A

`product.decreaseStock()`을 호출하는 순간 반드시 UPDATE SQL이 바로 실행된다.

 

- 틀림. product.decreaseStock()은 우선 영속 Entity의 Java 객체 상태를 변경할 뿐이며, UPDATE SQL은 보통 이후 flush 시점에 실행되므로 메서드를 호출한 순간 반드시 UPDATE가 실행되는 것은 아니다.

 

B

flush가 실행되어 UPDATE SQL이 DB에 전달됐다면 이후에는 rollback할 수 없다.

 

- 틀림. flush는 SQL을 DB에 실행해 영속성 컨텍스트와 DB 상태를 동기화하는 과정이지 트랜잭션을 확정하는 과정은 아니므로, 이후 트랜잭션이 Rollback되면 해당 변경도 취소될 수 있다.

 

C

JPA가 관리 중인 Entity의 값을 변경하면, 트랜잭션 종료 과정에서 Dirty Checking을 통해 UPDATE가 수행될 수 있으므로 수정 목적으로 `save()`를 다시 호출하지 않아도 되는 경우가 있다.

 

- 맞음. Repository에서 조회한 Entity가 영속성 컨텍스트의 관리 대상인 영속 상태이기 때문에 상태 변경을 JPA가 감지할 수 있다.

 

 

3) ` 초기 DB 재고 = 10 `일 때, 아래 코드를 참고하여 답을 작성한다.

@Transactional
public void order() {

    Product product = productRepository.findById(1L)
            .orElseThrow();

    product.decreaseStock();

    orderRepository.save(order);

    throw new IllegalStateException("주문 처리 실패");
}
메서드 내부에서 product.decreaseStock() 후 Java 객체의 stock 값은?
- 9
최종적으로 rollback이 정상적으로 이루어졌다면 DB의 stock 값은?
- 10
왜 두 값이 달라질 수 있는가?
- `product.decreaseStock()`이 실행되면서 메모리에 있는 Java 객체의 stock은 9로 변경된다. 하지만 DB 트랜잭션이 Rollback되면 DB에 적용하려던 변경은 취소되므로 DB의 stock은 원래 값인 10으로 유지된다. Rollback은 Java 객체의 값을 과거 상태로 되돌리는 기능이 아니라 DB 트랜잭션의 변경을 취소하는 기능이다.

 

 

정리

  • 영속성 컨텍스트는 JPA가 Entity를 관리하는 영역이며, 관리 중인 Entity를 영속 상태라고 볼 수 있다.
  • 영속 Entity의 상태가 바뀌면 JPA가 Dirty Checking으로 변경을 감지하고 flush 과정에서 UPDATE SQL을 실행할 수 있다.
  • flush는 SQL을 DB와 동기화하는 과정이고 commit은 트랜잭션 결과를 최종 확정하는 것이므로 같은 개념이 아니다.
🎵 Playlist
loading...
00:00 / 00:00