๐ org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: ... entity.Folder.children could not initialize proxy - no Session
๊ฐ์ธ์ ์ผ๋ก ์งํ์ค์ธ ํ๋ก์ ํธ์์ JPA ์ฐ๊ด๊ด๊ณ ํ ์ด๋ธ์ ์กฐํ & ์ญ์ ํ๋ ๊ณผ์ ์์ ์์ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.
๊ด๋ จ๋ ์ฝ๋๋ฅผ ๊ฐ๋จํ ๋ํ๋ด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
(ํฌ์คํ ์์ ํ๋ฆฐ ๋ด์ฉ์ด ์๋ค๋ฉด ํผ๋๋ฐฑ ์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค. ๐)
Folder ์ํฐํฐ
@Entity
class Folder(
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
@JsonBackReference
var parentFolder: Folder?,
) : BaseTimeEntity() {
@OrderBy("index asc")
@OneToMany(mappedBy = "parentFolder", cascade = [CascadeType.ALL])
@JsonManagedReference
var children: MutableList<Folder>? = mutableListOf()
@OneToMany(mappedBy = "folder", cascade = [CascadeType.ALL])
var folders: MutableList<AccountFolder>? = mutableListOf()
...
Entity๋ ์์ ๊ฐ์ด ์์ ์ ์ํ ์ฐธ์กฐ๋ก ๋ถ๋ชจ ๋ฐ ์์์ ๊ฐ์ง๋ ํ์์ผ๋ก ๊ตฌ์ฑ๋์ด ์๋๋ฐ์, ๋๋๊ธ๊ณผ ๋์ผํ ๊ตฌ์กฐ๋ผ๊ณ ์๊ฐํ์๋ฉด ๋ฉ๋๋ค.
Folder Conrtoller & Service
// Controller
@ApiOperation(value = "ํด๋ ์ญ์ (์ญ์ ํ๋ ค๋ ํด๋์ ํ์์ ์กด์ฌํ๋ ๋ชจ๋ ํด๋ ๋ฐ ๋ถ๋งํฌ๋ค ์ญ์ ) API")
@DeleteMapping("/{folderId}")
fun deleteAllBookmarkAndAllFolderWithRelatedFolder(
@PathVariable @ApiParam(value = "ํด๋ ID", example = "2", required = true) folderId: Long
): ResponseEntity<String> {
// ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๋ถ๋ถ!
val folder = folderService.findByFolderId(folderId)
folderService.deleteFolderRecursive(folder)
folderService.deleteFolder(folder)
return ResponseEntity.status(HttpStatus.OK).body(Message.SUCCESS)
}
// Service
fun deleteFolder(folder: Folder) {
folderRepository.deleteByFolder(folder)
}
fun deleteFolderRecursive(folder: Folder) {
// Base Condition: ์ตํ์ Depth์ ํด๋
val children: MutableList<Folder> = folder.children ?: return
children.let { folderList ->
folderList.stream().forEach { folder ->
deleteFolderRecursive(folder)
}
}
bookmarkRepository.findByFolderId(folder.id!!)
.let { list ->
list.forEach {
it.deletedByFolder()
it.remindOff()
bookmarkRepository.save(it)
}
}
folderRepository.deleteByFolder(folder)
}
์ปจํธ๋กค๋ฌ ๋ฐ ์๋น์ค์ ์ฝ๋๋ ์์ ๊ฐ์๋ฐ์, ํด๋๋ฅผ ์ญ์ ํ ์ ์์์ ๋ชจ๋ ํด๋ ๋ฐ ํด๋์ ์กด์ฌํ๋ ๋ถ๋งํฌ๋ค๋ ์ญ์ ๊ฐ ๋์ด์ผ ํฉ๋๋ค.
๋ํ ์คํ๋ง ํธ๋์ญ์
์ Self Invocation ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค๊ณ ์๊ฐํ์๊ธฐ์ ๋ ๊ฐ์ ์๋น์ค ๋ฉ์๋๋ฅผ ์ปจํธ๋กค๋ฌ์์ ํธ์ถํ๋๋ก ํ์์ต๋๋ค.
Spring Transactional self invocation
- https://stackoverflow.com/questions/23931698/spring-transactional-annotation-self-invocation
- https://gmoon92.github.io/spring/aop/2019/04/01/spring-aop-mechanism-with-self-invocation.html
- https://velog.io/@park2348190/Spring-Transaction%EC%9D%98-Self-Invocation
์ค์ ๋๋ฒ๊น ์ ํด๋ณด๋ฉด ์๋ ์ฝ๋์์ ์ฐ๊ด๊ด๊ณ๋ฅผ ์กฐํํ ๋ ์ด๋ฏธ ์์ธ๊ฐ ๋ฐ์ํ๊ณ ์์ต๋๋ค.
val folder = folderService.findByFolderId(folderId)
์ ๋ฌธ์ ๋ ์ง์ฐ ๋ก๋ฉ์์ ํ์ฌ ์ธ์ ์ด ์๊ธฐ์ ๋ฐ์ํ๋ ์ค๋ฅ์ธ๋ฐ์, ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ JPA์ OSIV(Open Session In View)๋ผ๋ ๊ฐ๋ ์ ๋ํด ์์์ผ ํ๊ธฐ์ ๊ฐ๋ตํ ์ค๋ช ์ ๋๋ฆฌ๊ฒ ์ต๋๋ค.
JPA ์์์ฑ ์ปจํ ์คํธ
JPA์์๋ ์์์ฑ ์ปจํ ์คํธ๋ผ๋ ๊ฐ๋ ์ด ์กด์ฌํ๋๋ฐ์, ์ด๋ ์ํฐํฐ๋ฅผ ์๊ตฌ ์ ์ฅํ๋ ํ๊ฒฝ ์ ์๋ฏธํฉ๋๋ค.
์ ํ๋ฆฌ์ผ์ด์ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ด์์ ๊ฐ์ฒด๋ฅผ ๋ณด๊ดํ๋ ๊ฐ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ์ญํ ์ ํฉ๋๋ค.
์ฃผ๋ก ์ฌ์ฉํ๋ JPA์ ๊ฒฝ์ฐ ํธ๋์ญ์ ์ ์ปค๋ฐํ๋ ์๊ฐ ์์์ฑ ์ปจํ ์คํธ์ ์๋ก ์ ์ฅ๋ ์ํฐํฐ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ํ๋๋ฐ, ์ด๋ฅผ flush๋ผ๊ณ ํฉ๋๋ค.
Entity์ ์๋ช ์ฃผ๊ธฐ๋ก ๋น์์, ์์, ์ค์์ ๋ฑ์ด ์กด์ฌํ๋๋ฐ์ ๊ฐ๋ตํ ๋ง์๋๋ฆฌ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ๋น์์(new/transient): ์์์ฑ ์ปจํ ์คํธ์ ์ ํ ๊ด๊ณ๊ฐ ์๋ ์ํ, Entity ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ๋ ์์์ฑ ์ปจํ ์คํธ์ ์ ์ฅํ์ง ์์ ์ํ
- ์์(managed): ์์์ฑ ์ปจํ ์คํธ์ ์ ์ฅ๋ ์ํ, Entity๊ฐ ์์์ฑ ์ปจํ ์คํธ์ ์ํด ๊ด๋ฆฌ๋จ
- ์ค์์(detached): ์์์ฑ ์ปจํ ์คํธ์ ์ ์ฅ๋ ํ ๋ถ๋ฆฌ๋ ์ํ
์ค์์ ๊ฒฝ์ฐ ์์์ฑ ์ปจํ ์คํธ์ ์ ์ฅ๋ ๊ฒฝํ์ด ์กด์ฌํ๊ธฐ์ ๋น์์๊ณผ๋ ๋ฌ๋ฆฌ ์๋ณ์๊ฐ ์กด์ฌํ๋ค๋ ๊ฒ์ด ๋ณด์ฅ์ด ๋ฉ๋๋ค.
์์์ฑ ์ปจํ ์คํธ๋ 1์ฐจ ์บ์, ๋์ผ์ฑ ๋ณด์ฅ, ์ง์ฐ ๋ก๋ฉ, ๋ณ๊ฒฝ ๊ฐ์ง(Dirty Checking) ๋ฑ์ ํน์ง์ด ์๋๋ฐ์, ์ฌ๊ธฐ์๋ ๋ฐ๋ก ์ค๋ช ํ์ง๋ ์๊ฒ ์ต๋๋ค ^^; ๊ด๋ จํ์ฌ ๊ถ๊ธํ์๋ฉด ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์. ๐
- https://www.baeldung.com/jpa-hibernate-persistence-context
- https://velog.io/@seongwon97/Spring-Boot-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context
ํธ๋์ญ์ ๋ฒ์ ๋ฐ ์์์ฑ ์ปจํ ์คํธ
์คํ๋ง JPA์ ๊ฒฝ์ฐ ์คํ๋ง ์ปจํ ์ด๋๊ฐ ์ ๊ณตํ๋ ์ ๋ต์ ๋ฐ๋ฅด๊ณ ์๋๋ฐ์, ๊ธฐ๋ณธ ์ ๋ต์ผ๋ก ํธ๋์ญ์ ๋ฒ์์ ์์์ฑ ์ปจํ ์คํธ ์ ๋ต์ ์ฌ์ฉํฉ๋๋ค. ์ด๋ ํธ๋์ญ์ ์ด ์์ํ๋ ์๊ฐ ์์์ฑ ์ปจํ ์คํธ๋ ์์ฑ๋๊ณ , ํธ๋์ญ์ ์ด ๋๋๋ ์๊ฐ ์์์ฑ ์ปจํ ์คํธ๊ฐ ์ข ๋ฃ๋๋ ์ ๋ต ์ ๋๋ค.
๋ฐ๋ผ์ ํธ๋์ญ์ ์ด ๋์ผํ ๊ฒฝ์ฐ ๋์ผํ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// Service
@Autowired val repo1: Repository1
@Autowired val repo2: Repository2
@Transactional
fun logic() {
repo1.save();
repo2.save();
}
์์ ๊ฐ์ ์ฝ๋๊ฐ ์์๋ logic() ๋ฉ์๋์์ ํธ๋์ญ์ ์ด ์ค์ ๋์ด์๊ธฐ ๋๋ฌธ์ repo1๊ณผ repo2๋ ๋์ผํ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
OSIV(Open Session In View)
OSIV(Open Session In View)๋ JPA์์ ์์์ฑ ์ปจํ ์คํธ์ Hibernate Session์ View๊ฐ ๋ ๋๋ง์ด ๋ ๋ ๊น์ง ์ ์ง์ํค๋ ๋ฐฉ๋ฒ์ ๋๋ค.
์คํ๋ง์ ๊ฒฝ์ฐ ๋ณดํต ๋น์ฆ๋์ค ๋ก์ง(Service Layer)์์ ํธ๋์ญ์ ์ ์ค์ ํ๊ณ (@Transactional) ์ด ํธ๋์ญ์ ์ด ์ ์ง๊ฐ ๋๋ ์์์ฑ ์ปจํ ์คํธ์ ๋ฒ์๋ ๋ทฐ ๋ ๋๋ง๊น์ง๋ ๋์ง๋ ์๋๋ฐ์, ๋ฐ๋ผ์ ์์์ฒ๋ผ Controller์์ Entity๋ฅผ ์กฐํํ ๋ Lazy loading์ด ๋์ด์๋ค๋ฉด LazyInitializationException์ ์์ธ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.(ํ์ฌ ๋ฐ์ํ๊ณ ์๋ ๋ฌธ์ ์ ๋๋ค.)
@Entity
class Folder(
...
@ManyToOne(fetch = FetchType.Lazy)
@JoinColumn(name = "parent_id")
@JsonBackReference
var parentFolder: Folder?, // LazyInitializationException
) : BaseTimeEntity() {
@Column
var emoji: String? = ""
@OrderBy("index asc")
@OneToMany(mappedBy = "parentFolder", cascade = [CascadeType.ALL])
@JsonManagedReference
var children: MutableList<Folder>? = mutableListOf()
@OneToMany(fetch = FetchType.EAGER, mappedBy = "folder", cascade = [CascadeType.ALL])
var folders: MutableList<AccountFolder>? = mutableListOf()
...
// Controller
@ApiOperation(value = "ํด๋ ์ญ์ (์ญ์ ํ๋ ค๋ ํด๋์ ํ์์ ์กด์ฌํ๋ ๋ชจ๋ ํด๋ ๋ฐ ๋ถ๋งํฌ๋ค ์ญ์ ) API")
@DeleteMapping("/{folderId}")
fun deleteAllBookmarkAndAllFolderWithRelatedFolder(
@PathVariable @ApiParam(value = "ํด๋ ID", example = "2", required = true) folderId: Long
): ResponseEntity<String> {
val folder = folderService.findByFolderId(folderId)
folderService.deleteFolderRecursive(folder)
folderService.deleteFolder(folder)
return ResponseEntity.status(HttpStatus.OK).body(Message.SUCCESS)
}
OSIV = false(default) ์ผ ๋
// application.properties
spring.jpa.open-in-view=false
OSIV = true ์ผ ๋
// application.properties
spring.jpa.open-in-view=true
LazyInitializationException ํด๊ฒฐ๋ฐฉ๋ฒ
์ ์๋ฌ์ ๊ด๋ จํ์ฌ ์ฌ๋ฌ ํด๊ฒฐ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋๋ฐ์, ์ด๋ ํ ๋ฐฉ๋ฒ์ด Best Practice์ธ์ง๋ ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฅผ ๊ฒ ๊ฐ์ต๋๋ค.
1. Entity์ FetchType์ Lazy -> Eager๋ก ์์
์์์ ์ฐ๊ด๊ด๊ณ๋ก ์ค์ ๋์ด ์๋ Entity์์ Lazy๋ฅผ Eager๋ก ์ค์ ํฉ๋๋ค.
์ฐธ๊ณ ๋ก JPA์์ default Fetch ์ ๋ต์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- OneToMany: Lazy
- ManyToOne: Eager
- ManyToMany: Lazy
- OneToOne: Eager
@Entity
class Folder(
...
@ManyToOne
@JoinColumn(name = "parent_id")
@JsonBackReference
var parentFolder: Folder?, // LazyInitializationException
) : BaseTimeEntity() {
@Column
var emoji: String? = ""
// FetchType์ Lazy์์ Eager๋ก ๋ณ๊ฒฝ
@OrderBy("index asc")
@OneToMany(fetch = FetchType.Eager, mappedBy = "parentFolder", cascade = [CascadeType.ALL])
@JsonManagedReference
var children: MutableList<Folder>? = mutableListOf()
// FetchType์ Lazy์์ Eager๋ก ๋ณ๊ฒฝ
@OneToMany(fetch = FetchType.EAGER, mappedBy = "folder", cascade = [CascadeType.ALL])
var folders: MutableList<AccountFolder>? = mutableListOf()
...
~ToOne์ ๊ฒฝ์ฐ deafult๋ก Eager์ด๊ธฐ์ ~ToMany๋ง Lazy์์ Eager๋ก ์์ ํฉ๋๋ค.
๊ทธ ํ ๋๋ฒ๊น ์ ํตํด ๊ฐ์ ํ์ธํด๋ณด๋ฉด ์์์ ๋ฐ์ํ๋ LazyInitializationException ์์ธ๊ฐ ๋ฐ์ํ์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
Lazy์์ Eager๋ก ๋ณ๊ฒฝํ์๊ธฐ์, ์ฐ๊ด ๊ด๊ณ ์ค์ ์ด ๋์ด์๋ Entity๊น์ง ๋ชจ๋ ๊ฐ์ ธ์จ๋ค๋ ๋จ์ ์ด ์์ต๋๋ค.
2. OSIV = true๋ก ์ค์
์์์ OSIV์ ๋ํด ๊ฐ๋ตํ ๋ง์๋๋ ธ๋๋ฐ์, OSIV๋ฅผ true๋ก ์ค์ ํ์ฌ ์์์ฑ ์ปจํ ์คํธ์ Hibernate Session์ View๊ฐ ๋ ๋๋ง์ด ๋ ๋ ๊น์ง ์ ์ง์ํค๋ฉด Controller์์ Entity๋ฅผ ์กฐํํ ๋ Lazy Loading์ด๋๋ผ๋ ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
// application.properties
spring.jpa.open-in-view=true
OSIV๋ฅผ true๋ก ์ค์ ํ ํ ํ์ธํด๋ณด๋ฉด ์์ธ ์์ด ์๋ํ๋๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
ํ์ง๋ง OSIV์ ๊ฒฝ์ฐ Anti-Pattern์ธ ๋ฏ ํ๋ฉฐ ์ฑ๋ฅ์ ์ธ ๋ฌธ์ ์๋ ์ด์๊ฐ ๋ฐ์ํ ์ ์๋ค๊ณ ํฉ๋๋ค. ๋ฐ๋ผ์ OSIV=true๋ ์ด๋๋ฏผ์ด๋ ๋ด๋ถ ์ ์ฉ ์ฌ์ดํธ ๋ฑ ์ค์๋๊ฐ ๋จ์ด์ง๋ ํ๋ก์ ํธ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํ๊ณ ์์ต๋๋ค.
์ถ๊ฐ์ ์ธ ๋ด์ฉ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
- https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot
- https://github.com/spring-projects/spring-boot/issues/7107
3. Controller์์ Lazy Loading์ Entity๋ฅผ ์กฐํํ์ง ์๊ธฐ
๊ฐ์ธ์ ์ธ ์๊ฐ์ผ๋ก๋ ๊ฐ์ฅ ์ด์์ ์ธ ๋ฐฉ๋ฒ์ธ ๋ฏ ํฉ๋๋ค. ํธ๋์ญ์ ์์์ ์ง์ฐ๋ก๋ฉ์ ์กฐํํ ์ ์๊ธฐ ๋๋ฌธ์ ํธ๋์ญ์ ์ด ์ค์ ๋์ด ์๋ ๋น์ฆ๋์ค ๋ก์ง ๋ด์์ Entity๋ฅผ ์กฐํํ๋ฉด ์์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
// Service
fun deleteFolderWithChild(folderId: Long) {
val folder = folderRepository.findById(folderId).orElseThrow { folderNotFoundException }
folderRepository.deleteByFolder(folder)
val children: MutableList<Folder> = folder.children ?: return
...
}
์ ์ฒ๋ผ Controller๊ฐ ์๋ ๋น์ฆ๋์ค ๋ก์ง์ด ์์ฑ๋์ด ์๋ Service์์ Entity๋ฅผ ์กฐํํ ํ ๋ก์ง์ ์ฒ๋ฆฌํ๋ฉด ์ ์์ ์ผ๋ก ๋์์ด ๋ฉ๋๋ค.
4. Use JOIN FETCH in JPQL
์ ๋ฐฉ๋ฒ์ ์ง์ ํด๋ณด์ง๋ ์์์ผ๋, JPQL์์ JOIN FETCH๋ฅผ ํตํด์๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค๊ณ ํฉ๋๋ค.
SELECT f FROM Folder f
JOIN FETCH f.parentFolder
JOIN FETCH f.children
JOIN FETCH f.folders
+ ์์ ๊ฐ์ด JOIN FETCH๋ฅผ ์ฌ๋ฌ๊ฐ ์ฌ์ฉํ ๊ฒฝ์ฐ ์๋์ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋๋ฐ์, ์๋์ ๊ฐ์ด ๋ฐ์ํ ๊ฒฝ์ฐ List๋ฅผ Set์ผ๋ก ๋ณ๊ฒฝํด์ฃผ๋ฉด ํด๊ฒฐํ ์ ์์ต๋๋ค.
"MultipleBagFetchException: cannot simultaneously fetch multiple bags is there a workaround"
๊ฒฐ๋ก
ํด๋น ๋ฌธ์ ๋ก ๊ตฌ๊ธ๋ง์ ํด๋ณด๋ฉด ๋๋ถ๋ถ @Transactional ์ค์ , Fetch๋ฅผ Eager๋ก ๋ณ๊ฒฝ ์ ๋ฐฉ๋ฒ์ด ๋๋ถ๋ถ์ด์๋๋ฐ์, ์ด๋ฒ ๊ธฐํ์ ์ข ๋ ์ฐพ์๋ณด๋ฉด์ ํท๊ฐ๋ ธ๋ OSIV์ ๊ฐ๋ ์ด๋ ์์ฃผ ๊น๋จน๋ JPA์ Fetch default ์ ๋ต ๋ฑ ๋ค์ ํ๋ฒ ํ์ตํ ์ ์์ด์ ์ข์์ต๋๋ค.
ํน์ ์์ ๊ฐ์ ๋ฌธ์ ๊ฐ ์๋๊ฒฝ์ฐ ํธ๋์ญ์ ๋ด์์ ์ง์ฐ๋ก๋ฉ์ ํ๊ณ ์๋์ง ํ์ธํด์ฃผ์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
์ฐธ๊ณ ๋ฌธ์
- https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot
- https://ttl-blog.tistory.com/183
- https://www.baeldung.com/hibernate-initialize-proxy-exception
- https://stackoverflow.com/questions/23931698/spring-transactional-annotation-self-invocation
- https://gmoon92.github.io/spring/aop/2019/04/01/spring-aop-mechanism-with-self-invocation.html
๋๊ธ