03b CORS·axios·@PathVariable 📚 전체 맵 09b @OneToMany·연관관계 매핑
Series 09 · JPA 완전 이해 · 1/3

영속성
컨텍스트
더티체킹

JPA의 핵심 — Entity 생명주기 · 영속성 컨텍스트 · 더티체킹
왜 save() 없이 UPDATE가 되는지 원리부터 이해하기

09a 영속성 컨텍스트·생명주기·더티체킹 09b @OneToMany·연관관계 매핑 09c 지연로딩·N+1·해결
01 Java 02 Spring 03 웹/통신 04~08 프로젝트·심화·DI 09 JPA 완전 이해 10 Bean·어노테이션 11 Docker
이 파일의 학습 목적
09a — JPA가 save() 없이 UPDATE를 하는 진짜 이유

@Transactional 메서드에서 Entity를 조회 후 setTitle()만 했는데 DB가 자동으로 UPDATE된다. 왜 그런지 이해하려면 영속성 컨텍스트(Persistence Context)와 더티체킹(Dirty Checking)을 알아야 한다. 09a는 JPA의 내부 동작 원리 — Entity 생명주기와 더티체킹 메커니즘을 정리한다.

이 파일에서 배울 5가지
  • ORM·JPA·Hibernate — 개념과 관계
  • 영속성 컨텍스트 — 1차 캐시·스냅샷·쓰기 지연
  • Entity 생명주기 — 비영속·영속·준영속·삭제
  • 더티체킹 — save() 없이 UPDATE 되는 원리
  • flush와 commit — SQL 전송 타이밍
프로젝트 코드 연결
  • · BoardService.update() — save() 없이 더티체킹
  • · @Transactional — 영속 상태 유지 조건
  • · getDetail() 조회수 증가 — 더티체킹 활용
  • · readOnly=true — 더티체킹 비활성화 이유
시리즈 순서: 09a ← 지금 여기 → 09b @OneToMany·연관관계 → 09c 지연로딩·N+1·해결 → 10 Bean·어노테이션
01 — JPA가 SQL을 대신한다는 진짜 의미
단순히 SQL 자동 생성 이상 — 객체와 DB 사이의 완충 레이어
🔄 ORM — Object Relational Mapping 핵심 개념
🌏 비유 — 자동 통역사
Java는 객체(Object)로 사고 → Board 객체, User 객체
DB는 테이블(Table)로 저장 → board 테이블, users 테이블
JPA(ORM) = 둘 사이 자동 통역사 → Java 코드 ↔ SQL 자동 변환
→ 개발자는 Java 객체만 다루면 됨, SQL은 JPA가 알아서
❌ JPA 없이 — SQL 직접 작성
String sql =
"INSERT INTO board(title,content,user_id)"
+ "VALUES(?,?,?)";
pstmt.setString(1, board.getTitle());
pstmt.setString(2, board.getContent());
pstmt.setLong(3, user.getId());
pstmt.executeUpdate();
// 필드 늘어날수록 SQL도 계속 수정
✅ JPA 사용 — 객체만
Board board = new Board();
board.setTitle(title);
board.setContent(content);
board.setUser(user);
boardRepository.save(board);
// JPA가 INSERT SQL 자동 생성
// 필드 추가해도 SQL 수정 불필요
💡 JPA = 인터페이스 / Hibernate = 실제 구현체
Spring Boot의 spring-boot-starter-data-jpa 의존성 하나로 모두 포함
→ 우리가 쓰는 JPA 기능은 실제로 Hibernate가 실행함
02 — 영속성 컨텍스트
JPA의 핵심 — Entity를 관리하는 1차 캐시 · 더티체킹의 근거
🗃️ Persistence Context — JPA의 임시 저장소 JPA Core
📋 비유 — 회사 메모판
DB = 회사 서버 (느리지만 영구 저장)
영속성 컨텍스트 = 내 책상 메모판 (빠르고 임시)
→ DB에서 꺼낸 Entity는 메모판에 올려둠
→ 수정하면 메모판에서 먼저 변경
→ 트랜잭션 끝날 때 메모판 → DB에 한 번에 반영 (flush)
영속성 컨텍스트 (1차 캐시)
트랜잭션 범위 안에서 Entity를 임시 보관하는 Map
Key: Board@id=1
Board{title="안녕", content="..."}
Key: User@id=1
User{username="hong", ...}
스냅샷: Board@id=1
Board{title="안녕"} ← 원본
변경 감지 (더티체킹)
title 변경 감지 → UPDATE 예약
흐름 — 영속성 컨텍스트 동작 원리
// 1. DB에서 조회 → 영속성 컨텍스트에 저장 (1차 캐시)
Board board = boardRepository.findById(1L).get();
// → SELECT * FROM board WHERE id=1 실행
// → board 객체를 영속성 컨텍스트에 보관
// → 동시에 스냅샷(원본 복사본)도 보관

// 2. 같은 id 다시 조회 → DB 안 가고 캐시에서 반환
Board board2 = boardRepository.findById(1L).get();
// → SQL 실행 안 함! 캐시에서 바로 반환
// → board == board2 → true (같은 객체)

