작성
·
25
1
안녕하세요! 질문이 생겨 질문글 남깁니다 :)
영속성 컨텍스트는 트랜잭션을 사용하면 생겨난다 라고 말씀 해주셨는데요.
이중에 1차 캐시 부분에 대한 내용이 궁금해서 코드를 작성하던중에 의아한점이 생겼습니다.
@Transactional
public List<UserResponse> getUsers() {
User user = userRepository.findById(4L).orElseThrow();
user.updateName("test");
userRepository.findById(4L);
userRepository.findById(4L);
return userRepository.findAll()
.stream()
.map(UserResponse::new)
.toList();
}
우선은 위 내용인데요, updateName을 호출한 시점에 변경 감지가 되어서 update문이 호출 되었는데, 이후에 로그로 찍히는 select문이 없더라구요. 1차캐시가 진행되면 그 이후에 객체의 값이 변경 되더라도 그 내용까지 다시 반영해서 캐싱 해주는걸까요?
public List<UserResponse> getUsers() {
userRepository.findById(4L);
userRepository.findById(4L);
userRepository.findById(4L);
return userRepository.findAll()
.stream()
.map(UserResponse::new)
.toList();
}
그리고 두번째 질문은 코드를 이런식으로 트랜잭션 어노테이션 없이 작성했을 때 인데요.
제 추측은 트랜잭션 어노테이션이 없으니 영속성컨텍스트가 생성되지 않을것이고 그에따라 select문이 3번 호출될것이다. 였는데 실행해보니 select문은 한번만 호출되더라구요.
나름대로 왜일까 고민해본 결론은 findById가 구현된 SimpleJpaRepository클래스에 붙어있는 Transactional이 영향을 주는건가? 싶긴 한데 명확한 답은 모르겠습니다 ☹
1차캐시에 한해서는 트랜잭셔널과는 독립되게 영속성컨텍스트가 동작하는걸까요?
답변 2
1
안녕하세요 영욱님! 🙂 정말 기가 막힌 포인트를 찾아내셨군요...! 👍
결론부터 빠르게 말씀드리면, 이것은 OSIV 라는 옵션 때문입니다.
Open-Session-In-View 옵션은 기본적으로 true로 설정되어 있는데
spring:
jpa:
open-in-view: false
를 이용해 false
로 만들 수 있고요 실무에서 권장되는 값은 false 입니다.
그럼 이제 OSIV 옵션이 무엇인지 설명을 드리면,
만약 이 값이 true
로 설정되어 있다면, 트랜잭션이 열러 영속성 컨텍스트가 한 번 로딩되면, 해당 요청이 완전히 종료될 때까지 영속성 컨텍스트를 유지하게
됩니다.
때문에 위 경우는 처음 로딩된 findById()
에서 Repository 내부에 있는 트랜잭션 실행 -> OSIV 설정에 의해 영속성 컨텍스트가 유지되어 버림 -> Service 종료시점까지 (심지어 Controller를 완전히 벗어나 요청이 끝날 때까지) 영속성 컨텍스트가 남아 있게 됩니다.
이런 설정이 존재하는 이유는 과거의 "템플릿 엔진"을 사용했기 때문인데요
요즘에는 react.js 혹은 next.js와 같은 FE 영역과 RESTful API BE 영역이 완전히 구분되지만, 옛날에는 서버가 직접 HTML을 브라우저에 반환하는 기법을 사용했습니다. 그리고 HTML을 사용자마다 다르게 보여주려면,
<h1> 안녕하세요 ??? 님 </h1>
처럼 ???
이란 빈 칸을 뚫어두고 DB에서 조회해온 값을 여기에 넣어주게 되었죠.
이때 DB에서 조회해온 값 (= Service와 Repository를 거쳐 받아온 값) 을 템플릿 엔진에서 조금 더 적극적으로 사용하기 위한 (= 빈칸에서 Entity를 이리저리 사용하기 위한) 옵션이 OSIV 였습니다.
현재 실무에서 OSIV를 false로 권장하는 이유는, 대부분 REST API를 만들기 때문에 OSIV 처럼 암시적으로 영속성 컨텍스트가 오래 유지되며 생기는 문제들을 원천 차단하기 위해서입니다. OSIV를 잘못 사용하게 되면 Controller에서 Entity에 넣은 값이 dirty check 등으로 저장되어 버릴 수도 있고, 영속성 컨텍스트를 오래 유지하며 시스템 자원이 낭비될 수도 있기 때문이죠.
이런 부분은 첫 서버를 배우는데 함께 설명드리기에는 어려운 내용이라 이번 강의에서는 제외되어 있습니다. 🙂 열심히 들어 주셔서 감사합니다. 또 궁금한 점 편하게 질문 올려주세요! 감사합니다. 🙇
아무래도 편리한 만큼 위험할 수 있는 지점들도 분명 있더라고요! 🥲 다만 어느 정도 지식이 쌓이면 개발 생산성과 편의성 측면에서는 무척 훌륭한 모습을 보여줍니다! 🙂
0
안녕하세요, 인프런 AI 인턴입니다. 김영욱님, 좋은 질문 감사합니다!
영속성 컨텍스트와 1차 캐시에 대한 질문에 대해 답변 드리겠습니다.
1차 캐시와 변경 감지:
영속성 컨텍스트는 엔티티를 조회할 때마다 1차 캐시에 저장하고, 트랜잭션 내의 모든 쿼리를 관리합니다. user.updateName("test")
를 호출하면 JPA는 변경 감지를 통해 알아서 DB에 업데이트 쿼리를 보냅니다. 이후 userRepository.findById(4L)
를 반복해서 호출해도 영속성 컨텍스트의 1차 캐시에 이미 엔티티가 존재하기 때문에 추가적인 select 쿼리를 발생시키지 않습니다. 이는 JPA의 캐시 메커니즘 덕분입니다. 캐시에 있는 엔티티는 변경된 상태일지라도 최신 상태로 업데이트되어 사용됩니다.
트랜잭션과 1차 캐시의 독립성:
@Transactional
이 붙지 않으면, 원래는 영속성 컨텍스트가 생성되지 않고, 캐싱도 이루어지지 않는 것으로 생각할 수 있습니다. 그러나 Spring Data JPA의 리포지토리 메소드 (예: findById
)에는 자체적으로 @Transactional
이 붙어 있기에 메소드 단위의 기본 트랜잭션이 활성화됩니다. 따라서 해당 메소드의 실행 범위 내에서 영속성 컨텍스트가 작동하여 캐싱을 수행합니다. 여러 번 findById
를 호출하더라도 동일한 영속성 컨텍스트 내에서 캐시가 작동하여 쿼리가 한 번만 실행됩니다.
이에 대한 논의는 다음 게시물에서도 유사하게 다루어지고 있습니다:
- 같은 Entity에 대해 findById() 호출 시 데이터베이스 쿼리 실행 여부
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
어우 jpa는 정말 방대하네요...🥲 쉽게 생각하고 도입하면 안되겠네요..
답변 감사합니다!!