일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Convention
- fetch join
- MySQLTransactionRollbackException
- awspring
- jdbc
- spring-cloud-starter-aws
- spring
- mockito
- @controller
- Cannotacquirelockexception
- Git
- @RequestMapping
- OIDC
- 이펙티브 자바
- 정적 팩터리 메서드
- AWS
- Cache
- Batch
- batch insert
- assert
- JPA
- 성능테스트
- N + 1
- ngrinder
- oauth2.0
- Hibernate
- injellij
- @Transaction(readOnly=true)
- 데드락
- 동시성
- Today
- Total
정리정리
@Transactional(readOnly = true) 성능 향상되는 이유 본문
@Transactional에는 읽기 전용인 readOnly 옵션이 있습니다. 처음에는 그저 Isolation level 설정 정도로 생각을 했었지만 그로 인해 JPA에서 성능의 차이가 발생하는 점을 알게 되어 포스팅을 하게 되었습니다.
@Transactional(readOnly = true) 성능 비교
우선 성능 차이가 왜 발생하는지 알아보기 전에 정말 성능 차이가 있는지 먼저 확인해 보겠습니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public void getMembersWithReadOnlyFalse() {
memberRepository.findAll();
}
@Transactional(readOnly = true)
public void getMembersWithReadOnlyTrue() {
memberRepository.findAll();
}
}
@SpringBootTest
@Rollback(false)
public class ReadOnlyTest {
@Autowired
MemberRepository memberRepository;
@Autowired
MemberService memberService;
static long readOnlyTrueTime;
static long readOnlyFalseTime;
@BeforeEach
void init() {
List<Member> members = new ArrayList<>();
for (int i = 0; i < 1_000; i++) {
members.add(new Member("member" + i));
}
memberRepository.saveAll(members);
}
@Test
@DisplayName("readOnly = true")
void readOnlyTest() throws Exception {
long start = System.currentTimeMillis();
memberService.getMembersWithReadOnlyTrue();
long end = System.currentTimeMillis();
readOnlyTrueTime = end - start;
}
@Test
@DisplayName("readOnly = false")
void noReadOnlyTest() throws Exception {
long start = System.currentTimeMillis();
memberService.getMembersWithReadOnlyFalse();
long end = System.currentTimeMillis();
readOnlyFalseTime = end - start;
}
@AfterAll
static void printTime() {
System.out.println("========================================");
System.out.println("(readOnly = true): " + readOnlyTrueTime / 1000. + "초");
System.out.println("(readOnly = false): " + readOnlyFalseTime / 1000. + "초");
System.out.println("========================================");
}
}
System.currentTimeMills를 이용해 데이터 1000개를 조회하는 테스트를 작성했습니다. 그리고 아래에서 설명하겠지만 성능에 차이를 주는 요소 중 하나인 flush를 하는 작업도 시간 측정에 포함이 되도록 @Rollback(false)를 추가해 줬습니다.
결과는 당연히 readOnly 옵션을 true로 줬을 때가 더 빨랐지만 생각보다 성능 차이가 꽤나 있었네요.
성능이 향상되는 이유
우선 결론부터 말씀드리면 크게 2가지 이유가 있습니다.
- Dirty Check를 위한 스냅샷을 만들지 않아도 됨
- flush가 발생하지 않음
1. Dirty Check를 위한 스냅샷을 만들지 않아도 됨
하이버네이트는 기본적으로 변화를 감지하는 더티 체킹을 위해 데이터를 조회할 때 스냅샷이라는 것을 만듭니다.
해당 작업을 TwoPhaseLoad 라는 클래스의 initializeEntityFromEntityEntryLoadedState 메서드에서 확인할 수 있습니다.
우선 스냅샷을 만들기 전에 db에서 가져온 값을 저장하고 있는 loadedState를 이용해 hydratedState라는 변수를 만들고 조회할 엔티티에 값을 넣어주는 작업을 합니다.
그리고 맨 마지막에 hydratedState에 있는 값들을 deepCopy를 하여 loadedState가 스냅샷이 되도록 작업을 해줍니다.
이 작업때문에 추후에 dirtyCheck 메서드를 보면 entity와 loadedState를 이용해 변화의 감지를 할 수 있는 것을 볼 수 있습니다.
여기서 deepCopy를 하기 때문에 당연히 테스트할 때처럼 1000개를 조회하는 경우, 1000개만큼 새로 할당을 해주는 작업을 해야 하기 때문에 성능에 큰 저하가 온 것으로 보입니다.
반면에 readOnly = true인 경우 당연히 스냅샷을 만들지 않기 때문에 성능과 메모리의 차이가 생기게 됩니다. 그 과정도 한 번 살펴보겠습니다.
readOnly가 true라면 해당 status와 함께 setEntryStatus를 타고 들어가게 됩니다.
그리고 AbstractEntityEntry 클래스의 setStatus에서 스냅샷이 될 loadedState를 null로 초기화하는 모습을 볼 수 있습니다.
주의할 점
OSIV가 켜져있으면 readOnly = true여도 스냅샷이 생성이 됩니다. 정확히 왜 이런 일이 발생하는지는 잘 모르겠지만 디버깅을 하면서 간단하게 추측을 할 수는 있었습니다.
우선 위에서도 볼 수 있듯이 initializeEntityFromEntityEntryLoadedState메서드에 readOnly라는 플래그가 파라미터로 넘어옵니다.
그런데 readOnly = true일 때도 파라미터 값이 false로 넘어오길래 호출하는 메서드를 거꾸로 가다 보니 다음과 같은 코드를 볼 수 있었습니다.
해당 부분은 HibernateJpaDialect 클래스의 beginTransaction 메서드입니다. 브래이크를 걸어둔 if문을 통과해야 Hibernate가 관리하는 session의 readOnly의 값이 true가 되고, 이 값이 나중에 파라미터로 넘어가게 됩니다.
여기서 TransactionDefinition의 값을 보면 readOnly가 추가되어 있음을 알 수 있습니다. 하지만 if 문의 두 번째 조건인 isLocalResource에서 문제가 발생했습니다.
isLocalResource는 찾아보니 리소스를 local transaction에서만 다루는지 확인하는 메서드였고, OSIV가 켜져 있으면 해당 값이 사진처럼 false로 설정이 되어있는 것을 볼 수 있었습니다.
그래서 TransactionDefinition을 쫒아 디버깅을 또 하다가 localResource의 값이 newEntityManagerHolder인지에 따라 정해지는 것을 알게 되었고, entityManagerHolder에 대해 검색을 하다가 다음 글을 보게 되었습니다.
https://perfectacle.github.io/2021/05/24/entity-manager-lifecycle/
이 글에서는 OSIV가 켜져있을 때 다른 트랜잭션이더라도 1차 캐시가 공유된다는 내용이 적혀있었습니다.
그렇기 때문에 readOnly = true인 트랜잭션에서 데이터를 읽더라도 다른 트랜잭션에서 1차캐시를 공유해야하기 때문에 localResource가 아닌 globalResource로 설정을 하고 스냅샷을 저장하지 않나 라는 추측을 할 수 있었습니다.
2. flush가 발생하지 않음
readOnly=true일 경우에는 Hibernate에서 세션의 FlushMode를 MANUAL로 설정을 합니다.
FlushMode를 MANUAL로 설정을 docs에도 나와있듯이, 개발자가 직접 flush를 호출하지 않는 이상 flush가 발생하지 않습니다.
실제로 HibernateJpaDialect 클래스를 열어보면 readOnly가 true일 때 MANUAL로 FlushMode를 설정을 해줍니다.
그리고 Hibernate의 flushBeforeTransactionCompletion 메서드에서 FlushMode가 MANUAL이어야 flush가 doFlush가 true가 되도록 코드가 작성이 되어있습니다.
실제로 조회가 끝난 후에 entityManager를 통해 스냅샷이 생성되지 않은 것과 flushMode가 MANUAL인 것을 볼 수 있습니다.
참고
https://perfectacle.github.io/2021/08/08/readonly-transaction-doesnt-make-entity-snapshot/
https://perfectacle.github.io/2021/05/24/entity-manager-lifecycle/
'JPA' 카테고리의 다른 글
JPA N + 1 문제 (0) | 2023.04.11 |
---|---|
[JPA] CascadeType.DELETE, orphanRemoval = true 차이 (0) | 2023.03.27 |
Spring JPA 데이터베이스 초기화 (0) | 2023.03.27 |
JPA, Hibernate, Spring Data JPA, JDBC (0) | 2023.03.27 |