You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
해당 연관관계는 모두 @ManyToOne(fetch = FetchType.LAZY)입니다 — ScoreJpaEntity.kt:76-78(project), EvidenceJpaEntity.kt:35-37(user), FileJpaEntity.kt:32-34(user) / :40-42(evidence).
왜 지금까지 드러나지 않았는가
application.yaml에 spring.jpa.open-in-view 설정이 없어 Spring Boot 기본값인 true가 적용됩니다. OSIV가 요청 스레드에 EntityManager를 열어둔 채 유지하므로, 위 프록시 초기화가 오류 없이 추가 쿼리로 조용히 해결됩니다.
즉 기능은 정상 동작하지만, 하나의 영속성 컨텍스트 안에서 서로 다른 미초기화 프록시 개수만큼 쿼리가 추가로 발생합니다.
ScorePersistenceAdapter의 기존 KDoc도 이미 같은 위험을 인지하고 있습니다:
Kotlin + JPA는 기본적으로 FIELD Access를 사용하므로, 지연 로딩 프록시의 식별자(user.userId)를 읽는 것만으로도 초기화(추가 쿼리)가 유발될 수 있어 user도 fetch join 대상에 포함합니다.
user에는 이 원칙을 적용했지만 project, evidence.user, file.user, file.evidence에는 누락되었습니다.
영향
myPercentInGrade는 학년 전체 학생의 점수를 로드하므로, 서로 다른 project / evidence.user / file.user / file.evidence 개수에 비례해 추가 쿼리가 발생합니다. 학년 규모가 커질수록 선형으로 증가합니다.
findAllByUserId가 findAllByUserIdIn에 위임하므로(:63) 개인 점수 조회 경로도 함께 영향받습니다.
추가로, 이 누락은 재계산을 별도 스레드로 옮기는 것을 막는 제약이기도 합니다. OSIV는 요청 스레드에만 EntityManager를 바인딩하므로, 워커 스레드에서 같은 코드가 실행되면 프록시 초기화가 LazyInitializationException으로 실패합니다. [score] 백분위 조회 캐시 스탬피드 방지 장치 부재 #145 참고.
문제
ScorePersistenceAdapter.findAllByUserIdIn(ScorePersistenceAdapter.kt:65-94)이 실행하는 두 쿼리에서 fetch join이 4곳 누락되어 있고,toDomain()이 그 지연 로딩 프록시를 읽습니다.쿼리 1 —
scoreJpaEntity조회 (:68-78)fetch join 대상:
user,category,evidenceScoreJpaEntity.toDomain()이 읽는 값user.userIdcategory.toDomain()CategoryJpaEntity에는 연관관계 없음)evidence?.toDomain()→ 내부에서user.userId(EvidenceJpaEntityExtensions.kt:15)evidence만 ✅,evidence.user❌project?.projectId(ScoreJpaEntityExtensions.kt:32)쿼리 2 —
fileJpaEntity조회 (:82-89)fetch join 대상:
scoreFileJpaEntity.toDomain()이 읽는 값user.userId(FileJpaEntityExtensions.kt:15)score?.scoreIdevidence?.evidenceId(FileJpaEntityExtensions.kt:20)해당 연관관계는 모두
@ManyToOne(fetch = FetchType.LAZY)입니다 —ScoreJpaEntity.kt:76-78(project),EvidenceJpaEntity.kt:35-37(user),FileJpaEntity.kt:32-34(user) /:40-42(evidence).왜 지금까지 드러나지 않았는가
application.yaml에spring.jpa.open-in-view설정이 없어 Spring Boot 기본값인true가 적용됩니다. OSIV가 요청 스레드에 EntityManager를 열어둔 채 유지하므로, 위 프록시 초기화가 오류 없이 추가 쿼리로 조용히 해결됩니다.즉 기능은 정상 동작하지만, 하나의 영속성 컨텍스트 안에서 서로 다른 미초기화 프록시 개수만큼 쿼리가 추가로 발생합니다.
ScorePersistenceAdapter의 기존 KDoc도 이미 같은 위험을 인지하고 있습니다:user에는 이 원칙을 적용했지만project,evidence.user,file.user,file.evidence에는 누락되었습니다.영향
myPercentInGrade는 학년 전체 학생의 점수를 로드하므로, 서로 다른project/evidence.user/file.user/file.evidence개수에 비례해 추가 쿼리가 발생합니다. 학년 규모가 커질수록 선형으로 증가합니다.findAllByUserId가findAllByUserIdIn에 위임하므로(:63) 개인 점수 조회 경로도 함께 영향받습니다.LazyInitializationException으로 실패합니다. [score] 백분위 조회 캐시 스탬피드 방지 장치 부재 #145 참고.제안하는 개선
누락된 4곳에 fetch join을 추가합니다. 넷 다 to-one 연관이므로 행이 늘지 않습니다(카테시안 곱 없음).
쿼리 1
evidence.user는 중첩 fetch join이므로QEvidenceJpaEntity별칭을 잡습니다.queryFactory .selectFrom(scoreJpaEntity) .join(scoreJpaEntity.user).fetchJoin() .join(scoreJpaEntity.category).fetchJoin() .leftJoin(scoreJpaEntity.evidence, evidenceJpaEntity).fetchJoin() .leftJoin(evidenceJpaEntity.user).fetchJoin() // 추가 .leftJoin(scoreJpaEntity.project).fetchJoin() // 추가 .where(scoreJpaEntity.user.userId.`in`(userIds)) .fetch()쿼리 2
queryFactory .selectFrom(fileJpaEntity) .join(fileJpaEntity.score).fetchJoin() .join(fileJpaEntity.user).fetchJoin() // 추가 .leftJoin(fileJpaEntity.evidence).fetchJoin() // 추가 .where(fileJpaEntity.score.scoreId.`in`(entities.map { it.scoreId })) .fetch()작업 목록
spring.jpa.show-sql=true로 수정 전 쿼리 수 측정 (myPercentInGrade기준)project,evidence.userfetch join 추가user,evidencefetch join 추가ScorePersistenceAdapterKDoc에 이번에 채운 4곳 반영ScorePersistenceAdapterTest스텁이 새 쿼리 형태와 어긋나지 않는지 확인참고
spring.jpa.open-in-view를 명시적으로false로 두는 것은 이 이슈의 범위를 넘습니다(다른 경로의 지연 로딩까지 영향). 여기서는 fetch join 누락 자체만 고칩니다.show-sql로만 검증 가능합니다.