일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- ddd
- Spring
- 교육기획팀
- 한국대학생it경영학회
- Spring Batch
- reactive operaton
- jdbc
- 영속성
- 큐시즘
- Domain Driven Design
- 교육기획팀원
- cicd
- JPA
- 이펙티브자바
- delayed message plugin
- JPQL
- kusitms
- 객체지향 쿼리 언어
- 도메인 주도 개발 시작하기
- java
- 자바 ORM 표준 JPA 프로그래밍
- 밋업프로젝트
- RESTClient
- springboot
- 자동처리
- GitHub Actions
- 최범균
- rabbitmq-delayed-message-exchange
- scheduling messages with rabbitmq
- 30기
- Today
- Total
코딩은 마라톤
[JPA] 엔티티 저장 후 조회, 동등성 문제 및 해설 (@Transactional) 본문
근황
큐시즘 30기를 수료한 후 종강도 맞이하면서, 요새 딱히 바쁘게 지내고 있지 않은 것 같다.
무언가를 시작할 때 고민이 많아 "할까, 말까?"를 고민하다가 결국 안 하는 경우가 많아서, 자연스레 바쁘지 않은 생활이 이어지는 듯하다.
그래도 이번에 스터디를 만들어 큐시즘 개발 파트 일부 사람들과 토비의 스프링 3.1 Vol. 1을 읽기로 했다!
개발을 하면서 다양한 기술을 배우는 것도 물론 중요하지만, 요즘은 워낙 자료가 풍부해 새로운 기술도 며칠만 투자하면 구현 정도는 가능하다고 생각한다. (나뿐만 아니라 대다수의 개발 경험이 있는 사람들이라면..?)
하지만 정작 지금 사용하는 프레임워크인 SpringBoot는 물론, 그 기반인 Spring에 대해 깊이 알고 있느냐는 질문에는 솔직히 "No"라고 답할 수밖에 없다.
코틀린, MSA, Kafka, K8S, Redis 등 공부하고 싶은 것들은 많지만, 우선은 현재 사용하는 Java, Spring, SpringBoot, DB, 네트워크부터 더 잘 알아야겠다는 생각이 들었다.

그래서,, 왜 위와 같은 생각을 했을까?

