| Flush 모드 | 쿼리 실행 전 Flush | 커밋 전 Flush | 설명 |
|---|---|---|---|
| AUTO (JPA 기본) | O | O | 조회성 쿼리 전·커밋 전 자동 플러시 |
| COMMIT | X | O | 커밋 시에만 플러시 |
| ALWAYS (Hibernate) | O | O | 모든 쿼리 전 무조건 플러시 |
| MANUAL (Hibernate) | X | X | 오직 flush() 호출 시에만 플러시 |
@Service
class UserService(
private val userRepo: UserRepository,
private val em: EntityManager
) {
@Transactional
fun removeAndRecreate(username: String) {
// 1) 기존 사용자 조회
val existing = userRepo.findByUsername(username)
?: throw IllegalArgumentException("No user: $username")
// 2) 삭제 → DELETE Action 등록
userRepo.delete(existing)
// 3) 곧바로 같은 username 으로 새 사용자 저장 → INSERT Action 등록
userRepo.save(User(username = username))
// **의도**: DELETE → INSERT 순
// **실제**: INSERT → DELETE 순으로 ActionQueue가 실행 → UNIQUE 위반
}
}INSERT INTO app_user (username) VALUES ('alice'); # ③ save()의 INSERT Action
/* DELETE FROM app_user WHERE id = ? */ # ② 기존 delete()
@Transactional
fun renameAndRecreateFixed(originalTitle: String) {
val book = bookRepo.findByTitle(originalTitle)!!
// 1) 제목 변경 → UPDATE Action
book.title = "Renamed"
// 2) 즉시 DB 반영
em.flush()
// 3) 안전하게 INSERT
bookRepo.save(Book(title = originalTitle))
}@Transactional
fun cloneBookWithSameTitle(originalTitle: String) {
val old = bookRepo.findByTitle(originalTitle)!!
// 새 객체를 완전히 분리된 상태로 복제
val copy = Book(title = old.title)
bookRepo.save(copy)
}@Service
class UserService(
private val userRepo: UserRepository,
private val profileService: ProfileService
) {
/**
* 상위 트랜잭션: readOnly = false, FlushMode.AUTO
*/
@Transactional
fun updateUsername(userId: Long, newName: String) {
// 1) 사용자 읽기 (Dirty 체크 대상)
val user = userRepo.findById(userId)
.orElseThrow { IllegalArgumentException("No user: $userId") }
user.name = newName // UPDATE Action 등록
// 2) 프로필 조회 (readOnly = true) 호출
// 하지만 같은 TX 에 합류하므로 FlushMode.AUTO 유지 → 조회 직전에 flush() 발생
val profile = profileService.getProfileForAudit(userId)
println("Fetched profile: $profile")
// 3) 동일 TX 내에서 다른 변경
userRepo.save(user) // INSERT/UPDATE Action 추가
// 트랜잭션 커밋 시점에도 남은 ActionQueue flush → 하지만 이미 flush가 먼저 일어남
}
}
@Service
class ProfileService(
private val profileRepo: ProfileRepository
) {
/**
* 의도: readOnly=true → FlushMode.MANUAL
* 실제: 상위 TX가 AUTO 이므로 AUTO 유지
*/
@Transactional(readOnly = true)
fun getProfileForAudit(userId: Long): Profile {
// 이 조회 직전에 자동으로 flush()가 호출된다!
return profileRepo.findByUserId(userId)
?: throw IllegalArgumentException("No profile for user: $userId")
}
}UPDATE user SET name = 'newName' WHERE id = :userId;
SELECT * FROM profile WHERE user_id = :userId;-- updateUsername TX start (FlushMode=AUTO)
SELECT * FROM user WHERE id = ? // findById
# Dirty: user.name 변경
-- getProfileForAudit 호출, same TX (intended MANUAL, but remains AUTO)
# 조회 직전 자동 flush()
UPDATE user SET name='newName' WHERE id=? // flush before SELECT
SELECT * FROM profile WHERE user_id=? // readOnly method
-- 돌아와서 save()
# (추가 Action)
UPDATE user SET name='newName' WHERE id=? // may be skipped if already flushed
-- commit TX
-- End of transaction
/**
* 완전 별도 트랜잭션, readOnly=true → FlushMode.MANUAL
*/
@Transactional(
readOnly = true,
propagation = Propagation.REQUIRES_NEW
)
fun getProfileForAudit(userId: Long): Profile {
return profileRepo.findByUserId(userId)
?: throw IllegalArgumentException("No profile for user: $userId")
}