// 3. 트랜잭션 끝 → flush 자동 실행
// → 스냅샷 vs 현재 상태 비교
// → 다르면 UPDATE SQL 자동 실행 (더티체킹)
💡 영속성 컨텍스트가 제공하는 4가지 기능
1. 1차 캐시 — 같은 트랜잭션 내 같은 Entity 재조회 시 DB 안 감
2. 더티체킹 — save() 없이 변경사항 자동 감지 → UPDATE 실행
3. 지연 로딩 — LAZY 관계는 실제 접근할 때 SQL 실행
4. 쓰기 지연 — INSERT/UPDATE를 모았다가 flush 시 한 번에 실행
03 — Entity 생명주기
비영속 → 영속 → 준영속 → 삭제 — 영속성 컨텍스트와의 관계
🔄 4가지 상태 Lifecycle
비영속
new로 만든 직후
JPA 모름
영속 ⭐
영속성 컨텍스트가
관리하는 상태
준영속
컨텍스트에서
분리된 상태
삭제
DB에서
삭제 예약
new Board() → 비영속
|
save() / findById() → 영속
|
트랜잭션 종료 → 준영속
|
delete() → 삭제
Java — 생명주기 상태 변화 코드
// 1. 비영속 — JPA가 전혀 모르는 상태
Board board = new Board();
board.setTitle("새 게시글");
// 영속성 컨텍스트와 무관, DB와도 무관

// 2. 영속 — save() 또는 findById() 후
boardRepository.save(board); // INSERT + 영속 상태로 전환
// 또는
Board found = boardRepository.findById(1L).get();
// → 영속성 컨텍스트가 관리 시작, 스냅샷 보관

// 3. 준영속 — @Transactional 메서드 종료 후
// → 트랜잭션 끝나면 영속성 컨텍스트 소멸
// → 이 상태에서 변경해도 DB 반영 안 됨!

// 4. 삭제
boardRepository.delete(board);
// → DELETE SQL 예약 → flush 시 실행
⚠️ 준영속 상태 — 가장 많이 실수하는 부분
@Transactional 메서드 밖에서 Entity 수정 → DB에 반영되지 않음
더티체킹은 영속 상태에서만 작동
→ 수정 기능에는 반드시 @Transactional 필요
04 — 더티체킹 (Dirty Checking)
save() 없이 자동 UPDATE — 스냅샷 비교 원리
🔍 변경 자동 감지 Dirty Checking
📸 비유 — 입사 전 사진 vs 현재
입사할 때 증명사진 찍어둠 (스냅샷)
퇴근 전에 현재 모습과 비교 → 달라졌으면 자동 감지
→ JPA가 스냅샷 vs 현재 Entity 비교 → 다르면 UPDATE 자동 실행
❌ save() 직접 호출 (불필요)
@Transactional
public void update(Long id, BoardDto dto) {
Board board =
boardRepository.findById(id).get();
board.setTitle(dto.getTitle());
board.setContent(dto.getContent());
boardRepository.save(board); // 불필요!
// 동작은 하지만 중복 호출
}
✅ 더티체킹 활용 — save() 생략
@Transactional // 필수!
public void update(Long id, BoardDto dto) {
Board board =
boardRepository.findById(id).get();
board.setTitle(dto.getTitle());
board.setContent(dto.getContent());
// save() 없음!
// @Transactional 끝날 때
// 자동으로 UPDATE 실행됨
}
흐름 — 더티체킹 내부 동작 순서
// @Transactional 시작 → 영속성 컨텍스트 생성

// 1. findById() → DB 조회 → 영속 상태
Board board = boardRepository.findById(1L).get();
// 스냅샷 저장: Board{title="안녕", content="반가워"}

// 2. 값 변경
board.setTitle("수정된 제목");
// 현재 상태: Board{title="수정된 제목", content="반가워"}
// 스냅샷: Board{title="안녕", content="반가워"}

// 3. @Transactional 끝 → flush 자동 실행
// JPA: 스냅샷 vs 현재 비교 → title 달라짐!
// → UPDATE board SET title='수정된 제목' WHERE id=1
// → 자동 실행 (save() 없이)
⚠️ @Transactional 없으면 더티체킹 작동 안 함
트랜잭션이 없으면 영속성 컨텍스트가 생성되지 않음
→ Entity를 수정해도 스냅샷 비교가 일어나지 않음 → DB 반영 안 됨
수정·삭제 Service 메서드에는 반드시 @Transactional
05 — flush와 commit
영속성 컨텍스트 → DB 반영 타이밍
💾 flush = SQL 전송 / commit = DB 확정 Transaction
🏦 비유 — 은행 거래
flush = 창구에 "이체해주세요" 요청 전송 (아직 취소 가능)
commit = 거래 최종 확정 (이제 되돌릴 수 없음)
rollback = 거래 취소 (예외 발생 시 자동 실행)
동작설명자동 실행 시점
flush영속성 컨텍스트 변경사항을 DB로 SQL 전송@Transactional 끝날 때
commit트랜잭션 확정 — DB에 영구 저장flush 직후
rollback예외 발생 시 모든 변경 취소예외 발생 시 자동
Java — @Transactional 전체 흐름 + rollback
@Transactional
public void writeBoard(BoardDto dto) {
// ① 트랜잭션 시작 → 영속성 컨텍스트 생성
Board board = new Board();
board.setTitle(dto.getTitle());
boardRepository.save(board);
// ② INSERT SQL 쓰기 지연 버퍼 저장 (아직 DB 미전송)
// ③ 정상 종료 → flush → INSERT SQL DB 전송
// ④ commit → DB 영구 저장
}

// 예외 발생 → rollback
@Transactional
public void transfer() {
accountA.withdraw(10000); // A에서 차감
throw new RuntimeException(); // 예외!
accountB.deposit(10000); // 실행 안 됨
// → rollback 자동 실행 → A 차감도 취소됨
}
03b CORS·axios·@PathVariable 📚 전체 맵 09b @OneToMany·연관관계 매핑