요즘 심심할 때마다 개발 유튜브(?)를 보고 있다.
개발 유튜브를 보면서 대부분의 영상은 듣고 "이런게 있구나~" 하고 뇌 1차 캐시에만 저장해둔다 ㅋㅋㅋ
근데 문제는 백기선님의 영상을 보고나서였다. (https://www.youtube.com/watch?v=bJfbPWEMj_c)
답을 듣고 나서도 인터페이스로 분리하고 테스트하기 편한 코드로 만드는 등의 중요성은 코드에서 어느 정도 이해할 수 있었지만, PSA가 무엇인지조차 몰랐다. 당시 내 상태를 표현하자면, 이렇게 말할 수 있다:
스프링이 뭔지 잘 모르는구나.
스프링부트로 개발을 쉽게 할 수만 있었을 뿐, 기본이 안되었다........

그래서 스터디를 직접 만들고, 다른 기술보다 백엔드의 본질적인 내용을 더 깊이 이해해야겠다고 마음먹었다.
본론
근황이 긴 이유가 있다..!

이 영상을 보다가 헷갈리는 부분이 있었다. (영상을 보고 오시면 헷갈리는 지점을 더 빨리 파악할 수 있어요!!!!!)

코드 설명
- 상수 객체를 선언한다. => KOI
- saveAndCompare() : KOI를 저장하고 KOI와 같은지 비교하는 테스트
- find() : KOI를 저장하고, 저장된 객체의 id를 통해 조회하여 저장한 객체와 조회한 객체가 같은지 비교하는 테스트
문제
영상에서는 saveAndCompare() 테스트와 find() 테스트를 독립적으로 실행하면 통과하고, 통합 테스트를 진행하면 find()에서 문제가 발생한다고 한다.
1. saveAndCompare() 테스트 (독립)
우선 saveAndCompare() 테스트는 당연히 통과할 수밖에 없다.

- entity가 비영속 객체일 경우 (isNew)
- 영속화(persist) 후 entity를 그대로 반환한다.
- 비영속 객체가 아닐 경우 (준영속, 영속 상태)
- 병합(merge) 후 entityManager.merge(entity)를 반환한다.
- entity != entityManager.merge(entity)
saveAndCompare() 테스트에서 KOI는 비영속인 상태였고, 저장 시 영속화 후 KOI 엔티티가 그대로 반환되기 때문에 당연히 통과한다.

2. find() 테스트 (독립)
영상에서는 find() 테스트를 독립적으로 실행했을 경우, 테스트는 성공했다고 한다.

하지만 직접 돌려봤을 때 테스트는 실패하였다.


실패 로그를 보니 저장한 객체와 조회한 객체가 동등하지 않다고 한다.

각 코드를 뜯어보자.

- KOI를 저장한다. 이때 KOI는 비영속 상태이기 때문에, crew와 KOI는 같다. (1번.savedAndCompare() 참고)

- crew의 id를 가져온다. (== KOI의 id)
- id를 통해 조회한다.
그리고 두 객체를 비교하면,,,

사실 위의 생각이 영상 보고 처음 떠올린 생각이었다. (아...)
실패 이유
주의 : 책이랑 유튜브 보면서 생각한 해결책인데.. 아닐 수도 있습니다..
혹시나 이 방법이 아니라면,,, 댓글로 꼭 알려주시면 바로 수정하겠습니다..!
우선 영속성 컨텍스트와 1차 캐시, 그리고 트랜잭션을 고려해야 한다.
위의 생각은 영속성 컨텍스트, 1차 캐시, 트랜잭션을 전혀 고려하지 않은 완전 틀린 생각이다.
다시 한번 코드를 뜯어보자!

- KOI를 저장한다. 이때 KOI는 비영속 상태이기 때문에, crew와 KOI는 같다. (1번.savedAndCompare() 참고)
- KOI가 영속화(persist)되면서 1차 캐시에 저장된다.
- 트랜잭션이 커밋되면서 db에 crew 객체가 플러시되고, 영속성 컨텍스트는 종료된다. (트랜잭션의 범위와 영속성 컨텍스트의 범위는 같기 때문이다.)

save() 메소드는 @Transactional이 붙어 있어 save() 시 SimpleJpaRepository 단에서 트랜잭션이 수행된다.
따라서 save() 메소드가 수행된 후에, 현재 영속성 컨텍스트는 종료되기 때문에 1차 캐시에 crew 객체가 저장되지 않는다.

- save() 트랜잭션(영속성 컨텍스트)이 종료되고 findById()에서 트랜잭션(영속성 컨텍스트)이 시작된다.

- crew의 id를 가지고 db에서 조회한다. (1차 캐시에는 없기 때문에 db에서 조회한다.)
- db에서 조회하면서 1차 캐시에 저장하고, findCrew 객체를 반환한다.
- findById() 트랜잭션(영속성 컨텍스트)가 종료된다.
따라서 crew와 findCrew 객체는 당연히 다르기 때문에 테스트가 실패한다.
해결 방안
테스트가 성공하려면 다음 조건을 갖춰야 한다.
1. crew 객체가 1차 캐시에 저장된다. (db에서 조회 X)
2. crew의 id를 가지고 조회할 때, 1차 캐시에서 crew의 id를 식별자로 하여 가져온다.
이렇게 되면 1차 캐시에 저장된 crew와 1차 캐시에서 조회된 crew는 같은 객체이기 때문에 동등성이 성립된다.
결론 : KOI == crew == findCrew
1차 캐시에 저장 및 조회를 하려면 각 트랜잭션이 합쳐져야 한다.
따라서 테스트 메소드에 @Transactional을 적용하여 메소드 전체를 하나의 트랜잭션으로 설정한다.


트랜잭션 전파(Propagation)
save()와 findById()의 @Transactional의 전파는 기본값인 REQUIRED 로 되어 있다.
따라서 find() 테스트 메소드의 @Transactional을 붙여줌으로써, find() 테스트 메소드의 트랜잭션에 참여하게 된다.
3. saveAndCompare(), find() 통합 테스트

이제 find() 독립적으로 실행할 때, 트랜잭션(영속성 컨텍스트)인 것도 알았으니 통합 테스트를 실행해도 당연히 되겠지?

그렇지 않다. saveAndCompare()는 성공하지만 find()는 실패한다.


조회한 엔티티와 KOI가 달라서 발생하는 문제였다. (준영속과 merge에서 발생한 문제)
- saveAndCompare()
- 비영속인 KOI 엔티티를 저장한다. (영속)
- 1차 캐시 저장 및 db에 플러시 되면서 KOI id의 식별자가 저장된다.
- save() 트랜잭션이 종료된 후 영속성 컨텍스트는 종료된다.
- find()
- 이미 식별자를 갖고 있는 준영속인 KOI 엔티티를 저장하려고 한다.
- 이때 1차 캐시에 있는지 확인 후, 없으면 db에 SELECT 쿼리를 1회 날려 조회한다.
- 준영속인 KOI는 merge(병합)되면서 영속 상태로 변경되어 crew 변수에 저장된다. (매개변수의 KOI와 다름)
- 따라서 crew의 id를 갖고 조회한 findCrew(영속)와 KOI(준영속)는 동등성에 위배된다.
- 이미 식별자를 갖고 있는 준영속인 KOI 엔티티를 저장하려고 한다.


위 로그를 보면 더 쉽게 이해할 수 있다!
해결 방법은 유튜브에 있으니 유튜브를 봐주시면 좋을 거 같아요!!!!!
(https://www.youtube.com/watch?v=kJexMyaeHDs, 5분 15초부터!)
느낀점
개발을 하면서 트랜잭션은 신경 써서 개발하려고 노력하였지만, 영속성 컨텍스트나 영속 상태, 1차 캐시와 같이 기본적인 내용은 인지하지 않은 상태에서 개발을 했다.. 저번 프로젝트도 그렇고 개발을 하면서 느끼는 점은, "다 공부 했던 건데.." 라는 생각이 참 많이 든다.
수박겉핥기 식으로 공부를 한 거 같아서,, 2025년부터는 스터디도 하면서 계속 해서 공부한 내용 리마인드도 하고 더 깊게 공부하고 많이 찾아봐야겠다는 생각이 들었다.
짧은 테스트 코드가 JPA의 기본적인 이해를 돕는데 나에게 큰 도움이 된 거 같다. 이론만 봤을 때, 사실 "이정도면 됐지" 라는 식으로 공부하고(읽기만 하고) 넘겼었는데, 한 번 공부할 때 제대로 진득하게 해야겠다고 느꼈다.
요새 들어 개발이 참 재밌어지는 것 같다 😆
2025년에는 지금 이 순간을 기억하며 개발하기를..!!!!!
실패 이유와 해결 방안이 정답과 다를 수 있어요..!
정답을 아시거나 의견이 있으시면 편하게 댓글로 남겨주세요 🥹
'Backend > JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 10장. Criteria 쿼리 (0) | 2024.06.21 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (1) (1) | 2024.06.16 |
[자바 ORM 표준 JPA 프로그래밍] 9장. 값 타입 (0) | 2024.03.17 |
[자바 ORM 표준 JPA 프로그래밍] 8장. 프록시와 연관관계 관리 (0) | 2024.02.25 |
[자바 ORM 표준 JPA 프로그래밍] 7장. 고급 매핑 (0) | 2024.02.22 |