티스토리 뷰
문제
JPA에서는 1차 캐시를 사용하기 때문에 같은 엔티티에 대해서는 동일성(주소값이 같음)을 만족한다고 알고 사용해왔다. 그래서 나는 객체를 비교할 때 id가 아닌 객체 그 자체로 비교해 왔다. 그런데 어느 날 id로 가지고 온 객체를 이용해 fetch join을 통해 가지고 온 객체 중 하나를 찾는 로직을 작성했는데, 하나도 못 찾는 문제가 발생했다. 확인해보니 id를 통해 가지고 온 객체와 fetch join을 통해 가지고 온 객체의 주소값이 달랐다. 결론부터 말하자면 원인은 @Transactional을 붙이지 않아 발생했다.
테스트1. @Transactional 없이 조회
아래 코드는 테스트를 위해 작성한 코드로, findByIdPerson은 personRepository에서 id를 이용해 person을 가지고 왔고, entityGraphPerson 객체는 team을 가져올 때 fetch join으로 거기에 속한 멤버도 함께 가져오도록 해서 person 객체를 조회했다.
@Test
public void jpaFirstCacheTransactionalTest() {
// id 로 사람 조회
Member findByIdMember = personRepository.findById(1L).get();
// team과 사람 한번에 조회(패치 조인)
Team teamWithMember = teamRepository.findWithPersonById(1L).get();
Member entityGraphMember = teamWithMember.getMembers().get(0);
assertEquals(findByIdMember.getId(), entityGraphMember.getId(), "id가 다릅니다");
assertEquals(findByIdMember, entityGraphMember, "주소값이 다릅니다");
}
데이터는 아래와 같이 들어있다
첫 번째 테스트인 id 값 비교는 성공하지만, 두 번째 테스트인 주소값 비교는 실패한다.
테스트2. @Transactional 추가 후 조회
그럼 이번에는 @Transactional을 추가해서 테스트해보자.
@Test
@Transactional // 추가**
public void jpaFirstCacheTransactionalTest() {
// id 로 사람 조회
Member findByIdMember = personRepository.findById(1L).get();
// team과 사람 한번에 조회(패치 조인)
Team teamWithMember = teamRepository.findWithPersonById(1L).get();
Member entityGraphMember = teamWithMember.getMembers().get(0);
assertEquals(findByIdMember.getId(), entityGraphMember.getId(), "id가 다릅니다");
assertEquals(findByIdMember, entityGraphMember, "주소값이 다릅니다");
}
성공이다!👍
JPA 1차 캐시 조건
JPA 의 1차 캐시의 조건에는 아래와 같은 내용이 있다.
1. 트랜잭션이 종료되거나, flush()될 경우 캐시는 초기화
2. 캐시에서 값을 가져오는건 PK를 기반으로한 find (entity manager를 쓸때 find() 메서드 혹은 data-jpa의 findById)에서만 동작
3. EntityManager 단위에서 1차 캐시가 관리됨
지금까지 테스트해본 문제는 1번 조건이 적용되지 않아 동일 트랜잭션이 아니었던 조회에서 1차 캐시가 공유되지 않았고, 그래서 엔티티의 동일성이 보장되지 않았던 것이다.
테스트3. id로 조회후 name으로 조회
여기서 2번의 경우 쿼리가 몇번 날라가는지 테스트를 해보자. 만약 id로 조회한 후 name로 조회하면 쿼리는 몇 번 실행될까?
@Test
@Transactional
public void jpaFirstCacheOrderTest() {
// id 로 사람 조회
Member findByIdMember = personRepository.findById(1L).get();
// name으로 동일한 사람 조회
Member findByNameMember = personRepository.findByName("YR").get();
assertEquals(findByIdMember.getId(), findByNameMember.getId(), "id가 다릅니다");
assertEquals(findByIdMember, findByNameMember, "주소값이 다릅니다");
}
여기서 테스트는 성공하지만 중요한건 쿼리가 2번 나간다. 어떻게 동작하는지 생각해보면 첫번째 id로 조회했을 때 1차 캐시에 아래와 같이 데이터가 저장됐을 것이다. 하지만 name으로 조회할 땐 2번 조건에 따라 1차 캐시가 아닌 db에서 다시 가지고 오기 때문에 쿼리가 한번 더 나간다.
1차 캐시 | |
@Id | Entity |
1 | Member(YR) |
테스트4. name으로 조회 후 id로 조회
이번에는 name으로 먼저 조회하고 id 로 조회해보자
@Test
@Transactional
public void jpaFirstCacheOrderTest() {
// name으로 동일한 사람 조회
Member findByNameMember = personRepository.findByName("YR").get();
// id 로 사람 조회
Member findByIdMember = personRepository.findById(1L).get();
assertEquals(findByIdMember.getId(), findByNameMember.getId(), "id가 다릅니다");
assertEquals(findByIdMember, findByNameMember, "주소값이 다릅니다");
}
이번에는 쿼리가 한번만 나갔다. 첫번째 name으로 조회했을 때 첫번째 테스트에서와 마찬가지로 1차 캐시에 아래와 같이 데이터가 저장됐을 것이다 그 이후에 pk 로 조회했을 때 1차 캐시에서 id로 1번 데이터를 찾았을 거고, 첫번째 조회에서 저장된 엔티티가 있으니 쿼리는 날라가지 않은 것이다.
1차 캐시 | |
@Id | Entity |
1 | Member(YR) |
이번 문제를 통해 JPA에서 @Transactional의 중요성을 다시 한번 깨달았다. 보통 나는 service 단에 @Transactional(readOnly=true) 를 붙여놓고 데이터 저장,수정,삭제 등이 필요한 메서드에만 @Transactional 를 따로 붙여주는데 이 서비스는 기존에 있던 서비스에 내용을 추가하다보니 놓친 것 같다.. 앞으로는 간단한 내용만 추가한다고 해도 서비스를 꼼꼼히 살펴보고 추가하는게 좋을 것 같다.
'웹개발' 카테고리의 다른 글
권한 설계하기 (3) | 2020.07.01 |
---|---|
[PHP - Laravel] 라라벨 디버그모드 설정 (2) | 2020.01.09 |
[POSTMAN] POST로 보냈는데 GET으로 인식하는 문제 (7) | 2020.01.09 |
JWT (1) | 2019.07.12 |
쿠키/세션/캐시 (1) | 2019.07.12 |
- Total
- Today
- Yesterday
- multiple datasource
- SynchronousQueue
- cleanup policies
- 쓰레드 변수
- docker
- 제우스8.5
- 보관주기
- 넥서스 파일 보관주기
- 오블완
- 넥서스 보관주기
- 주키퍼 없는 카프카
- db 두개
- volatile
- spring boot
- kafka with raft
- 티스토리챌린지
- API Gateway
- kafka without zookeeper
- 제우스 로그
- cleanup policy
- jpa 1차 캐시
- 제우스8
- 도커
- php
- AWS
- 네트워크
- s3
- 다중 데이터소스
- 스레드 동기화
- 카프카
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |