이 파일의 학습 목적
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();
✅ JPA 사용 — 객체만
Board board = new Board();
board.setTitle(title);
board.setContent(content);
board.setUser(user);
boardRepository.save(board);
💡 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 예약
흐름 — 영속성 컨텍스트 동작 원리
Board board = boardRepository.findById(1L).get();
Board board2 = boardRepository.findById(1L).get();
💡 영속성 컨텍스트가 제공하는 4가지 기능
1. 1차 캐시 — 같은 트랜잭션 내 같은 Entity 재조회 시 DB 안 감
2. 더티체킹 — save() 없이 변경사항 자동 감지 → UPDATE 실행
3. 지연 로딩 — LAZY 관계는 실제 접근할 때 SQL 실행
4. 쓰기 지연 — INSERT/UPDATE를 모았다가 flush 시 한 번에 실행
03 — Entity 생명주기
비영속 → 영속 → 준영속 → 삭제 — 영속성 컨텍스트와의 관계
🔄
4가지 상태
Lifecycle
new Board() → 비영속
|
save() / findById() → 영속
|
트랜잭션 종료 → 준영속
|
delete() → 삭제
Java — 생명주기 상태 변화 코드
Board board = new Board();
board.setTitle("새 게시글");
boardRepository.save(board);
Board found = boardRepository.findById(1L).get();
boardRepository.delete(board);
⚠️ 준영속 상태 — 가장 많이 실수하는 부분
@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());
}
흐름 — 더티체킹 내부 동작 순서
Board board = boardRepository.findById(1L).get();
board.setTitle("수정된 제목");
⚠️ @Transactional 없으면 더티체킹 작동 안 함
트랜잭션이 없으면 영속성 컨텍스트가 생성되지 않음
→ Entity를 수정해도 스냅샷 비교가 일어나지 않음 → DB 반영 안 됨
→ 수정·삭제 Service 메서드에는 반드시 @Transactional
📌 우리 프로젝트 더티체킹 사용 위치
BoardService.update() — 게시글 수정 시 save() 없이 @Transactional만
BoardService.getDetail() — 조회수 증가 시 더티체킹 활용
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);
}
@Transactional
public void transfer() {
accountA.withdraw(10000);
throw new RuntimeException();
accountB.deposit(10000);
}